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

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

IOS小彩虹2021-07-15 12:00:0680A+A-

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

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

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

例子

具体到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多线程到底不安全在哪里?

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1

联系我们