5.使用原子操作
非阻塞同步的方式是用来执行某些类型的操作而避免扩展使用锁。尽管锁是同步两个线程的很好方式,获取一个锁是一个很昂贵的操作,即使在无竞争的状态下。相比,许多原子操作花费很少的时间来完成操作也可以达到和锁一样的效果。
原子操作可以让你在32位或64位的处理器上面执行简单的数学和逻辑的运算操作。这些操作依赖于特定的硬件设施(和可选的内存屏障)来保证给定的操作在影响内存再次访问的时候已经完成。在多线程情况下,你应该总是使用原子操作,它和内存屏障组合使用来保证多个线程间正确的同步内存。
表4-3列出了可用的原子运算和本地操作和相应的函数名。这些函数声明在/usr/include/libkern/OSAtomic.h头文件里面,在那里你也可以找到完整的语法。这些函数的64-位版本只能在64位的进程里面使用。
Table 4-3 Atomic math and logic operations
Operation | Function name | Description |
Add | OSAtomicAdd32 | Adds two integer values together and stores the result in one of the specified variables. |
Increment | OSAtomicIncrement32 | Increments the specified integer value by 1. |
Decrement | OSAtomicDecrement32 | Decrements the specified integer value by 1. |
Logical OR | Performs a logical OR between the specified 32-bit value and a 32-bit mask. | |
Logical AND | Performs a logical AND between the specified 32-bit value and a 32-bit mask. | |
Logical XOR | Performs a logical XOR between the specified 32-bit value and a 32-bit mask. | |
Compare and swap | OSAtomicCompareAndSwap32 | Compares a variable against the specified old value. If the two values are equal, this function assigns the specified new value to the variable; otherwise, it does nothing. The comparison and assignment are done as one atomic operation and the function returns a Boolean value indicating whether the swap actually occurred. |
Test and set | Tests a bit in the specified variable, sets that bit to 1, and returns the value of the old bit as a Boolean value. Bits are tested according to the formula (0×80 >> (n & 7)) of byte((char*)address + (n >> 3)) where n is the bit number and address is a pointer to the variable. This formula effectively breaks up the variable into 8-bit sized chunks and orders the bits in each chunk in reverse. For example, to test the lowest-order bit (bit 0) of a 32-bit integer, you would actually specify 7 for the bit number; similarly, to test the highest order bit (bit 32), you would specify 24 for the bit number. | |
Test and clear | Tests a bit in the specified variable, sets that bit to 0, and returns the value of the old bit as a Boolean value. Bits are tested according to the formula (0×80 >> (n & 7)) of byte((char*)address + (n >> 3)) where n is the bit number and address is a pointer to the variable. This formula effectively breaks up the variable into 8-bit sized chunks and orders the bits in each chunk in reverse. For example, to test the lowest-order bit (bit 0) of a 32-bit integer, you would actually specify 7 for the bit number; similarly, to test the highest order bit (bit 32), you would specify 24 for the bit number. |
大部分原子函数的行为是相对简单的并应该是你想要的。然而列表4-1显式了测试-设置和比较-交换操作的原子行为,它们相对复杂一点。OSAtomicTestAndSet 第一次调用展示了如何对一个整形值进行位运算操作,而它的结果和你预期的有差异。最后两次调用OSAtomicCompareAndSwap32显式它的行为。所有情况下,这些函数都是无竞争的下调用的,此时没有其他线程试图操作这些值。
Listing 4-1 Performing atomic operations
- int32_t theValue = 0;
- OSAtomicTestAndSet(0, &theValue);
- // theValue is now 128.
- theValue = 0;
- OSAtomicTestAndSet(7, &theValue);
- // theValue is now 1.
- theValue = 0;
- OSAtomicTestAndSet(15, &theValue)
- // theValue is now 256.
- OSAtomicCompareAndSwap32(256, 512, &theValue);
- // theValue is now 512.
- OSAtomicCompareAndSwap32(256, 1024, &theValue);
- // theValue is still 512.
关于原子操作的更多信息,参见atomic的主页和/usr/include/libkern/OSAtomic.h头文件。
6.使用锁
锁是线程编程同步工具的基础。锁可以让你很容易保护代码中一大块区域以便你可以确保代码的正确性。Mac OS X和iOS都位所有类型的应用程序提供了互斥锁,而Foundation框架定义一些特殊情况下互斥锁的额外变种。以下个部分显式了如何使用这些锁的类型。
6.1 使用POSIX互斥锁
POSIX互斥锁在很多程序里面很容易使用。为了新建一个互斥锁,你声明并初始化一个pthread_mutex_t的结构。为了锁住和解锁一个互斥锁,你可以使用pthread_mutex_lock和pthread_mutex_unlock函数。列表4-2显式了要初始化并使用一个POSIX线程的互斥锁的基础代码。当你用完一个锁之后,只要简单的调用pthread_mutex_destroy来释放该锁的数据结构。
Listing 4-2 Using a mutex lock
- pthread_mutex_t mutex;
- void MyInitFunction()
- {
- pthread_mutex_init(&mutex, NULL);
- }
- void MyLockingFunction()
- {
- pthread_mutex_lock(&mutex);
- // Do work.
- pthread_mutex_unlock(&mutex);
- }
注意:上面的代码只是简单的显式了使用一个POSIX线程互斥锁的步骤。你自己的代码应该检查这些函数返回的错误码,并适当的处理它们。
6.2 使用NSLock类
在Cocoa程序中NSLock中实现了一个简单的互斥锁。所有锁(包括NSLock)的接口实际上都是通过NSLocking协议定义的,它定义了lock和unlock方法。你使用这些方法来获取和释放该锁。
除了标准的锁行为,NSLock类还增加了tryLock和lockBeforeDate:方法。方法tryLock试图获取一个锁,但是如果锁不可用的时候,它不会阻塞线程。相反,它只是返回NO。而lockBeforeDate:方法试图获取一个锁,但是如果锁没有在规定的时间内被获得,它会让线程从阻塞状态变为非阻塞状态(或者返回NO)。
下面的例子显式了你可以是NSLock对象来协助更新一个可视化显式,它的数据结构被多个线程计算。如果线程没有立即获的锁,它只是简单的继续计算直到它可以获得锁再更新显式。
6.3 使用@synchronized指令
@synchronized指令是在Objective-C代码中创建一个互斥锁非常方便的方法。@synchronized指令做和其他互斥锁一样的工作(它防止不同的线程在同一时间获取同一个锁)。然而在这种情况下,你不需要直接创建一个互斥锁或锁对象。相反,你只需要简单的使用Objective-C对象作为锁的令牌,如下面例子所示:
- - (void)myMethod:(id)anObj
- {
- @synchronized(anObj)
- {
- // Everything between the braces is protected by the @synchronized directive.
- }
- }
创建给@synchronized指令的对象是一个用来区别保护块的唯一标示符。如果你在两个不同的线程里面执行上述方法,每次在一个线程传递了一个不同的对象给anObj参数,那么每次都将会拥有它的锁,并持续处理,中间不被其他线程阻塞。然而,如果你传递的是同一个对象,那么多个线程中的一个线程会首先获得该锁,而其他线程将会被阻塞直到第一个线程完成它的临界区。
作为一种预防措施,@synchronized块隐式的添加一个异常处理例程来保护代码。该处理例程会在异常抛出的时候自动的释放互斥锁。这意味着为了使用@synchronized指令,你必须在你的代码中启用异常处理。了如果你不想让隐式的异常处理例程带来额外的开销,你应该考虑使用锁的类。
关于更多@synchronized指令的信息,参阅The Objective-C Programming Language。
6.4 使用其他Cocoa锁
以下个部分描述了使用Cocoa其他类型的锁。
使用NSRecursiveLock对象
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对象定义了一个互斥锁,可以使用特定值来锁住和解锁。不要把该类型的锁和条件(参见“条件”部分)混淆了。它的行为和条件有点类似,但是它们的实现非常不同。
通常,当多线程需要以特定的顺序来执行任务的时候,你可以使用一个NSConditionLock对象,比如当一个线程生产数据,而另外一个线程消费数据。生产者执行时,消费者使用由你程序指定的条件来获取锁(条件本身是一个你定义的整形值)。当生产者完成时,它会解锁该锁并设置锁的条件为合适的整形值来唤醒消费者线程,之后消费线程继续处理数据。
NSConditionLock的锁住和解锁方法可以任意组合使用。比如,你可以使用unlockWithCondition:和lock消息,或使用lockWhenCondition:和unlock消息。当然,后面的组合可以解锁一个锁但是可能没有释放任何等待某特定条件值的线程。
下面的例子显示了生产者-消费者问题如何使用条件锁来处理。想象一个应用程序包含一个数据的队列。一个生产者线程把数据添加到队列,而消费者线程从队列中取出数据。生产者不需要等待特定的条件,但是它必须等待锁可用以便它可以安全的把数据添加到队列。
- id condLock = [[NSConditionLock alloc] initWithCondition:NO_DATA];
- while(true)
- {
- [condLock lock];
- /* Add data to the queue. */
- [condLock unlockWithCondition:HAS_DATA];
- }
因为初始化条件锁的值为NO_DATA,生产者线程在初始化的时候可以毫无问题的获取该锁。它会添加队列数据,并把条件设置为HAS_DATA。在随后的迭代中,生产者线程可以把到达的数据添加到队列,无论队列是否为空或依然有数据。唯一让它进入阻塞的情况是当一个消费者线程充队列取出数据的时候。
因为消费者线程必须要有数据来处理,它会使用一个特定的条件来等待队列。当生产者把数据放入队列时,消费者线程被唤醒并获取它的锁。它可以从队列中取出数据,并更新队列的状态。下列代码显示了消费者线程处理循环的基本结构。
- while (true)
- {
- [condLock lockWhenCondition:HAS_DATA];
- /* Remove data from the queue. */
- [condLock unlockWithCondition:(isEmpty ? NO_DATA : HAS_DATA)];
- // Process the data locally.
- }
使用NSDistributedLock对象
NSDistributedLock类可以被多台主机上的多个应用程序使用来限制对某些共享资源的访问,比如一个文件。锁本身是一个高效的互斥锁,它使用文件系统项目来实现,比如一个文件或目录。对于一个可用的NSDistributedLock对象,锁必须由所有使用它的程序写入。这通常意味着把它放在文件系统,该文件系统可以被所有运行在计算机上面的应用程序访问。
不像其他类型的锁,NSDistributedLock并没有实现NSLocking协议,所有它没有lock方法。一个lock方法将会阻塞线程的执行,并要求系统以预定的速度轮询锁。以其在你的代码中实现这种约束,NSDistributedLock提供了一个tryLock方法,并让你决定是否轮询。
因为它使用文件系统来实现,一个NSDistributedLock对象不会被释放除非它的拥有者显式的释放它。如果你的程序在用户一个分布锁的时候崩溃了,其他客户端简无法访问该受保护的资源。在这种情况下,你可以使用breadLock方法来打破现存的锁以便你可以获取它。但是通常应该避免打破锁,除非你确定拥有进程已经死亡并不可能再释放该锁。
和其他类型的锁一样,当你使用NSDistributedLock对象时,你可以通过调用unlock方法来释放它。