IOS多线程 - @synchronized

本文首发于 我的博客git

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

@synchronized 原理

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

倒是使用起来最方便的一个,一般咱们这么用:sass

 @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显示汇编,打上断点启动真机,就看到了以下代码:markdown

上图中的全部方法的调用我都圈出来了,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;
}
复制代码

从官方代码和注释中咱们能够得出:async

  • 使用@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,可能就会出现线程安全问题,这块我会在后面去验证。oop

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 讲清楚,欢迎指正和沟通。

相关文章
相关标签/搜索