IOS多线程 - @synchronized

2,607 阅读8分钟

本文首发于 个人博客

在IOS开发中,同步锁相信大家都使用过,即 @synchronized ,这篇文章向大家介绍一些 @synchronized的原理和使用。

@synchronized 原理

@synchronized 是IOS多线程同步中性能最差的:

却是使用起来最方便的一个,通常我们这么用:

 @synchronized (self) {
        // code
    }

为了了解其底层是如何 实现的,我们在测试工程的ViewController.m中写了一段代码

static NSString *token = @"synchronized-token";
- (void)viewDidLoad {
    [super viewDidLoad];
    @synchronized (token) {
        // code
        NSLog(@"haha");
    }
}

点击Xcode--> Debug -->Debug Workflow --> Always Show Disassembly显示汇编,打上断点启动真机,就看到了如下代码:

上图中的所有方法的调用我都圈出来了,ARC帮我们自动插入了retainrelease,而且还找到了NSLog的方法,那么包含NSLog的正是 objc_sync_enter objc_sync_exit 我们猜测这个应该就是 @synchronized 的具体实现,所以我们来到 Objective-c源码 处查找,很快我们发现了这两个函数的实现:

// Begin synchronizing on 'obj'. 
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.  
int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
        SyncData* data = id2data(obj, ACQUIRE);
        assert(data);
        data->mutex.lock();
    } else {
        // @synchronized(nil) does nothing
        if (DebugNilSync) {
            _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
        }
        objc_sync_nil();
    }

    return result;
}


// End synchronizing on 'obj'. 
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
        SyncData* data = id2data(obj, RELEASE); 
        if (!data) {
            result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
        } else {
            bool okay = data->mutex.tryUnlock();
            if (!okay) {
                result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
            }
        }
    } else {
        // @synchronized(nil) does nothing
    }


    return result;
}

从官方代码和注释中我们可以得出:

  • 使用@synchronized 会创建一个递归(recursive)互斥(mutex)的锁与 obj参数进行关联。
  • @synchronized(nil)does nothing

递归锁其实就是为了方便同步锁的嵌套使用而不会出现死锁的情况:

static NSString *token = @"synchronized-token";
static NSString *token1 = @"synchronized-token1";
- (void)viewDidLoad {
    [super viewDidLoad];
    @synchronized (token) {
        NSLog(@"haha");
        @synchronized (token1) {
            NSLog(@"didi");
        }
    }
}

@synchronized(nil) 不起任何作用,说明我们要注意传入obj 的生命周期,因为当obj被释放这个地方就起不到加锁的作用,有同学可能注意到我这为什么没有用self 作为obj传递参数,就是为了避免token被多个地方持有修改,一旦出现nil,可能就会出现线程安全问题,这块我会在后面去验证。

obj 是如何保存的

我们的@synchronized是针对传入参数obj做绑定的,那么内部obj究竟是干嘛用的,而且我们知道@synchronizedobject-c全局都可以使用,那么@synchronized是如何区分不同的obj进行一一对应的,带着这些问题,我们看看底层对obj究竟是如何处理的。

首先我们看到底层是这样处理传入的对象 obj 的:

SyncData* data = id2data(obj, ACQUIRE);

内部都会将 obj 转化成相应的 SyncData 类型的对象,然后 id2data 内部是下面这样取的:

SyncData **listp = &LIST_FOR_OBJ(object);

看看LIST_FOR_OBJ 是如何操作obj的(下面代码留下关键部分,具体细节请自行前往源码查看):

 1// obj传入sDataLists
2#define LIST_FOR_OBJ(obj) sDataLists[obj].data
3
4// 哈希表结构,内部存SyncList
5static StripedMap<SyncList> sDataLists;
6
7// SyncList结构体,内部data就是SyncData
8struct SyncList {
9    SyncData *data;
10    spinlock_t lock;
11    constexpr SyncList() : data(nil), lock(fork_unsafe_lock{ }
12};
13
14// 哈希表结构
15class StripedMap {
16    enum { StripeCount = 64 };
17
18    struct PaddedT {
19        value alignas(CacheLineSize);
20    };
21
22    PaddedT array[StripeCount];
23
24    // 哈希函数
25    static unsigned int indexForPointer(const void *p{
26        uintptr_t addr = reinterpret_cast<uintptr_t>(p);
27        return ((addr >> 4) ^ (addr >> 9)) % StripeCount; 
28
29    }
30
31 public:
32   // 此处的p就是上面的obj,也就是obj执行上面的哈希函数对应到数组的index
33    T& operator[] (const void *p) { 
34        return array[indexForPointer(p)].value
35    }

从上述代码看出整体StripedMap是一个哈希表结构,表外层是一个数组,数组里的每个位置存储一个类似链表的结构(SyncList),SyncData 存储的位置具体依赖第25行处的哈希函数,如图:

obj1 处,经过哈希函数计算得出索引2,起初我们要顺着上面的 A 线对List进行查找,没找到,将当前的obj插入到最前面,也是为了更快的找到当前使用的对象而这么设计。

    // Allocate a new SyncData and add to list.
    // XXX allocating memory with a global lock held is bad practice,
    // might be worth releasing the lock, allocating, and searching again.
    // But since we never free these guys we won't be stuck in allocation very often.
    posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
    result->object = (objc_object *)object;
    result->threadCount = 1;
    new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
    result->nextData = *listp;
    *listp = result;

obj2 处就不多分析了,找到直接返回,进行加锁解锁处理。

慎用@synchronized(self)

@synchronized 中传入object的内存地址,被用作key,通过一定的hash函数映射到一个系统全局维护的递归锁中,所以不论传入什么类型的值,只要它有内存地址就可以达到同步锁的效果。

引用这篇文章开头所说的例子:

通常我们直接用@synchronized(self)

没毛病,但是很粗糙,也确实存在问题。是不是他喵的被我说乱了,到底有没有问题?

    @property (nonatomicstrongNSMutableArray *array;

    for (NSInteger i = 0; i < 20000; i ++) {
        dispatch_async(dispatch_get_global_queue(00), ^{
            self->_array = [NSMutableArray array];
        });
    }

这块代码我们知道是有问题的,会直接Crash,原因就在于多线程同时操作array,导致在某一个瞬间可能同时释放了多次,也就是野指针的问题。那么我们尝试用今天的 @synchronized 来同步锁一下:

    for (NSInteger i = 0; i < 20000; i ++) {
        dispatch_async(dispatch_get_global_queue(00), ^{
            @synchronized (self->_array) {
                self->_array = [NSMutableArray array];
            }
        });
    }

调试一下发现依然崩溃,@synchronized 不是加锁吗,怎么还会Crash 呢?

原来我们绑定的对象是array,而内部多线程对array的操作却是频繁的创建和release,当某个瞬间arry执行了release的时候就达成了我们所说的 @synchronized(nil) ,上文已经分析了这个时候do nothing ! 所以能起到同步锁的作用么,很显然不能,这就是崩溃的主要原因。

由此可见@synchronized 在一些不断的循环,递归的时候并不如人意,我们唯独要注意obj的参数唯一化,也就是与所要锁的对象一一对应,这样就避免了多个地方持有obj。

    static NSString *token = @"synchronized-token";
    @synchronized (token) {
        self.array1 = [NSMutableArray array];
    }
    --------------------------------------------------
    static NSString *another_token = @"another_token";
    @synchronized (another_token) {
        self.array2 = [NSMutableArray array];
    }

总结

虽然 @synchronized 使用起来很简单,但是其内部的实现却是很复杂,尽管其性能很差,适当的地方使用也无可厚非,无非就是注意对象的生命周期,以及内部的嵌套等。希望这篇文章能把@synchronized 讲清楚,欢迎指正和沟通。