atomic、锁、多线程

738 阅读5分钟

[TOC]

@(iOS开发学习)[温故而知新]

一:atomic是线程安全的吗

atomic所说的线程安全只是保证了属性的gettersetter存取方法的线程安全,并不能保证整个对象是线程安全的。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的gettersetter是原子操作,但当我们使用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);
}

虽然stringAatomicproperty,而且在取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_enterdispatch_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

参考博客

深入理解 iOS 开发中的锁

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

正确使用多线程同步锁@synchronized()

五个案例让你明白GCD死锁