对象引用的正确性在多线程环境下是一个复杂的问题,请参考,处理由引用计数引发的泄漏.简单来讲,咱们应该尽可能减小使用强引用,不然将有可能产生[处理由引用计数引发的泄漏]一文中描述的难以察觉的内存泄漏问题.也就是说,大多数状况下咱们应该使用一个弱引用来指代一个对象,当咱们真正须要访问这个对象的时候才将其转换成实际的对象.因此能够弱引用理解为一种handle,它只是底层对象表示的一层间接引用.html
能够考虑以下场景:算法
咱们设计了一种网络库,分为IO层和逻辑层,IO层管理实际的socket对象,而逻辑层能看到的只是socket的一个handle.逻辑层须要发送数据的时候,将handle和数据一块儿打包交给IO层,IO层把handle转化成实际的socket对象并完成数据发送.那么问题来了,若是在IO层收到一个发送请求时,那个handle对应的socket实际上已经销毁,那么对handle的转换就应该反映出这种状况,让转换返回一个空指针.网络
由于在[处理由引用计数引发的泄漏]描述的算法中,refobj *cast2refobj(ident _ident);
和atomic_32_t refobj_dec(refobj *r);
两个方法是相当重要,而且实现相对复杂,因此本文主要目的就是介绍这两个函数的做用及其正确性.多线程
咱们首先来看下refobj_dex
:socket
atomic_32_t refobj_dec(refobj *r) { atomic_32_t count; int c; struct timespec ts; assert(r->refcount > 0); if((count = ATOMIC_DECREASE(&r->refcount)) == 0){ r->identity = 0; c = 0; for(;;){ if(COMPARE_AND_SWAP(&r->flag,0,1)) break; if(c < 4000){ ++c; __asm__("pause"); }else{ ts.tv_sec = 0; ts.tv_nsec = 500000; nanosleep(&ts, NULL); } } r->destructor(r); } return count; }
关键部分在引用计数为0,要准备销毁对象的分支.首先将对象的identity置0,而后在一个for循环中对尝试flag变量置1,只有当设置成功才会退出循环执行最后的析构函数.这里的主要迷惑之一是for循环和flag变量的做用是什么.让咱们先看下cast2refobj
的实如今回来讨论;ide
refobj *cast2refobj(ident _ident) { refobj *ptr = NULL; if(!_ident.ptr) return NULL; TRY{ refobj *o = (refobj*)_ident.ptr; do{ atomic_64_t identity = o->identity; if(_ident.identity == identity){ if(COMPARE_AND_SWAP(&o->flag,0,1)){ identity = o->identity; if(_ident.identity == identity){ if(refobj_inc(o) > 1) ptr = o; else ATOMIC_DECREASE(&o->refcount); } o->flag = 0; break; } }else break; }while(1); }CATCH_ALL{ ptr = NULL; }ENDTRY; return ptr; }
cast2refobj
的做用就是将一个handle转换成对象,若是对象未被销毁返回对象,不然返回NULL.在do循环中,首先判断handle保存的identity与实际对象的是否一致,若是不一致代表handle中存放的对象确定已经不是原来的对象了,因此返回NULL.而当identity一致的时候,首先作的第一件事又是对flag置1.可见这个flag是这个算法的重点.如今咱们来讨论flag的做用.函数
flag主要由两个做用:atom
1) 防止多个线程同时进入cast2refobj
的核心部分,让咱们考虑如下场景:线程
有A,B,C3个线程,A线程执行refobj_dec
,在成功执行if((count = ATOMIC_DECREASE(&r->refcount)) == 0)
以后,r->identity = 0;
以前暂停.B,C则几乎同时执行cast2refobj
,这个时候由于identity还未被清0,因此B,C看到的identity必然与其持有的handle的保持一致,若是没有if(COMPARE_AND_SWAP(&o->flag,0,1))
这行代码咱们看看会发生什么事情.假设B线程先执行, 它执行if(refobj_inc(o) > 1)的时候返回值应该是1,那么条件判断失败,没有将ptr设置为o,因此ptr依旧是NULL.但在执行完判断在准备执行另外一个分支的ATOMIC_DECREASE(&o->refcount);
以前它也被暂停,那么当C执行if(refobj_inc(o) > 1)
它会进入ptr=o
的分支(由于refobj_inc(o)会返回2),也就是说,转换成功,而实际上返回的倒是一个正准备销毁的对象.flag就是为了防止这种状况的发生,它使得多个线程执行cast2refobj
的时候,只能互斥的进入if(refobj_inc(o) > 1)
.设计
2) 让r->destructor
延后执行,使得执行cast2refobj
并已经进入if(COMPARE_AND_SWAP(&o->flag,0,1))
内部的线程先退出cast2refobj
,而后再执行 r->destructor
.
另外还要注意的是cast2refobj
是被TRY CATCH所保护的,这样作的缘由在于,在内存压力大的状况下,被销毁对象的内存可能马上归还给系统,那么对对象的访问将会产生访问异常.咱们必须捕获这个异常,同时让函数返回NULL(异常出现代表handle持有的对象一定是非法的了).