怎样做才能保证线程安全?

在软件编程中,多线程是个绕不开的话题。多线程的使用,能够提高程序的运行效率,但也带来新的问题:如何保证线程安全?

在维基百科中线程安全的解释是:指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。换句话说,就是某个变量在被某条线程访问期间是“一致”的。这个“一致”指的是这条线程从开始访问这个变量到结束访问这个变量期间,这个变量不会发生任何变化。

那么,保证某个变量的线程安全,也就可以理解成保证某个变量在某个特定时间段内是一致的。这个某个特定时间,也就可以理解成为线程安全的原子性粒度,具体下面有介绍。

例子

具体到iOS上,经常能看到下面的代码例子:

// 例子1
@property (atomic, assign) int num;

// thread A
for (int i = 0; i < 10000; i++) {
    self.num = self.num + 1;
    NSLog(@"Thread A: %d\d ",self.num);
}

// thread B
for (int i = 0; i < 10000; i++) {
    self.num = self.num + 1;
    NSLog(@"Thread B: %d\d ",self.num);
}
// 例子2
@property (atomic, strong) NSString   * stringA;

//thread A
for (int i = 0; i < 10000; i ++) {
    if (i % 2 == 0) {
        self.stringA = @"a very long string";
    }
    else {
        self.stringA = @"string";
    }
    NSLog(@"Thread A: %@\n", self.stringA);
}

//thread B
for (int i = 0; i < 10000; i ++) {
    if (self.stringA.length >= 10) {
        NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];
    }
    NSLog(@"Thread B: %@\n", self.stringA);
}

例子A最后输出不一定是20000,例子B有可能会crash。这两个例子说明了一个问题:property加上atomic关键字,并不一定能保证属性的线程安全

线程安全的原子性粒度

那为什么用了atomic关键字不能保证上述场景的property变量的线程安全?

atomic关键字的作用其实就是对属性的读写操作进行加锁,换句话说就是对属性的Setter/Getter操作加锁。但atomic关键字只能保证在同一时间段内,最多有且只有一条线程对当前关键字进行读写。

例子1中self.num = self.num + 1;包含了三个操作:通过Getter读取num,对读取的num进行加1,将加1后的结果写回num。atomic关键字能保证每一个操作都是原子的。但是,每个操作之间的间隙时间,atomic不能保证属性不被其他线程访问。在TheadA对num进行加1操作后,此时CPU时间被分配给了Thread B,Thread B有可能对num进行了修改,当CPU时间再次分配回Thread A的时候,此时的num+1不一定是原来的num+1,此时Thread 将当前的num值修改成原来的的num+1的值,最后导致预期值跟实际值不一样,这种场景就是多线程的线程不安全。而且使用atomic无法避免一个问题,如果多线程对属性的访问是直接通过Ivar来访问, 不通过调用Getter/Setter来访问的话,atomic没有任何作用。

同样,例子2也是一样,当执行代码self.stringA.length >= 10时,假设stringA的值是“a very long string”,符合判断条件,此时线程切换到Thread A,Thread A将stringA修改成“string”。这时CPU时间再次分配给Thread B,此时Thread B会执行[self.stringA substringWithRange:NSMakeRange(0, 10)],但当前的stringA的值已经被Thread A修改成了“string”,所以会字符串访问越界,直接crash。

例子1和例子2出现问题的原因在于虽然对字符串的每次读写都是安全的,但是并不能保证各个线程组合起来的操作是安全的,这就是一个线程安全的原子性粒度问题。atomic的原子粒度是Getter/Setter,但对多行代码的操作不能保证原子性。针对例子1和例子2的问题,更好的办法是使用锁机制。

// 例子3
// thread A
[_lock lock];
for (int i = 0; i < 10000; i++) {
    self.num = self.num + 1;
    NSLog(@"Thread A: %d\d ",self.num);
}
[_lock unlock];

// thread B
[_lock lock];
for (int i = 0; i < 10000; i++) {
    self.num = self.num + 1;
    NSLog(@"Thread B: %d\d ",self.num);
}
[_lock unlock];
// 例子4
//thread A
[_lock lock];
for (int i = 0; i < 10000; i ++) {
    if (i % 2 == 0) {
        self.stringA = @"a very long string";
    }
    else {
        self.stringA = @"string";
    }
    NSLog(@"Thread A: %@\n", self.stringA);
}
[_lock unlock];

//thread B
[_lock lock];
for (int i = 0; i < 10000; i ++) {
    if (self.stringA.length >= 10) {
        NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];
    }
    NSLog(@"Thread B: %@\n", self.stringA);
}
[_lock unlock];

对代码进行加锁后,只有对加锁代码加锁了的线程才能访问加锁代码,这样就保证了加锁代码不会被其他线程执行,从而从更大粒度上保证了线程安全。如果使用了锁机制进行代码级原子粒度的控制,就没有必要再使用更小粒度的atomic了。因为大粒度的原子性已经能够保障相关业务代码的线程安全,如果再加多更小粒度的原子性控制,一来会多此一举,二来atomic是一种更小粒度的加锁机制,会对性能有不少的影响,所以一般来说如果使用了更大粒度的原子性,就没有必要使用更小粒度的原子性了,所以加锁后的代码中的属性变量,没有必要再使用atomic

不加锁的小技巧

对于例子2,如果不加锁,怎么保证不会代码不会crash?

// 例子5
for (int i = 0; i < 10000; i ++) {
    NSString *immutableTempString = self.stringA;
    if (immutableTempString.length >= 10) {
        NSString* subStr = [immutableTempString substringWithRange:NSMakeRange(0, 10)];
    }
}

例子2发生crash的原因是,stringA指向的内存区域发生了变化,访问时发生了越界。但例子5中则不会有这种情况,因为例子5中使用了临时变量immutableTempString,指向stringA未发生变化前的内存空间,当stringA指向的内存发生变化后,由于原来stringA指向的内存被immutableTempString指向,所以暂时不会被系统回收。当[immutableTempString substringWithRange:NSMakeRange(0, 10)]调用时,immutableTempString指向的还是原来的stringA的值,所以不会发生crash。这种方法的原理是,通过使用临时变量来持有原来变动前的值,所有操作都对这个临时变量指向的值进行操作,而不是直接使用属性指向的值,这样的话能保证上下文情景下变量的值是一致的,而且由于变量是临时变量,所以只会对当前线程可见,对其他线程不可见,从而在某种程度上保证了线程安全。

总结

在iOS中,不能简单的认为只要加上atomic关键字就能保证属性的线程安全。而在实际使用中,由于业务代码的复杂性,大部分情况下都会使用比atomic更大粒度的锁控制。由于使用了更大粒度的锁,从性能和必要性方面考虑,就不需要再使用atomic了。在某些情况下,如果不能采用加锁的做法,又要保证代码不会发生crash,可以使用临时变量指向原值,保证一定程度的线程安全。

总而言之,多线程的线程安全是个复杂的问题,最好的做法是尽量避免多线程的设计

Reference

iOS多线程到底不安全在哪里?

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

推荐阅读更多精彩内容

  • 下面是我自己收集整理的Java线程相关的面试题,可以用它来好好准备面试。 参考文档:-《Java核心技术 卷一》-...
    阿呆变Geek阅读 14,729评论 14 507
  • 接着上节 mutex,本节主要介绍atomic的内容,练习代码地址。本文参考http://www.cplusplu...
    jorion阅读 73,502评论 1 14
  • 一.线程安全性 线程安全是建立在对于对象状态访问操作进行管理,特别是对共享的与可变的状态的访问 解释下上面的话: ...
    黄大大吃不胖阅读 811评论 0 3
  • 1 叶瑶在家入口的玄关处拖鞋的时候,还是察觉到了她的粉红色棉拖和前两天出差前摆放的地方不一样。 她抿了抿嘴唇,把这...
    萧洛zzy阅读 2,176评论 3 6
  • 匆忙结束了一天工作的我,回到家,发现冷锅冷灶,一切都停停当当地静默在那里,猛然想起自己加班,家人都回乡下过周末兼避...
    坐忘mao阅读 386评论 1 2