[TOC]
@(iOS开发学习)[温故而知新]
一:atomic是线程安全的吗
atomic
所说的线程安全只是保证了属性的getter
和setter
存取方法的线程安全,并不能保证整个对象是线程安全的。atomic
有个很大的问题是很慢,要比nonatomic
慢20倍。
1.1、结果一:不崩溃但是结果非预期
@property (atomic, assign) int intA;
//thread A
for (int i = 0; i < 10000; i ++) {
self.intA = self.intA + 1;
NSLog(@"Thread A: %d\n", self.intA);
}
//thread B
for (int i = 0; i < 10000; i ++) {
self.intA = self.intA + 1;
NSLog(@"Thread B: %d\n", self.intA);
}
即使我将
intA
声明为atomic
,最后的结果也不一定会是20000。原因就是因为self.intA = self.intA + 1
;不是原子操作,虽然intA的getter
和setter
是原子操作,但当我们使用intA的时候,整个语句并不是原子的,这行赋值的代码至少包含读取(load
),+1(add
),赋值(store
)三步操作,当前线程store
的时候可能其他线程已经执行了若干次store
了,导致最后的值小于预期值。这种场景我们也可以称之为多线程不安全。
1.2、结果二:直接崩溃
@property (atomic, strong) NSString* stringA;
//thread A
for (int i = 0; i < 100000; 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 < 100000; i ++) {
if (self.stringA.length >= 10) {
NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];
}
NSLog(@"Thread B: %@\n", self.stringA);
}
虽然
stringA
是atomic
的property
,而且在取substring
的时候做了length
判断,线程B还是很容易crash
,因为在前一刻读length的时候self.stringA = @"a very long string";
,下一刻取substring的时候线程A已经将self.stringA = @"string"
;,立即出现out of bounds
的Exception,crash,多线程不安全。
//thread A
[_lock lock];
for (int i = 0; i < 100000; 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];
if (self.stringA.length >= 10) {
NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];
}
[_lock unlock];
加锁以后,整段代码就具有原子性了,就可以认为是多线程安全了。
二:锁
2.1、pthread_mutex
pthread_mutex
表示互斥锁,互斥锁的原理与信号量非常相似,不是使用忙等,而是阻塞线程并休眠,并进行上下文切换,让出时间片。pthread_mutex
有多种类型(比如递归锁),在申请加锁时需要对锁的类型进行判断,因此即使互斥锁和信号量的实现非常类似但效率略低。
一般情况下,一个线程只能申请一次锁,也只能在获得所得锁的情况下释放锁,多次申请锁或者释放未申请的锁都会导致崩溃。假设在已经获得锁的情况下再去申请锁,线程会因为等待锁的释放而进入睡眠状态,也就不能再释放锁,从而导致死锁。
2.2、NSLock
NSLock
只是在内部封装了一个 pthread_mutex
2.3、@synchronized
@synchronized
后面需要紧跟一个OC
对象,它实际上是把这个对象当做锁来使用。这是通过一个哈希表来实现的,OC
在底层使用了一个互斥锁的数组(你可以理解为锁池),通过对对象去哈希值来得到对应的互斥锁。synchronized
中传入的object
的内存地址,被用作key
,通过hash map
对应的一个系统维护的递归锁。
@synchronized (tokenA) {
[arrA addObject:obj];
}
@synchronized (tokenB) {
[arrB addObject:obj];
}
@synchronized(nil)
不起任何作用- 不要使用
@synchronized(self)
- 因为
self
很可能会被外部对象访问,被用作key
来生成一锁。两个公共锁交替使用的场景就容易出现死锁。 - 应该是不同的数据使用不同的锁,尽量将粒度控制在最细的程度
@synchronized
还有个很容易变慢的场景,就是{}内部有其他隐蔽的函数调用
2.4、GCD
信号量dispatch_semaphore_t
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
// dispatch_semaphore_wait等待信号,当信号总量少于0的时候就会一直等待,否则就可以正常的执行,并让信号总量-1
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// dispatch_semaphore_signal是发送一个信号,自然会让信号总量加1
dispatch_semaphore_signal(semaphore);
任务组dispatch_group
dispatch_group_enter
和dispatch_group_leave
防止异步线程立即返回
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 执行耗时任务
dispatch_group_leave(group);
});
2.5、自旋锁
自旋锁的目的是为了保证临界区只有一个线程可以访问。
自旋锁伪代码的实现
bool lock = false; // 一开始没有锁上,任何线程都可以申请锁
do {
while(lock); // 如果 lock 为 true 就一直死循环,相当于申请锁
lock = true; // 挂上锁,这样别的线程就无法获得锁
Critical section // 临界区
lock = false; // 相当于释放锁,这样别的线程可以进入临界区
Reminder section // 不需要锁保护的代码
}
互斥锁的优点:
如果临界区的执行时间过长,使用自旋锁并不是一个好主意,比如文件的读写,这种忙等是毫无必要的。
自旋锁的优点:
主动让出时间片并不总是代表效率高,让出时间片会导致操作系统切换到另外一个线程,这种上下文的切换会耗时大约10微妙左右,而且至少需要切换两次,如果线程需要执行的任务耗时很短(比如几个微妙)的时候,使用忙等的效率就会比让出时间片让线程休眠高。
三:死锁
详情参考:五个案例让你明白GCD死锁
从上面的文章拿一个典型的案例分析:
NSLog(@"1"); // 任务1
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2"); // 任务2
});
NSLog(@"3"); // 任务3
