IOS - @synchronized详解

本文首发于 个人博客

在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_enterobjc_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
5  static StripedMap<SyncList> sDataLists;
6
7  // SyncList结构体,内部data就是SyncData
8  struct SyncList {
9      SyncData *data;
10    spinlock_t lock;
11    constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
12  };
13
14  // 哈希表结构
15  class StripedMap {
16      enum { StripeCount = 64 };
17
18    struct PaddedT {
19        T 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 (nonatomic, strong) NSMutableArray *array;

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

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

for (NSInteger i = 0; i < 20000; i ++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            @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 又不停的对这个哈希表进行增删改查,所以其性能很差,但是适当的地方使用也无可厚非,无非就是注意对象的生命周期,以及内部的嵌套等。希望这篇文章能把@synchronized 讲清楚,欢迎指正和沟通。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容