多线程(四):同步

iOS多线程之线程同步

Posted by Ted on May 27, 2016

苹果官方文档同步

应用程序中存在多个线程会导致潜在的问题。修改相同资源的两个线程可能会以无意的方式相互干扰。例如,一个线程可能会覆盖另一个线程的更改,或者将该应用程序置于未知且无效的状态。如果幸运的话,损坏的资源可能会导致明显的性能问题或崩溃,这种情况还相对容易追踪和修复。但是,如果你不幸,这种破坏可能会导致微小的错误,直到很晚才会出现,或者错误可能需要对你的基本编码进行重大改革。

谈到线程安全性,一个好的设计是最好的保护。避免共享资源并尽量减少线程之间的交互,使这些线程不太可能互相干扰。然而,完全无干扰的设计并不总是可行的。在线程必须交互的情况下,您需要使用同步工具来确保交互时安全。

一、原子操作

原子操作是一种简单的同步形式,适用于简单的数据类型。 注意,这个不是属性修饰符中的原子性

原子操作的优点是它们不会阻塞竞争的线程。 对于简单的操作,比如增加一个计数器变量,这可能会导致比锁定更好的性能。

二、内存屏障与易变变量

为了达到最佳性能,编译器经常重新安排汇编级指令,以尽可能地保持处理器的指令流水线操作。作为此优化的一部分,编译器可能会重新排序访问主内存的指令,因为它认为这样做不会生成不正确的数据。不幸的是,编译器并不总是能够检测所有依赖于内存的操作。如果看似单独的变量实际上相互影响,编译器优化可能会以错误的顺序更新这些变量,从而产生可能不正确的结果。

内存屏障

内存屏障是一种非阻塞同步工具,用于确保内存操作以正确的顺序进行。内存屏障就像栅栏一样,强制处理器完成位于栅栏前面的任何加载和存储操作,然后才允许执行位于栅栏之后的加载和存储操作。内存屏障通常用于确保一个线程(但对另一个线程可见)的内存操作始终按预期的顺序进行。在这种情况下缺乏内存屏障可能会让其他线程看到看似不可能的结果。 (例如,请参阅维基百科条目的内存屏障。)

要使用内存屏障,只需在代码中的适当位置调用OSMemoryBarrier函数即可。

易变变量

易变变量将另一种内存约束应用于单个变量。编译器通常通过将变量值加载到寄存器中来优化代码。对于局部变量,这通常不是问题。如果变量从另一个线程可见,那么这样的优化可能会阻止其他线程注意到它的任何变化。将volatile关键字应用于变量会强制编译器在每次使用内存时从内存加载该变量。你可能会声明一个变量,如果它的值可能随时被编译器可能检测不到的外部源所改变,那么这个变量是volatile的。

因为内存障碍和volatile变量都会减少编译器可以执行的优化次数,所以应该谨慎使用,只有在需要确保正确性的情况下才能使用。

三、锁

锁是最常用的同步工具之一。 您可以使用锁来保护代码的关键部分(一段代码,每次只允许一个线程访问)。

锁的种类

描述
互斥锁(Mutex) 互斥(或互斥锁)作为资源周围的保护屏障。 互斥锁是一种信号量,一次只能访问一个线程。 如果一个互斥体正在使用,而另一个线程试图获取它,则该线程阻塞,直到互斥体被其原始持有线程释放。 如果多个线程竞争同一个互斥体,则一次只允许一个线程访问它。
递归锁( Recursive lock) 递归锁是互斥锁的变体。 递归锁允许单个线程在释放之前多次获取锁。 其他线程保持阻塞状态,直到锁的所有者释放锁的次数与获取它的次数相同。 递归锁主要在递归迭代中使用,但也可能在多个方法需要单独获取锁的情况下使用。
读写锁(Read-write lock) 读写锁也被称为共享排他锁。这种类型的锁通常用于较大规模的操作-如果受保护的数据结构被频繁读取和偶尔修改,可以显着提高性能。 在正常操作期间,多个阅读器可以同时访问数据结构。 当一个线程想要写入结构时,它会阻塞,直到所有的读者释放锁,在这一点上它获得锁,并且可以更新结构。 写入线程正在等待锁定时,新的线程将被阻塞,直到写入线程完成。 系统仅支持使用POSIX线程的读写锁定。
分布式锁( Distributed lock) 分布式锁提供进程级别的互斥访问。 与真正的互斥锁不同,分布式锁不会阻塞进程或阻止进程运行。 它只是报告锁何时忙,让流程决定如何进行。
自旋锁( Spin lock) 自旋锁重复其锁定条件,直到该条件成立。 自旋锁最常用于多处理器系统,其中锁的预期等待时间很短。 在这些情况下,轮询通常比阻塞线程更高效,这涉及到上下文切换和线程数据结构的更新。 由于轮询性质,系统不提供自旋锁的任何实现,但是您可以在特定情况下轻松实现它们。
双重检查锁( Double-checked lock) 双重检查锁试图通过在锁定之前测试锁定标准来降低锁定的开销。 由于双重检查的锁可能是不安全的,系统不提供对它们的明确的支持,并且它们的使用是不鼓励的。

注意:大多数类型的锁还包含一个内存屏障,以确保在进入临界区之前完成前面的加载和存储指令

我们在iOS开发中最常接触的是互斥锁、递归锁和自旋锁。

自旋锁和互斥锁的区别

  • 相同点:都能保证同一时间只有一个线程访问共享资源。都能保证线程安全。
  • 不同点:
    • 互斥锁:如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁。一旦被访问的资源被解锁,则等待资源的线程会被唤醒。
    • 自旋锁:如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁,一旦被访问的资源被解锁,则等待资源的线程会立即执行。自旋锁的效率高于互斥锁。

1、@synchronized

        // @synchronized
        id obj = [NSObject new];
        lastTime = CFAbsoluteTimeGetCurrent();
        for (NSInteger i = 0; i < KRunCount; ++i) {
            @synchronized(obj) {
            }
        }
        current = CFAbsoluteTimeGetCurrent();
        NSLog(@"@synchronized: %f ms", (current - lastTime) * 1000);

2、NSLock、NSLock+IMP

        // NSLock
        NSLock *myLock = [NSLock new];
        lastTime = CFAbsoluteTimeGetCurrent();
        for (NSInteger i = 0; i < KRunCount; ++i) {
            [myLock lock];
            [myLock unlock];
        }
        current = CFAbsoluteTimeGetCurrent();
        NSLog(@"NSLock: %f ms", (current - lastTime) * 1000);
        
        // NSLock + IMP
        typedef void (*func)(id, SEL);
        SEL lockSEL = @selector(lock);
        SEL unlockSEL = @selector(unlock);
        func lockFunc = (void (*)(id, SEL))[myLock methodForSelector : lockSEL];
        func unlockFunc = (void (*)(id, SEL))[myLock methodForSelector : unlockSEL];
        lastTime = CFAbsoluteTimeGetCurrent();
        for (NSInteger i = 0; i < KRunCount; ++i) {
            lockFunc(myLock, lockSEL);
            unlockFunc(myLock, unlockSEL);
        }
        current = CFAbsoluteTimeGetCurrent();
        NSLog(@"NSLock + IMP: %f ms", (current - lastTime) * 1000);

3、NSCondition

条件不是锁,但是与锁一样是同步工具

- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;

4、NSConditionLock

通常,当多线程需要以特定的顺序来执行任务的时候,你可以使用一个 NSConditionLock 对象,比如当一个线程生产数据,而另外一个线程消费数据。生产者执行时,消费者使用由你程序指定的条件来获取锁(条件本身是一个你定义的整形 值)。当生产者完成时,它会解锁该锁并设置锁的条件为合适的整形值来唤醒消费者 线程,之后消费线程继续处理数据。

- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
        // NSConditionLock
        NSConditionLock *conditionLock = [[NSConditionLock alloc] init];
        lastTime = CFAbsoluteTimeGetCurrent();
        for (NSInteger i = 0; i < KRunCount; ++i) {
            [conditionLock lock];
            [conditionLock unlock];
        }
        current = CFAbsoluteTimeGetCurrent();
        NSLog(@"NSConditionLock: %f ms", (current - lastTime) * 1000);

5、NSRecursiveLock

NSRecursiveLock 类定义的锁可以在同一线程多次获得,而不会造成死锁。一个递归锁会跟踪它被多少次成功获得了。每次成功的获得该锁都必须平衡调用锁住和解 锁的操作。只有所有的锁住和解锁操作都平衡的时候,锁才真正被释放给其他线程获 得。正如它名字所言,这种类型的锁通常被用在一个递归函数里面来防止递归造成阻 塞线程。你可以类似的在非递归的情况下使用他来调用函数,这些函数的语义要求它 们使用锁。以下是一个简单递归函数,它在递归中获取锁。如果你不在该代码里使用 NSRecursiveLock 对象,当函数被再次调用的时候线程将会出现死锁。

        // NSRecursiveLock
        NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
        lastTime = CFAbsoluteTimeGetCurrent();
        for (NSInteger i = 0; i < KRunCount; ++i) {
            [recursiveLock lock];
            [recursiveLock unlock];
        }
        current = CFAbsoluteTimeGetCurrent();
        NSLog(@"NSRecursiveLock: %f ms", (current - lastTime) * 1000);

6、pthread_mutex_t

        // pthread_mutex_t
        pthread_mutex_t theLock;
        pthread_mutex_init(&theLock, NULL);
        lastTime = CFAbsoluteTimeGetCurrent();
        for (NSInteger i = 0; i < KRunCount; ++i) {
            pthread_mutex_lock(&theLock);
            pthread_mutex_unlock(&theLock);
        }
        current = CFAbsoluteTimeGetCurrent();
        NSLog(@"pthread_mutex: %f ms", (current - lastTime) * 1000);
        pthread_mutex_destroy(&theLock);

7、OSSpinLock

自旋锁比较适用于锁使用者保持锁时间比较短的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用,而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。

自旋锁效率最高,但是在等待时会消耗大量的CPU资源,不适合长时间任务

自旋锁应用:关联对象添加,weak对象的添加

        // OSSpinLock
        OSSpinLock spinlock = OS_SPINLOCK_INIT;
        lastTime = CFAbsoluteTimeGetCurrent();
        for (NSInteger i = 0; i < KRunCount; ++i) {
            OSSpinLockLock(&spinlock);
            OSSpinLockUnlock(&spinlock);
        }
        current = CFAbsoluteTimeGetCurrent();
        NSLog(@"OSSpinlock: %f ms", (current - lastTime) * 1000);

8、dispatch_barrier_async

        // dispatch_barrier_async
        dispatch_queue_t queue =
        dispatch_queue_create("com.helloted", DISPATCH_QUEUE_CONCURRENT);
        lastTime = CFAbsoluteTimeGetCurrent();
        for (NSInteger i = 0; i < 10; ++i) {
            dispatch_barrier_async(queue, ^{
                if (i==9) {
                    current = CFAbsoluteTimeGetCurrent();
                    NSLog(@"dispatch_barrier_async: %f ms", (current - lastTime) * 1000 * KRunCount / 10);
                }
            });
        }

9、dispatch_semaphore_t

        // dispatch_semaphore_t
        dispatch_queue_t my_queue = dispatch_queue_create("com.helloted2", DISPATCH_QUEUE_CONCURRENT);
        dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
        lastTime = CFAbsoluteTimeGetCurrent();
        for (int index = 0; index < 10; index++) {
            dispatch_async(my_queue, ^(){
                dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
                if (index==9) {
                    current = CFAbsoluteTimeGetCurrent();
                    NSLog(@"dispatch_barrier_async: %f ms", (current - lastTime) * 1000 * KRunCount / 10);
                }
                dispatch_semaphore_signal(semaphore);
            });
        }

耗时

2015-05-25 16:46:26.699786+0800 Tst[69225:10974999] @synchronized: 103.094995 ms

2015-05-25 16:46:26.737976+0800 Tst[69225:10974999] NSLock: 37.993014 ms

2015-05-25 16:46:26.775264+0800 Tst[69225:10974999] NSLock + IMP: 37.218988 ms

2015-05-25 16:46:26.813209+0800 Tst[69225:10974999] NSCondition: 37.881017 ms

2015-05-25 16:46:26.926069+0800 Tst[69225:10974999] NSConditionLock: 112.789989 ms

2015-05-25 16:46:26.978869+0800 Tst[69225:10974999] NSRecursiveLock: 52.753985 ms

2015-05-25 16:46:27.002183+0800 Tst[69225:10974999] pthread_mutex: 23.241997 ms

2015-05-25 16:46:27.013260+0800 Tst[69225:10974999] OSSpinlock: 11.014998 ms

2015-05-25 16:46:27.034420+0800 Tst[69225:10974999] dispatch_barrier_async: 703.334808 ms

2015-05-25 16:46:27.350780+0800 Tst[69225:10974999] dispatch_semaphore_t: 9799.003601 ms

从上面数据可以看出,OSSpinlock最为省时间,其次为NSLock,至于dispatch_barrier_async与dispatch_semaphore_t耗时比较大。

四、信号量与锁的区别

“信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作(大家都在semtake的时候,就阻塞在 哪里)。而互斥锁是用在多线程多任务互斥的,一个线程占用了某一个资源,那么别的线程就无法访问,直到这个线程unlock,其他的线程才开始可以利用这 个资源。比如对全局变量的访问,有时要加锁,操作完了,在解锁。有的时候锁和信号量会同时使用的” 也就是说,信号量不一定是锁定某一个资源,而是流程上的概念,比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务 并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。而线程互斥量则是“锁住某一资源”的概念,在锁定期间内,其他线程无法对被保护的数据进行操作。在有些情况下两者可以互换。

两者之间的区别:

作用域 信号量: 进程间或线程间(linux仅线程间的无名信号量pthread semaphore) 互斥锁: 线程间

**上锁时 ** 信号量: 只要信号量的value大于0,其他线程就可以sem_wait成功,成功后信号量的value减一。若value值不大于0,则sem_wait使得线程阻塞,直到sem_post释放后value值加一,但是sem_wait返回之前还是会将此value值减一 互斥锁: 只要被锁住,其他任何线程都不可以访问被保护的资源

与锁的性能比较

  • OSSpinLock 自旋锁,性能最高的锁。原理很简单,就是一直 do while 忙等。它的缺点是当等待时会消耗大量 CPU 资源,所以它不适用于较长时间的任务。对于内存缓存的存取来说,它非常合适。
  • dispatch_semaphore 是信号量,但当信号总量设为 1 时也可以当作锁来。在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源。对磁盘缓存来说,它比较合适。

五、条件

条件是另一种类型的信号量,它允许线程在某个条件为真时互相发信号。条件通常用于指示资源的可用性或确保任务按特定顺序执行。当一个线程测试一个条件时,线程会阻塞,除非这个条件变成True。它保持阻塞状态,直到其他线程明确地改变并发出信号。

条件和互斥锁之间的区别是:多个线程可能被允许同时访问该条件。

这种情况更多的是看门人,根据一些特定的标准,让不同的线程通过门。

你可能会使用一个条件的一种方法是管理一个等待处理的事件池。当队列中有事件时,事件队列将使用一个条件变量来通知等待的线程。如果有一个事件到达,队列将适当地发出信号。如果一个线程已经在等待,它将被唤醒,随后它将把事件从队列中拉出来处理。如果两个事件大致同时进入队列,则队列将两次发信号唤醒两个线程。

系统为几种不同技术的条件提供支持。条件是一种特殊类型的锁,您可以使用它来同步操作必须执行的顺序。 它们与互斥锁以微妙的方式不同。 等待条件的线程将保持阻塞状态,直到该条件由另一个线程显式指示。

[cocoaCondition lock];
while (timeToDoWork <= 0)
    [cocoaCondition wait];
 
timeToDoWork--;
 
// Do real work here.
 
[cocoaCondition unlock];

另一个线程来发射信号

[cocoaCondition lock];
timeToDoWork++;
[cocoaCondition signal];
[cocoaCondition unlock];

六、Perform Selector

NSObject 声明了一些方法 performing a selector用于活跃线程. 这些方法让你的线程异步传递消息,并保证它们将被目标线程同步执行。 例如,可以使用performing a selector消息将分布式计算的结果传递到应用程序的主线程或指定的协调程序线程。 performing a selector的每个请求都在目标线程的运行循环中排队,然后按接收到的顺序按顺序处理这些请求。