Thread 同步

同步工具

涉及线程安全时,一个好的设计是最好的保护。避免共享资源,减少线程交互,这样可以缩小线程间干扰,但是一个完全没有干扰的设计是不可能的,就需要使用同步技术来确保安全。

原子操作

原子操作是同步简单数据类型一个非常简单的形式,其优势是不阻塞线程竞争。对于简单的操作,比如递增一个计数器,原子操作比锁更有性能优势。MacOSX和iOS包含了许多32位和64位执行基本的数学运算和逻辑运算(比较-交换,测试-设值,测试-清理)的原子操作(/usr/include/libkern/OSAtomic.h)。

内存屏障和Volatile变量

编译器优化代码时会重新排序汇编指令,来使处理器流水线执行指令以达到最佳性能。其中就可能对访问内存的指令进行(其认为可能不产生错误)重新排序,但事实上编译器几乎不可能检测到所有依赖内存的操作。如果看似独立的变量却是相互影响的,那么优化就可能打乱这些变量内存访问指令的顺序,导致潜在问题。

内存屏障(Memory Barrier)是一个确保内存操作按照正确顺序工作、非阻塞的同步工具(OSMemoryBarrier系列函数)。内存屏障就像一个栅栏,处理器必须先完成障碍前所有的内存读取操作,才能执行屏障后的内存读取操作,这样内存屏障就能确保一个线程的内存操作总是按照预定顺序执行。

Volatile是适用于独立变量的另一种内存限制形式。编译器通过把变量的值加载到寄存器(寄存器的访问速度比内存访问速度快)来实现优化。对于非线程竞争的变量不会有问题,反之则会。volatile变量会强制编译器每次使用变量时都从内存而非寄存器读取。如果一个变量的值有编译器无法检测更改的可能,那么可以把它声明为volatile。

内存屏障和volatile变量降低了编译器优化的程度,因此只在的确需要并确保正确的前提下谨慎使用。

锁是保护**临界区(Critical Section)**关键代码在同一个时间只被一个线程访问的最常用同步工具。MacOSX和iOS提供了常使用的锁:

Mutex(互斥锁) 互斥锁扮演着资源保护屏障的角色。如果一个线程尝试获取一个已经被获取的互斥锁,那么该线程会阻塞一直到拥有互斥锁的线程释放互斥锁。如果有多个线程竞争获取互斥锁,每次只有一个能成功。

Recursive lock(递归锁) 递归锁是互斥锁的变种。一个递归锁允许同一个线程释放前多次获取,其他线程保持阻塞直到拥有递归锁的线程释放同样次数后。递归锁用在递归语句中,也可用在同一线程的多个需要单独获取递归锁的方法里。

Read-write lock(读写锁) 读写锁也被称为共享互斥锁。通常用在大规模操作(经常读偶尔写)保护数据中显着提高性能。一般多个线程可以同时读数据,当有另一个线程要写数据时,写线程阻塞直到所有读线程释放后再获取读写锁,进行写操作。当一个写线程在等待锁时,新来的读线程会阻塞直到写线程结束。系统只对POSIX线程支持读写锁。

Distributed lock(分布锁) 一个分布锁提供进程间的互斥访问。和互斥锁不一样的是,分布锁不会阻塞进程或阻止进程运行。它只是简单地在尝试获取已被获取的分布锁时通知进程并让其自己决定如何处理。

Spin lock(自旋锁) 一个自旋锁会反复查询其锁定条件直到条件成为真。自旋锁通常用在获取锁预期时间很小的多核系统里。这种情况下,切换上下文和更新线程数据通常比阻塞线程更有效率。系统因为自旋锁特性而没有提供支持,但是在一些特别的情况下很容易实现它。

Double-checked lock(双重检查锁) 双重检查锁是一种,通过获锁前测试其标准来减少获取过程开销的尝试。因为双重检查锁是潜在不安全的,系统对该锁没有显式的实现并且不鼓励使用。

条件

条件是另一种当判断为真时允许线程间互相发送信号的同步工具。条件通常用来确认资源可以性或者确保任务以特定的顺序执行。当一个线程测试条件时,它阻塞直到其他线程显式地改变该条件为真。条件和互斥锁虽然都只允许多个线程中的一个通过,但互斥锁是随机选择,条件可以根据指定判断选择。系统用不同的技术对条件提供支持,条件的正确使用需要仔细思考。

同步的开销和性能

同步确保正确性,但同时会牺牲性能(甚至在无竞争时)。锁和原子操作通常使用内存屏障和内核级同步机制来确保关键代码被正确保护。同步虽然使关键代码安全,但过多同步的多线程程序和单线程程序相比反而可能会降低性能。安全和性能之间寻找平衡需要大量经验累积。在无竞争时互斥锁和原子操作的近似开销:

完全避免同步

设计代码结构和数据结构来避免使用同步是很好的解决办法。如果整体设计导致特定资源的高竞争,可能设计本身就存在问题。实现并发最好的方法是减少并发任务之间的交互和依赖。如果每个任务在它自己的数据集上面操作,则不需要使用同步来保护这些数据。

注意正确地同步

// Code 1
NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject;
[arrayLock lock];
anObject = [myArray objectAtIndex:0];
[arrayLock unlock];
[anObject doSomething];

// Code 2
NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject;
[arrayLock lock];
anObject = [myArray objectAtIndex:0];
[anObject doSomething];
[arrayLock unlock];

// Code 3
NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject;
[arrayLock lock];
anObject = [myArray objectAtIndex:0];
[anObject retain];
[arrayLock unlock];
[anObject doSomething];

Code1中anObject在执行doSomething时有野指针的可能。Code2中确保anObject有效,但是doSomething可能耗时很长,引起其他线程长时间等锁释放。Code3才是正确的写法。尽管这个示例很简单,但说明了非常重要的一点,当同步涉及到正确性时,不仅仅需要考虑问题的表面,内存管理和其他因素都有可能因为多线程而受到影响。此外,应该假设编译器总是出现最坏的情况。这种意识和警惕性,可以帮避免潜在问题,确保关键代码正确运行。

正确使用Volatile

使用一个互斥锁来保护一段关键代码后,并不还需要用volatile来保护关键代码段里的重要变量。一个互斥锁包含了内存屏障来确保内存读写操作能正确顺序执行。一个临界区里volatile变量会强制每次读写都在内存。这种同步组合在一些特定区域是必要的,但是同样会导致显著的性能损失。如果互斥锁已经可以保护变量,那么不使用volatile同样很重要。通常情况下,互斥锁和其他同步工具比volatile更好。

使用原子操作

原子操作以非阻塞方式执行某些简单类型的同步操作且避免锁的开销。尽管锁是同步线程很好的工具,但即使在无竞争状态下,获取一个锁都相对昂贵。相比而言,许多原子操作花费更少时间也能达到和锁一样的效果。原子操作(/usr/include/libkern/OSAtomic.h)在32位或64位处理器上执行简单的数学运算和逻辑运算,保证执行的操作在被影响内存再次读写前已经完成。可以使用原子操作和内存屏障组合使用,来保证多线程间内存正确同步。大部分原子操作行为都如其简单明了的函数名所理解那样。

使用POSIX互斥锁

pthread_mutex_t mutex;
void MyInitFunction() {
    pthread_mutex_init(&mutex, NULL);
}
void MyLockingFunction() {
    pthread_mutex_lock(&mutex);
    // Do work
    pthread_mutex_unlock(&mutex);
}
void MyReleaseFunction() {
    pthread_mutex_destroy(metex);
}

以上代码只是简单地展示了POSIX线程互斥锁的过程,应该还要检查函数错误码并做适当处理。

使用NSLock

Cocoa所有锁(包括NSLock)都是遵循NSLocking协议(定义了lock和unlock)来获取和释放。NSLock类除此之外还有tryLock(尝试获取一个锁并马上返回结果)和lockBeforeDate:(尝试获取一个锁,在规定时间内阻塞,最后返回结果)。

NSLock *theLock = [[NSLock alloc] init];
while (YES) {
    if ([theLock tryLock]) {
        // Do work
        [theLock unlock];
        break;
    }
}

使用@synchronized

在Objc代码中,@synchronized是一个非常方便创建互斥锁的方法。@synchronized做和其他互斥锁一样的工作,只是不需要直接创建锁对象。只需要简单使用任意Objc对象作为区别临界区的唯一标志符。

@synchronized(token) {
    // Do work
}

作为预防措施,@synchronized会隐式地添加一个异常处理代码起保护作用。该代码会在异常抛出时自动释放互斥锁。这意味着使用@synchronized就必须启用异常处理。如果不想要隐式异常处理代码带来的额外开销,应该考虑使用锁。

使用NSRecursiveLock

NSRecursiveLock在同一线程多次获取不造成死锁。一个递归锁会自己统计成功获取次数。只有获取和释放操作次数平衡时,才可以真正被释放给其他线程获取。这类锁通常用在递归(也可以是类似的非递归)函数里防止递归造成线程阻塞。因为递归锁直到获取和释放操作平衡后才会被释放,而长时间持有一个锁将会导致其他线程阻塞直到递归完成,所以必须仔细权衡使用递归锁对性能的潜在影响。如果重构代码可以消除递归或者消除使用递归锁,那可能会有更好的性能。

NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init];
void MyRecursiveFunction(int value) {
    [theLock lock];
    if (value != 0) {
        --value;
        MyRecursiveFunction(value);
    }
    [theLock unlock];
}
MyRecursiveFunction(5);

使用NSConditionLock

NSConditionLock是一个使用特定值来获取和释放的互斥锁,其行为和NSCondition有点类似,但实现非常不同。通常多线程用NSConditionLock实现以特定顺序执行任务,比如生产者消费者模式。NSConditionLock的获取和释放方法可以任意组合:lock和unlockWithCondition:,lockWhenCondition:和unlock 。

id condLock = [[NSConditionLock alloc] initWithCondition:NO_DATA];

// Thread A
while(true) {
    [condLock lock];
    // Add data to the queue
    [condLock unlockWithCondition:HAS_DATA];
}

// Thread B
while (true) {
    [condLock lockWhenCondition:HAS_DATA];
    // Remove data from the queue
    [condLock unlockWithCondition:(isEmpty ? NO_DATA : HAS_DATA)];
    // Process the data locally
}

使用NSCondition

NSCondition类不仅提供了POSIX条件相同的语义,但又封装互斥锁和条件数据。可以像互斥锁一样获取和释放它,又可以像条件一样等待它。可以作为NSLock解决多线程之间的同步问题,但设计出NSCondition更重要的目的是解决线程之间的调度问题(也必须获取锁和释放锁)。某线程调用wait使其阻塞处于等待状态(其他线程就可以继续进入临界区。这和标准锁不同,标准锁被获取后无论什么情况,只要拥有线程没有释放,其他线程就无法进入临界区),直到其他线程调用signal(唤醒多个等待线程中任意一个)或者broadcast(唤醒所有等待线程)才能继续运行。

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

// Thread B
[cocoaCondition lock];
timeToDoWork++;
[cocoaCondition signal];
[cocoaCondition unlock];

Cocoa多线程指南

Immutable对象一般都是线程安全的,Mutable对象一般不是线程安全的,在多线程中需要同步。对于线程不安全的对象,只要保证同一时间只有一个线程使用,那么整个程序总体而言还是线程安全的。如果使用多线程绘画视图,绘画代码应放在NSView的lockFocusIfCanDraw和unlockFocus之间。


延伸阅读


Thread 同步
https://hllovesgithub.github.io/2015/10/04/2015-10-04-Thread-同步/
作者
Hu Liang
发布于
2015年10月4日
许可协议