今天遇到一枚crash,利用堆栈,初步判断原因是“多线程写DB”,问题代码大致如下:
NSMutableArray *arr;
@synchronized(arr) {
arr = [self func]; // func方法中有写DB操作
if(arr == nil) {
arr = [NSMutableArray array];
}
}
可是这里明明用了同步锁@synchronized,为什么还会有多个线程同时进入block呢?老套路,重写得到如下C++实现:
static void _I_Demo_synchronizedTest(Demo * self, SEL _cmd) {
NSMutableArray *arr;
{
id _sync_obj = (id)arr;
objc_sync_enter(_sync_obj); // 同步锁进入,参数是arr
try {
struct _SYNC_EXIT {
_SYNC_EXIT(id arg) : sync_exit(arg) {}
~_SYNC_EXIT() {objc_sync_exit(sync_exit); // 同步锁退出,参数是arr
}
id sync_exit;
} _sync_exit(_sync_obj);// 调用结构体的构造函数,参数是arr
} catch (id e) {
}
}
}
进一步,查看objc_sync_enter和objc_sync_exit的源码实现,如下:
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
// 根据obj获取对应的SyncData节点,id2data函数在下面有解析
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)是不上锁的
接着看看如下关键的数据结构,显然,SyncList是个单链表,SyncData是单链表节点,而整体存储则是一个“拉链法哈希表”。
typedef struct SyncData {
struct SyncData* nextData; // 指向下一个SyncData节点的指针
DisguisedPtr<objc_object> object; // @synchronized的参数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:单链表
// 根据obj获取对应的SyncData节点static SyncData* id2data(id object, enum usage why)
{
spinlock_t *lockp = &LOCK_FOR_OBJ(object); // SyncList锁
SyncData **listp = &LIST_FOR_OBJ(object); // obj对应的SyncData节点所在的
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节点
result = p;
// SyncData节点对应的线程数加1
OSAtomicIncrement32Barrier(&result->threadCount);
goto done;
}
// SyncData节点对应的递归锁没有线程在用了,回收重用,可以节省节点创建的时间和空间
if ( (firstUnused == NULL) && (p->threadCount == 0) )
firstUnused = p;
}
// 链表中还没有obj对应的SyncData节点,但是有可重用的SyncData节点
// an unused one was found, use it
if ( firstUnused != NULL ) {
result = firstUnused;
result->object = (objc_object *)object;
result->threadCount = 1;
goto done;
}
}
// 链表中还没有obj对应的SyncData节点,而且没有可重用的SyncData节点
result = (SyncData*)calloc(sizeof(SyncData), 1);
result->object = (objc_object *)object;
result->threadCount = 1;
new (&result->mutex) recursive_mutex_t();
// 新建的SyncData节点往链表头部加
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地址的哈希值作为数组的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的源码实现,再回头看看crash,问题主要有两个:
1、arr没有初始化时为nil,同步锁没生效,block并非临界区;
2、arr被修改了,即内存地址并非常量,线程1拿到arr对应的地址为addr1,进入block;线程2拿到
arr对应的地址为addr2,同样可以进入block,而不会等待线程1执行完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