今天遇到一枚crash,利用堆棧,初步判斷原因是“多線程寫DB”,問題代碼大致如下:
NSMutableArray *arr;
@synchronized(arr) {
arr = [self func]; // func方法中有寫DB操作
if(arr == nil) {
arr = [NSMutableArray array];
}
}
可是這里明明用了同步鎖@synchronized,為什么還會(huì)有多個(gè)線程同時(shí)進(jìn)入block呢?老套路,重寫得到如下C++實(shí)現(xiàn):
static void _I_Demo_synchronizedTest(Demo * self, SEL _cmd) {
NSMutableArray *arr;
{
id _sync_obj = (id)arr;
objc_sync_enter(_sync_obj); // 同步鎖進(jìn)入,參數(shù)是arr
try {
struct _SYNC_EXIT {
_SYNC_EXIT(id arg) : sync_exit(arg) {}
~_SYNC_EXIT() {objc_sync_exit(sync_exit); // 同步鎖退出,參數(shù)是arr
}
id sync_exit;
} _sync_exit(_sync_obj);// 調(diào)用結(jié)構(gòu)體的構(gòu)造函數(shù),參數(shù)是arr
} catch (id e) {
}
}
}
進(jìn)一步,查看objc_sync_enter和objc_sync_exit的源碼實(shí)現(xiàn),如下:
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
// 根據(jù)obj獲取對(duì)應(yīng)的SyncData節(jié)點(diǎn),id2data函數(shù)在下面有解析
SyncData* data = id2data(obj, ACQUIRE);// 上鎖
result = recursive_mutex_lock(&data->mutex); }
else
{ // @synchronized(nil) does nothing
}
return result;
}
以下:
int objc_sync_exit(id obj)
{ int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, RELEASE); // 釋放鎖
result = recursive_mutex_unlock(&data->mutex);
} else {
// @synchronized(nil) does nothing
}
return result;
}
從上面源碼可以看出:
1、@synchronized用的是遞歸鎖(即同個(gè)線程可重入,而不會(huì)導(dǎo)致死鎖);
2、@synchronized(nil)是不上鎖的
接著看看如下關(guān)鍵的數(shù)據(jù)結(jié)構(gòu),顯然,SyncList是個(gè)單鏈表,SyncData是單鏈表節(jié)點(diǎn),而整體存儲(chǔ)則是一個(gè)“拉鏈法哈希表”。
typedef struct SyncData {
struct SyncData* nextData; // 指向下一個(gè)SyncData節(jié)點(diǎn)的指針
DisguisedPtr<objc_object> object; // @synchronized的參數(shù)obj
int32_t threadCount; // number of THREADS using this block
recursive_mutex_t mutex; // 遞歸鎖
} SyncData;
struct SyncList {
SyncData *data; // 單鏈表頭指針
spinlock_t lock; // 保證多線程安全訪問該鏈表
SyncList() : data(nil) { }
};
define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists; // 哈希表,key:obj,value:?jiǎn)捂湵?
// 根據(jù)obj獲取對(duì)應(yīng)的SyncData節(jié)點(diǎn)static SyncData* id2data(id object, enum usage why)
{
spinlock_t *lockp = &LOCK_FOR_OBJ(object); // SyncList鎖
SyncData **listp = &LIST_FOR_OBJ(object); // obj對(duì)應(yīng)的SyncData節(jié)點(diǎn)所在的
SyncList SyncData* result = NULL;// 這里省略一大坨cache代碼
lockp->lock();
{
SyncData* p;
SyncData* firstUnused = NULL;
// 遍歷單鏈表
for (p = *listp; p != NULL; p = p->nextData) {
if ( p->object == object ) {
// 找到obj對(duì)應(yīng)的SyncData節(jié)點(diǎn)
result = p;
// SyncData節(jié)點(diǎn)對(duì)應(yīng)的線程數(shù)加1
OSAtomicIncrement32Barrier(&result->threadCount);
goto done;
}
// SyncData節(jié)點(diǎn)對(duì)應(yīng)的遞歸鎖沒有線程在用了,回收重用,可以節(jié)省節(jié)點(diǎn)創(chuàng)建的時(shí)間和空間
if ( (firstUnused == NULL) && (p->threadCount == 0) )
firstUnused = p;
}
// 鏈表中還沒有obj對(duì)應(yīng)的SyncData節(jié)點(diǎn),但是有可重用的SyncData節(jié)點(diǎn)
// an unused one was found, use it
if ( firstUnused != NULL ) {
result = firstUnused;
result->object = (objc_object *)object;
result->threadCount = 1;
goto done;
}
}
// 鏈表中還沒有obj對(duì)應(yīng)的SyncData節(jié)點(diǎn),而且沒有可重用的SyncData節(jié)點(diǎn)
result = (SyncData*)calloc(sizeof(SyncData), 1);
result->object = (objc_object *)object;
result->threadCount = 1;
new (&result->mutex) recursive_mutex_t();
// 新建的SyncData節(jié)點(diǎn)往鏈表頭部加
result->nextData = *listp;
*listp = result;
done:
lockp->unlock();
return result;}
}
template<typename T>
class StripedMap {
#if TARGET_OS_EMBEDDED
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };#endif
static unsigned int indexForPointer(const void *p) {
// 取obj地址的哈希值作為數(shù)組的index
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}
public:
T& operator[] (const void *p) {
return array[indexForPointer(p)].value;
}
};
搞清楚了@synchronized的源碼實(shí)現(xiàn),再回頭看看crash,問題主要有兩個(gè):
1、arr沒有初始化時(shí)為nil,同步鎖沒生效,block并非臨界區(qū);
2、arr被修改了,即內(nèi)存地址并非常量,線程1拿到arr對(duì)應(yīng)的地址為addr1,進(jìn)入block;線程2拿到
arr對(duì)應(yīng)的地址為addr2,同樣可以進(jìn)入block,而不會(huì)等待線程1執(zhí)行完block。
參考鏈接:
https://opensource.apple.com/source/objc4/objc4-680/runtime/objc-sync.mm
https://github.com/opensource-apple/objc4/blob/master/runtime/objc-private.h