对象引用的正确性在多线程环境下是一个复杂的问题,请参考,处理由引用计数引起的泄漏.简单来说,我们应该尽量减少使用强引用,否则将有可能产生[处理由引用计数引起的泄漏]一文中描述的难以察觉的内存泄漏问题.也就是说,大多数情况下我们应该使用一个弱引用来指代一个对象,当我们真正需要访问这个对象的时候才将其转换成实际的对象.所以可以弱引用理解为一种handle,它只是底层对象表示的一层间接引用.

可以考虑如下场景:

我们设计了一种网络库,分为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:

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的实现在回来讨论;

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主要由两个作用:

  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).

  1. r->destructor延后执行,使得执行cast2refobj并已经进入if(COMPARE_AND_SWAP(&o->flag,0,1))内部的线程先退出cast2refobj,然后再执行 r->destructor.

另外还要注意的是cast2refobj是被TRY CATCH所保护的,这样做的原因在于,在内存压力大的情况下,被销毁对象的内存可能立刻归还给系统,那么对对象的访问将会产生访问异常.我们必须捕获这个异常,同时让函数返回NULL(异常出现表明handle持有的对象必定是非法的了).

一种线程安全的handle的更多相关文章

  1. Java 四种线程池newCachedThreadPool,newFixedThreadPool,newScheduledThreadPool,newSingleThreadExecutor

    介绍new Thread的弊端及Java四种线程池的使用,对Android同样适用.本文是基础篇,后面会分享下线程池一些高级功能. 1.new Thread的弊端执行一个异步任务你还只是如下new T ...

  2. Java四种线程池的使用

    Java通过Executors提供四种线程池,分别为:newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程.newFixe ...

  3. Java四种线程池

    Java四种线程池newCachedThreadPool,newFixedThreadPool,newScheduledThreadPool,newSingleThreadExecutor 时间:20 ...

  4. 三种线程不安全现象描述(escaped state以及hidden mutable state)

    hidden mutable state和escaped state是两种线程不安全问题:两者原因不同,前者主要是由于类成员变量中含有其他对象的引用,而这个引用是immutable的:后者是成员方法的 ...

  5. Java四种线程池newCachedThreadPool,newFixedThreadPool,newScheduledThreadPool,newSingleThreadExecutor

    1.new Thread的弊端 执行一个异步任务你还只是如下new Thread吗? Java new Thread(new Runnable() { @Override public void ru ...

  6. Java通过Executors提供四种线程池

    http://cuisuqiang.iteye.com/blog/2019372 Java通过Executors提供四种线程池,分别为:newCachedThreadPool创建一个可缓存线程池,如果 ...

  7. JAVA四种线程池实例

    1.new Thread的弊端 执行一个异步任务你还只是如下new Thread吗?   Java   1 2 3 4 5 6 7 new Thread(new Runnable() {        ...

  8. AsyncTask两种线程池

        AsyncTask两种线程池  http://bbs.51cto.com/thread-1114378-1.html (API 3.0以后): 1.THREAD_POOL_EXECUTOR, ...

  9. Java 四种线程池的用法分析

    1.new Thread的弊端 执行一个异步任务你还只是如下new Thread吗? new Thread(new Runnable() { @Override public void run() { ...

随机推荐

  1. react纯前端不依赖于打包工具的代码

    ####react最基础的语法和不依赖环境的纯前端免编译代码 参照:http://www.ruanyifeng.com/blog/2015/03/react.html 注意事项:1.必须放倒服务器上, ...

  2. 我使用中的Linux命令和快捷键(For Ubuntu)

    虽然自己工作一直都在MS的平台体系,自然很多的工作都需要在windows上完成,自己的电脑也一直使用windows操作系统,但是笔记本配置不是很好,导致装windows 8后,笔记本卡的难以忍受.想想 ...

  3. fiddler 无法捕获apache httpclient报文的问题及解决

    问题如题,解决办法为在构建httpclient对象的时候设置代理,因为fiddler内置了一个代理,只有流量(traffic)经过这个代理,才能够被捕捉到. HttpHost proxy = ); C ...

  4. 尝试在mac上用dotnet cli运行asp.net core示例程序

    自从知道微软用dotnet cli取代dnx之后,一直在等dotnet cli支持asp.net core... 昨天看到这篇新闻(ASP.NET Core 1.0 Hello World)后,才知道 ...

  5. 深入理解java虚拟机【类加载机制】

    Java虚拟机类加载过程是把Class类文件加载到内存,并对Class文件中的数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的java类型的过程. 在加载阶段,java虚拟机需要完成以下 ...

  6. .Net下 自动执行MSI和EXE文件

    MSI是安装文件,需要系统自带的msiexec.exe来执行 var tempDir = @"D:\UploadFiles\SCADASetupWix.msi"; var star ...

  7. [ACM_其他] 总和不小于S的连续子序列的长度的最小值——尺缩法

    Description: 给定长度为n的整数数列,A[0],A[1],A[2]….A[n-1]以及整数S,求出总和不小于S的连续子序列的长度的最小值.如果解不存在,则输出0. Input: 输入数据有 ...

  8. 深入理解c++构造函数, 复制构造函数和赋值函数重载(operator=)

    注 以下代码编译及运行环境均为 Xcode 6.4, LLVM 6.1 with GNU++11 support, Mac OS X 10.10.2 调用时机 看例子 // // main.cpp / ...

  9. mysql where执行顺序

    where执行顺序是从左往右执行的,在数据量小的时候不用考虑,但数据量多的时候要考虑条件的先后顺序,此时应遵守一个原则:排除越多的条件放在第一个. 在用MySQL查询数据库的时候,连接了很多个过滤条件 ...

  10. 深入理解 CSS 的 :before 和 :after 选择器(制作select下拉列表美化插件)

    原文链接:http://www.cnblogs.com/LY-leo/p/5765598.html 对于 :before 和 :after 选择器,大家并不陌生,但是很少有人会主动去用它们.先解释下它 ...