今天遇到一枚crash椰棘,利用堆棧,初步判斷原因是“多線程寫DB”梭伐,問題代碼大致如下:
NSMutableArray *arr;
@synchronized(arr) {
arr = [self func]; // func方法中有寫DB操作
if(arr == nil) {
arr = [NSMutableArray array];
}
}
可是這里明明用了同步鎖@synchronized医窿,為什么還會有多個線程同時進入block呢?老套路统阿,重寫得到如下C++實現(xiàn):
static void _I_Demo_synchronizedTest(Demo * self, SEL _cmd) {
NSMutableArray *arr;
{
id _sync_obj = (id)arr;
objc_sync_enter(_sync_obj); // 同步鎖進入彩倚,參數(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);// 調用結構體的構造函數(shù)扶平,參數(shù)是arr
} catch (id e) {
}
}
}
進一步署恍,查看objc_sync_enter和objc_sync_exit的源碼實現(xiàn),如下:
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
// 根據(jù)obj獲取對應的SyncData節(jié)點蜻直,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用的是遞歸鎖(即同個線程可重入,而不會導致死鎖)概而;
2呼巷、@synchronized(nil)是不上鎖的
接著看看如下關鍵的數(shù)據(jù)結構,顯然赎瑰,SyncList是個單鏈表王悍,SyncData是單鏈表節(jié)點,而整體存儲則是一個“拉鏈法哈希表”餐曼。
typedef struct SyncData {
struct SyncData* nextData; // 指向下一個SyncData節(jié)點的指針
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:單鏈表
// 根據(jù)obj獲取對應的SyncData節(jié)點static SyncData* id2data(id object, enum usage why)
{
spinlock_t *lockp = &LOCK_FOR_OBJ(object); // SyncList鎖
SyncData **listp = &LIST_FOR_OBJ(object); // obj對應的SyncData節(jié)點所在的
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對應的SyncData節(jié)點
result = p;
// SyncData節(jié)點對應的線程數(shù)加1
OSAtomicIncrement32Barrier(&result->threadCount);
goto done;
}
// SyncData節(jié)點對應的遞歸鎖沒有線程在用了源譬,回收重用集惋,可以節(jié)省節(jié)點創(chuàng)建的時間和空間
if ( (firstUnused == NULL) && (p->threadCount == 0) )
firstUnused = p;
}
// 鏈表中還沒有obj對應的SyncData節(jié)點,但是有可重用的SyncData節(jié)點
// an unused one was found, use it
if ( firstUnused != NULL ) {
result = firstUnused;
result->object = (objc_object *)object;
result->threadCount = 1;
goto done;
}
}
// 鏈表中還沒有obj對應的SyncData節(jié)點踩娘,而且沒有可重用的SyncData節(jié)點
result = (SyncData*)calloc(sizeof(SyncData), 1);
result->object = (objc_object *)object;
result->threadCount = 1;
new (&result->mutex) recursive_mutex_t();
// 新建的SyncData節(jié)點往鏈表頭部加
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的源碼實現(xiàn)刮刑,再回頭看看crash,問題主要有兩個:
1、arr沒有初始化時為nil雷绢,同步鎖沒生效泛烙,block并非臨界區(qū);
2翘紊、arr被修改了蔽氨,即內存地址并非常量,線程1拿到arr對應的地址為addr1帆疟,進入block鹉究;線程2拿到
arr對應的地址為addr2,同樣可以進入block鸯匹,而不會等待線程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