看过 Andrei Alexandrescu 老师的 《锁无关的(Lock-Free)数据结构》
和 《锁无关的数据结构与Hazard指针》两篇文章,感觉受益颇深
前文给出一个 WRRMBNTM (Write-Rarely-Read-Many-But-Not-Many,少写多 读,但不是太多)的map
后文给出一个 Hazard 指针彻底解决这个问题,真正实现 WRRM 的map
不过看他的解法好像有点复杂,看了半天才明白,原来是实现了一种垃圾回收机制(里面的Hazard指针非常值得借鉴)
还要使用线程局部存储 为啥C语言解决一个资源释放的问题要这么麻烦?
本人经过分析和实验,给出如下解决方案:
具体思想是在对象还能被全局访问到时使用全局的那个引用计数,在对象不能被全局访问到(只能被正在读取它的线程访问到)时
使用对象备份的引用计数,引用计数降为0则销毁对象。
以下是具体的实现代码和测试程序(windows下编写,使用的Interlocked API)
#include "stdafx.h"
#include <windows.h>
#include <winbase.h>
#include <process.h>
#include <stdio.h>
//对象的引用
struct object
{
void* pAnyRes; //point to any resource etc list,map...
int nref_count;
int nTmpRelease; //对象临时使用的,释放计数,为解决 写线程意外死亡造成的 活锁问题
};
typedef struct object object;
//对象的拥有者
struct owner
{
union
{
struct
{
object* pobject;
int nref_count;
};
__int64 nobj_count;
};
};
typedef struct owner owner;
#ifdef _DEBUG
LONG g_nNewCount = 0;
LONG g_nDelCount = 0;
#define DEBUG_INC( X ) InterlockedIncrement( (volatile long*)&X)
#else
#define DEBUG_INC( X )
#endif
#define INVALID_REF_COUNT (-100) //标识一个无效的引用计数
//全局的引用者
owner g_owner;
//获得对象指针
object* Acquire()
{
owner stuOld,stuNew;
do
{
stuOld.pobject = g_owner.pobject;
stuOld.nref_count = g_owner.nref_count;
stuNew.pobject = stuOld.pobject;
stuNew.nref_count = stuOld.nref_count + 1;
} while ( stuOld.nobj_count != InterlockedCompareExchange64( (LONGLONG volatile*)&g_owner.nobj_count , stuNew.nobj_count , stuOld.nobj_count ) );
return stuNew.pobject;
}
void Release( object* pobject )
{
bool bReleaseInOwner = (pobject == g_owner.pobject);
if( bReleaseInOwner )//当前对象还是当初访问的对象
{
owner stuOld,stuNew;
owner stuTest;
do
{
stuOld.pobject = stuNew.pobject = pobject;
stuOld.nref_count = g_owner.nref_count;
stuNew.nref_count = stuOld.nref_count - 1;
stuTest.nobj_count = InterlockedCompareExchange64( (volatile __int64*)&g_owner , stuNew.nobj_count , stuOld.nobj_count );
if( stuTest.nobj_count == stuOld.nobj_count )
{
if( stuNew.nref_count == 0 )
{
DEBUG_INC( g_nDelCount );
delete pobject;
}
break;
}
else if( stuTest.pobject != stuOld.pobject )//原对象已被更新
{
bReleaseInOwner = false;
break;
}
} while ( true );
}
if( !bReleaseInOwner )
{
bool bReleaseObject = true;
if( pobject->nref_count == INVALID_REF_COUNT )
{
object stuOldObj,stuNewObj,stuTestObj;
do
{
stuOldObj.nref_count = INVALID_REF_COUNT;
stuOldObj.nTmpRelease = pobject->nTmpRelease;
stuNewObj.nref_count = stuOldObj.nref_count;
stuNewObj.nTmpRelease = stuOldObj.nTmpRelease + 1;
__int64 llOld = *(__int64*)&stuOldObj.nref_count;
__int64 llNew = *(__int64*)&stuNewObj.nref_count;
__int64 llTest = InterlockedCompareExchange64( (volatile __int64*)&pobject->nref_count , llNew , llOld );
if( llTest == llOld )
{
bReleaseObject = false;
break;
}
else
{
*((__int64*)&(stuTestObj.nref_count)) = llTest;
if( stuTestObj.nref_count != stuOldObj.nref_count )//对象的引用计数已经更新
{
break;
}
}
} while ( true );
}
if( bReleaseObject )
{
long nRet = InterlockedDecrement( (volatile long*)&pobject->nref_count );
if( nRet == 0 )
{
DEBUG_INC( g_nDelCount );
delete pobject;
}
else if( nRet < 0 )
{
DEBUG_INC( g_nDelCount );
delete pobject; //must error
}
}
}
}
bool Update( object* pOld , object* pNew )
{
owner stuOld,stuNew;
owner stuTest;
bool bret = false;
do
{
stuOld.pobject = pOld;
stuNew.pobject = pNew;
stuNew.nref_count = 1;
pNew->nref_count = INVALID_REF_COUNT;
pNew->nTmpRelease = 0;
stuOld.nref_count = g_owner.nref_count;
InterlockedCompareExchange64( (volatile __int64*)pOld , stuOld.nobj_count , stuOld.nobj_count );
stuTest.nobj_count = InterlockedCompareExchange64( (volatile __int64*)&g_owner , stuNew.nobj_count , stuOld.nobj_count );
if( stuTest.nobj_count == stuOld.nobj_count )
{
bret = true;
//复制引用计数
object stuOldObj,stuNewObj,stuTestObj;
do
{
stuOldObj.nref_count = pOld->nref_count;
stuOldObj.nTmpRelease = pOld->nTmpRelease;
stuNewObj.nref_count = stuOld.nref_count - stuOldObj.nTmpRelease; //注意,这里更新
stuNewObj.nTmpRelease = 0;
__int64 llOld = *(__int64*)&stuOldObj.nref_count;
__int64 llNew = *(__int64*)&stuNewObj.nref_count;
__int64 llTest = InterlockedCompareExchange64( (volatile __int64*)&pOld->nref_count , llNew , llOld );
if( llTest == llOld )
{
*((__int64*)&(stuTestObj.nref_count)) = llTest;
if( stuNewObj.nref_count == 0 )//实际上是不可能的,因为在Update的时候 WriteThread自己还保留着1个,全局还有1个至少2个
{
delete pOld;
}
break;
}
} while (true);
break;
}
else if( stuTest.pobject != stuOld.pobject )
{
break;
}
} while ( true );
return bret;
}
UINT CALLBACK ReadThread( void* pCount )
{
UINT uCount = (UINT)pCount;
do
{
object* pObject = Acquire();
// ... do some read on pObject
Release( pObject );
uCount --;
} while ( uCount > 0 );
return 0;
}
UINT CALLBACK WriteThread( void* pCount )
{
UINT uCount = (UINT)pCount;
do
{
object* pObject = NULL;
object* pNew = NULL;
do
{
if( pNew != NULL )
{
DEBUG_INC( g_nDelCount );
delete pNew;
}
if( pObject != NULL )
{
Release( pObject );
}
pObject = Acquire();
DEBUG_INC( g_nNewCount );
pNew = new object;
// ... do some modify on pNew
} while ( !Update( pObject , pNew ) );
if( pObject != NULL )
{
Release( pObject ); //一次是自己的
Release( pObject ); //一次是全局的那次
}
uCount --;
} while ( uCount > 0 );
return 0;
}
int main(int argc, char* argv[])
{
int nSize = sizeof(object);
DEBUG_INC( g_nNewCount );
g_owner.pobject = new object;
g_owner.pobject->nTmpRelease = 0;
g_owner.nref_count = 1;
#define READ_COUNT 10000000
#define WRITE_COUNT 1000000
#define THREAD_COUNT 4
HANDLE pWaitHandle[THREAD_COUNT];
UINT uThreadID = 0;
pWaitHandle[0] = (HANDLE)_beginthreadex( NULL , 0 , ReadThread , (void*)READ_COUNT , 0 , &uThreadID );
pWaitHandle[1] = (HANDLE)_beginthreadex( NULL , 0 , ReadThread , (void*)READ_COUNT , 0 , &uThreadID );
pWaitHandle[2] = (HANDLE)_beginthreadex( NULL , 0 , WriteThread , (void*)WRITE_COUNT , 0 , &uThreadID );
pWaitHandle[3] = (HANDLE)_beginthreadex( NULL , 0 , ReadThread , (void*)READ_COUNT , 0 , &uThreadID );
WriteThread( (void*)WRITE_COUNT );
WaitForMultipleObjects( THREAD_COUNT , pWaitHandle , TRUE , INFINITE );
#ifdef _DEBUG
printf( "new %d delete %d det is %d/n",g_nNewCount , g_nDelCount , g_nNewCount - g_nDelCount );
#endif
return 0;
}
上述解法也存在问题,就是如果任何写线程 在 更新完指针 之后 在 pOld->nref_count = stuOld.nref_count; 之前 突然被杀掉
那么其他的读线程在Release的时候会 死循环 ,不过这种可能性 发生的几率极小,如何防微杜渐,请看下回分解。
参考文献
《lock-free Data Structures》 http://erdani.org/publications/cuj-2004-10.pdf
刘未鹏 同学翻译的:(真是个大善人啊)
http://blog.csdn.net/pongba/archive/2006/01/26/588638.aspx
http://blog.csdn.net/pongba/archive/2006/01/29/589864.aspx