iOS:多线程编程指南(三)--线程同步 Synchronization

--参考:(官网) https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Multithreading/Introduction/Introduction.html + (中文翻译) http://www.cocoachina.com/bbs/read.php?tid=87592

============Synchronization============

         应用程序里面多个线程的存在引发了潜在安全问题当access to resources from multiple threads of execution。两个线程同时修改同一资源有可能以意想不到的方式互相干扰。比如,一个线程可能覆盖其他线程改动的地方,或让应用程序进入一个未知的潜在无效状态。如果你幸运的话,受损的资源可能会导致明显的性能问题或崩溃,这样比较容易跟踪并修复它。然而如果你不走运,资源受损可能导致微妙的错误,这些错误不会立即显现出来,而是很久之后才出现,或者the errors might require a significant overhaul of your underlying coding assumptions。
        但涉及到线程安全时,一个好的设计是最好的保护。避免共享资源,并尽量减少线程间的interactions,这样可以让它们减少互相的干扰。但是一个完全无干扰的设计是不可能的。在线程必须交互的情况下,你需要使用同步工具(synchronization tools),来确保当它们交互的时候是安全的。
        Mac OS X 和 iOS提供了你可以使用的多个同步工具,从提供互斥访问你程序的有序的事件的工具等。以下个部分介绍了这些工具和如何在代码中使用他们来安全的访问程序的资源。

------------Synchronization Tools同步工具------------

        为了防止不同线程意外修改数据,你可以设计你的程序没有同步问题,或你也可以使用同步工具。尽管完全避免出现同步问题相对更好一点,但是几乎总是无法实现。 以下个部分介绍了你可以使用的同步工具的基本类别。

------Atomic Operations(原子操作)------

        原子操作是同步的一个简单的形式,它处理简单的数据类型。原子操作的优势是它们不block competing threads。对于简单的操作,比如递增一个计数器,原子操作比使用锁具有更高的性能优势。
        Mac OS X 和 iOS 包含了许多在 32 位和 64 位执行基本的数学和逻辑运算的操作。Among these operations are atomic versions of the compare-and-swap, test-and-set, and test-and-clear operations。查看支持原子操作的列表,参阅/user/include/libkern/OSAtomic.h头文件和参考atomic 主页。

------Memory Barriers and Volatile Variables(内存屏障和Volatile 变量)------

        内存屏障(memory barrier)是一个使用来确保内存操作按照正确的顺序工作的 非阻塞的同步工具。内存屏障的作用就像一个栅栏,迫使处理器来完成位于障碍前面 的任何加载和存储操作,才允许它执行位于屏障之后的加载和存储操作。内存屏障同样使用来确保一个线程(但对另外一个线程可见)的内存操作总是按照预定的顺序完成。如果在这些地方缺少内存屏障有可能让其他线程看到看似不可能的结果(详见Memory Barriers的Wikipedia)。为了使用一个内存屏障,你只要在你代码里面需要的地 方简单的调用OSMemoryBarrier 函数。
       Volatile 变量适用于另外一种memory constraint 对于individual variables。编译器优化代码通过加 载这些变量的值进入寄存器。对于本地变量,这通常不会有什么问题。但是如果一个变量对另外一个线程可见,那么这种优化可能会阻止其他线程发现变量的任何变化。在变量之前加上关键字 volatile 可以强制编译器每次使用变量的时候都从内存里面 加载。You might declare avariable as volatile if its value could be changed at any time by an external source that the compiler may not be able to detect
        因为内存屏障和 volatile 变量降低了编译器可执行的优化,因此你应该谨慎使用它们,只在有需要的地方时候,以确保正确性。更多使用内存屏障的信息,参阅OSMemoryBarrier 主页

------Locks------

         锁是最常用的同步工具。你可以是使用锁来保护临界区(critical section of your code),这些代码段在同一个时间只能允许被一个线程访问。比如,一个临界区可能会操作一个特定的数据结构,或使用了每次只能一个客户端client访问的资源。通过在临界区加锁,你可以排除其它线程对这段代码的影响。
        下表列出了程序最常使用的锁。Mac OS X 和 iOS 提供了这些锁里面大部分类型的实现,但是并不是全部实现。对于不支持的锁类型,下面列表说明了为什么这些锁不能直接在平台上面实现的原因。

Mutex[互斥锁] A mutually exclusive (or mutex) lock acts as a protective barrier around a resource. A mutex is a type ofsemaphorethat grants access to only one thread at a time.If a mutex is in use and another thread tries to acquire it, that thread blocks until the mutex is released by its original holder. If multiple threads compete for the same mutex, only one at a time is allowed access to it.
Recursive lock[递归锁] A recursive lock is a variant on the mutex lock. A recursive lock allows a single thread to acquire the lock multiple times before releasing it. Other threads remain blocked until the owner of the lock releases the lock the same number of times it acquired it. Recursive locks are used during recursive iterations primarily but may also be used in cases where multiple methods each need to acquire the lock separately.
Read-write lock[读写锁] A read-write lock is also referred to as a shared-exclusive lock. This type of lock is typically used in larger-scale operations and can significantly improve performance if the protected data structure is read frequently and modified only occasionally. During normal operation, multiple readers can access the data structure simultaneously. When a thread wants to write to the structure, though, it blocks until all readers release the lock, at which point it acquires the lock and can update the structure. While a writing thread is waiting for the lock, new reader threads block until the writing thread is finished.The system supports read-write locks using POSIX threads only. For more information on how to use these locks, see thepthread主页
Distributed lock [分布锁] A distributed lock provides mutually exclusive access at the process level. Unlike a true mutex, a distributed lock does not block a process or prevent it from running. It simply reports when the lock is busy and lets the process decide how to proceed.
Spin lock [自旋锁] A spin lock polls its lock condition repeatedly until that condition becomes true. Spin locks are most often used on multiprocessor systems where the expected wait time for a lock is small. In these situations, it is often more efficient to poll than to block the thread, which involves a context switch and the updating of thread data structures. The system does not provide any implementations of spin locks because of their polling nature, but you can easily implement them in specific situations. For information on implementing spin locks in the kernel, see Kernel Programming Guide.
Double-checked lock [双重检查锁] A double-checked lock is an attempt to reduce the overhead of taking a lock by testing the locking criteria prior to taking the lock. Because double-checked locks are potentially unsafe, the system does not provide explicit support for them and their use is discouraged.
           注意:大部分锁类型都合并了内存屏障来确保在进入临界区之前它前面的加载和存储指令都已经完成。关于如何使用所,参考“Use Locks”小节

------Conditions------

          条件是信号量(semaphore)的另外一个形式,它允许在条件为真的时候线程间互相发送信号。条件通常被使用来说明资源可用性,或用来确保任务以特定的顺序执行。When a thread tests a condition, it blocks unless that condition is already true. It remains blocked until some other thread explicitly changes and signals the condition。条件和互斥锁(mutex lock)的区别在于multiple threads may be permitted access to the condition at the same time。条件像个守门人允许不同线程通过根据一些指定的标准(specified criteria)。
          一个你使用条件的方式是manage a pool of pending events。事件队列可能使用condition variable来给等待线程发送信号,此时它们在事件队列中的时候。如果一个事件到达时,队列将给条件发送合适信号。如果一个线程已经处于等待,它会被唤醒,届时它将会取出事件并处理它。如果两个事件到达队列的时间大致相同,队列将会发送两次信号唤醒两个线程。
         系统通过几个不同的技术来支持条件。然而正确实现条件需要仔细编写代码。详见”Using Conditions”小节例子

------Perform Selector Routines执行Selector例程------

          Cocoa程序包含了一个简单方法of delivering messages in a synchronized manner to a single thread。The NSObject class declares methods for performing a selector on one of the application’s active threads。这些方法允许你的线程以异步的方式来传递消息,以确保它们在同一个线程上面执行是同步的。比如,you might use perform selector messages to deliver results from a distributed computation to your application’s main thread or to a designated coordinator thread.Each request to perform a selector is queued on the target thread’s run loop and the requests are then processed sequentially in the order in which they were received.。
         关于执行selector的例子和更多关于如何使用它们的信息,参考“Cocoa Perform Selector Sources”小节。

------------Synchronization Costs and Performance成本和性能------------

         同步帮助确保你代码的正确性,但同时将会牺牲部分性能,甚至在无竞争(uncontested)的情况下。同步工具的使用将在后面介绍。锁和原子操作通常包含了内存屏障(memory barriers)和内核级别(kernel-level)同步的使用来确保代码正确被保护。如果,发生锁的争夺,你的线程有可能进入阻塞, 在体验上会产生更大的迟延。
         下表列出了在无竞争(uncontested)情况下使用互斥锁和原子操作的近似的相关成本。这些测试的平均值是使用了上千的样本分析出的结果。随着线程创建时间的推移,互斥的获得(acquisition)时间(即使在无竞争情况下)可能相差也很大,这依赖于进程的加载,计算机的处理 速度和系统和程序现有可用的内存。

         图表略(主要是Mutex acquisition time 和 Atomic compare-and-swap大约时间比较)。

         当设计你的并发(concurrent)任务时,正确性是最重要的因素,但是也要考虑性能因素。代码在多个线程下面正确执行,但比相同代码在一个单独线程中执行慢,这是难以改善的。如果你是改造已有的单线程应用,你应该给关键任务做基本性能测试。当增加额外线程后,对相同的任务你应该采取新的测量方法并比较多线程和单线程情况下的性能状况。在改变代码之后,线程并没有提高性能,你应该需要重新考虑具体的实现或使用多线程问题。
         关于性能的信息和收集指标的工具,参阅 Performance Overview。关于锁和原子操作cost信息,参考”线程成本”小节。

-------------Thread Safety and Signals-------------

          当涉及到多线程应用程序时,没有什么比处理信号量更令人恐惧和困惑的了。信号量是底层BSD机制,它可以用来传递信息给进程或manipulate it in some way。一些应用程序使用信号量来检测特定事件,比如子进程的消亡。系统使用信号量来终止失控进程和communicate other types of information。
          使用信号量的问题并不是你要做什么,而是当你程序是多线程的时候它们的行为behavior。在单线程应用程序里面,所有的信号量处理都在主线程进行。在多线程应用程序里面,信号量被delivered to whichever thread happens to be running at the time,而不依赖于特定的硬件错误(比如非法指令)。如果多个线程同时运行,信号量被传递到任何一个系统挑选的线程。换而言之,信号量可以传递给你应用的任何线程。
          在你应用程序里面实现信号量处理signal handlers的第一条规则是:avoid assumptions about which thread is handling the signal。如果一个指定的线程想要处理给定的信号,你需要通过某些方法来通知该线程信号何时到达。You cannot just assume that installation of a signal handler from that thread will result in the signal being delivered to the same thread。
           关于更多信号量的信息和信号量处理例程的安装信息,参见signalsigaction主页

-------------Tips for Thread-Safe Designs-------------

          同步工具是让你代码安全的有用方法,但是它们并非灵丹妙药。使用太多锁和其他synchronization primitives跟非多线程相比明显会降低你应用的线程性能。在性能和安全之间寻找平衡是一门需要经验的艺术。以下各部分提供帮助你为你应用选择合适的同步级别的技巧。

------Avoid Synchronization Altogether------

           对于你新的项目,甚至已有项目,设计你的代码和数据结构来避免使用同步是一个很好的解决办法。虽然锁和其他类型同步工具很有用,但是它们会影响任何应用的性能。而且如果整体设计导致特定资源的高竞争,你的线程可能需要等待更长时间。
           实现并发concurrency最好的方法是减少你并发任务之间的交互和相互依赖。如果每个任务在它自己的数据集上面操作,那它不需要使用锁来保护这些数据。甚至如果两个任务共享一个普通数据集,你可以查看partitioning that set or providing each task with its own copy。当然,拷贝数据集本身也需要成本,所以在你做出决定前,你需要权衡这些成本和使用同步工具造成的成本那个更可以接受

-------Understand the Limits of Synchronization-------

          同步工具只有当它们被用在应用程序中的所有线程是一致时才是有效的。如果你创建了互斥锁来限制特定资源的访问,你所有线程都必须在试图操纵资源前获得同一互斥锁。如果不这样做导致破坏一个互斥锁提供的保护,这是编程的错误。

------注意Threats to Code Correctness------

         当你使用锁和内存屏障locks and memory barriers时,你应该总是小心的把它们放在你代码正确的地方。即使锁看起来在代码中很好放置,但通常会让你产生一个虚假的安全感。以下一系列例子试图通过指出看似无害的代码的漏洞来举例说明该问题。其基本前提是你有一个可变的数组,它包含一组不可变的对象集。假设你想要invoke a method of 数组中第一个对象。你可能会做类似下面那样的代码:

NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject;
 
[arrayLock lock];
anObject = [myArray objectAtIndex:0];
[arrayLock unlock];
 
[anObject doSomething];
          因为数组是可变的,所有数组周围的锁防止其他线程修改该数组直到你获得了想要的对象。而且因为对象限制它们本身是不可更改的,所以在调用对象的doSomething方法周围不需要锁。
         但是上面显式的例子有一个问题。如果当你释放该锁,而在你有机会执行doSomething方法前其他线程到来并从数组中删除所有对象,那会发生什么呢?对于没有使用垃圾回收的应用程序,你代码中那个对象可能已经释放了,让anObject对象指向一个非法的内存地址。为了修正该问题,你可能决定简单的重新安排你的代码,让它在调用doSomething之后才释放锁,如下所示:
NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject;
 
[arrayLock lock];
anObject = [myArray objectAtIndex:0];
[anObject doSomething];
[arrayLock unlock];
          通过把doSomething的调用移到锁的内部,你的代码可以保证该方法被调用的时候该对象还是有效的。不幸的是,如果doSomething方法需要耗费很长的时间,这有可能导致你的代码保持拥有该锁很长时间,这会产生一个性能瓶颈。

          该代码的问题is not that the critical region was poorly defined, but that the actual problem was not understood. The real problem is a memory management issue that is triggered only by the presence of other threads。因为它可以被其他线程释放,最好的解决办法是在释放锁之前retain anObject。该解决方案涉及对象被释放,并没有引发一个潜在的性能损失。

NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject;
 
[arrayLock lock];
anObject = [myArray objectAtIndex:0];
[anObject retain];
[arrayLock unlock];
 
[anObject doSomething];
[anObject release];
           尽管前面的例子非常简单,它们说明了非常重要的一点。当它涉及到代码正确性时,你需要考虑不仅仅是问题的表面。内存管理和其他影响你设计的因子都有可能因为出现多个线程而受到影响,所以你必须考虑从上到下考虑这些问题。此外,你应该在涉及安全的时候假设编译器总是出现最坏的情况。这种意识和警惕性,可以帮你避免潜在的问题,并确保你的代码运行正确。
          关于更多介绍如何让你应用程序安全的额外例子,参阅” Thread Safety Summary”小节。

------当心Deadlocks and Livelocks-------

          任何时候线程试图同时获得多于一个锁,都有可能引发潜在的死锁。当两个不同的线程分别保持一个锁(而该锁是另外一个线程需要的)又试图获得另外线程保持的锁时就会发生死锁。结果是每个线程都会进入持久性阻塞状态,因为它永远不可能获得另外那个锁。
          一个活锁和死锁类似,当两个线程竞争同一个资源的时候就可能发生活锁。在发生活锁的情况里,一个线程放弃它的第一个锁并试图获得第二个锁。一旦它获得第二个锁,它返回并试图再次获得第一个锁时。It locks up because it spends all its time releasing one lock and trying to acquire the other lock rather than doing any real work.。
          避免死锁和活锁的最好方法是同一个时间只拥有一个锁。如果你必须在同一时间获取多于一个锁,你应该确保其他线程没有做类似的事情。

------正确使用Volatile变量------ 

         如果你已经使用了一个互斥锁mutex来保护一个代码段,不要自动假设你需要使用volatile keyword来保护该代码段的重要的变量。一个互斥锁包含了内存屏障memory barrier来确保加载和存储操作是按照正确顺序的。在一个临界区添加关键字volatile到变量上面会强制每次访问该变量的时候都要从内存里面从加载。这两种同步技巧的组合使用在一些特定区域是必须的,但是同样会导致显著的性能损失。如果单独使用互斥锁已经可以保护变量,那么忽略关键字volatile。
         为了避免使用互斥锁而不使用volatile变量同样很重要。通常情况下,互斥锁和其他同步机制是比volatile变量更好的方式来保护数据结构的完整性。关键字volatile只是确保从内存加载变量而不是使用寄存器register里面的变量。它不保证你variable is accessed是正确的。

--------------Using Atomic Operations--------------

           Nonblocking synchronization方式是用来执行某些类型的操作而避免扩展使用锁。锁是synchronize two threads的很好方式,但获取一个锁是一个很昂贵的操作,即使在无竞争的状态下。相比,许多原子操作花费很少的时间来完成操作也可以达到和锁一样的效果。
           原子操作可以让你在32位或64位的处理器上面执行简单的数学和逻辑的运算操作。这些操作依赖于特定的硬件设施(和可选的内存屏障)来保证给定的操作在影响内存再次访问的时候已经完成。在多线程情况下,你应该总是使用原子操作,它和内存屏障组合使用来保证多个线程间正确的同步内存。
           下表列出了可用的原子运算和本地操作和相应的函数名。这些函数声明在/usr/include/libkern/OSAtomic.h头文件里面,在那里你也可以找到完整的语法syntax。这些函数的64-位版本只能在64位的进程里面使用:

Operation Function name Description
Logical OR(AND、XOR类似) OSAtomicOr32  OSAtomicOr32Barrier Performs a logical OR between the specified 32-bit value and a 32-bit mask.
Compare  and swap OSAtomicCompareAndSwap32
OSAtomicCompareAnd-
Swap32Barrier
OSAtomicCompareAndSwap64
OSAtomicCompareAnd-
Swap64Barrier
OSAtomicCompareAndSwapPtr
OSAtomicCompareAnd-
SwapPtrBarrier
OSAtomicCompareAndSwapInt等
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 OSAtomicTestAndSet
OSAtomicTestAndSetBarrier
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 (0x80>>(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 OSAtomicTestAndClear
OSAtomicTestAnd-
ClearBarrier
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 (0x80>>(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-set and compare-and-swap操作的原子行为,它们相对复杂一点。OSAtomicTestAndSet 第一次调用展示了如何对一个整形值进行位运算操作,而它的结果和你预期的有差异。最后两次调用OSAtomicCompareAndSwap32显式它的行为。所有情况下,这些函数都是无竞争的下调用的,此时没有其他线程试图操作这些值

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头文件。

------------Using Locks------------

            锁是线程编程同步工具的基础。锁可以让你很容易保护代码中一大块区域以便你可以确保代码的正确性。Mac OS X和iOS都位所有类型的应用程序提供了basic mutex locks(互斥锁),而Foundation框架定义一些特殊情况下互斥锁的additional variants。以下个部分显式了如何使用这些锁的类型。

------Using a POSIX Mutex Lock------

           POSIX互斥锁在很多程序里面很容易使用。为了新建一个互斥锁,你声明并初始化一个pthread_mutex_t的结构。为了锁住和解锁一个互斥锁,你可以使用pthread_mutex_lock和pthread_mutex_unlock函数。下面代码显式了要初始化并使用一个POSIX线程的互斥锁的基础代码。当你用完一个锁之后,只要简单的调用pthread_mutex_destroy来释放该锁的数据结构。

pthread_mutex_t mutex;
void MyInitFunction()
{
    pthread_mutex_init(&mutex, NULL);
}
 
void MyLockingFunction()
{
    pthread_mutex_lock(&mutex);
    // Do work.
    pthread_mutex_unlock(&mutex);
}
          注意:上面的代码只是简单的显式了使用一个 POSIX 线程互斥锁的步骤。你自己的代码应该检查 这些函数返回的错误码,并适当的处理它们。

-------使用NSLock类-------

         在 Cocoa 程序中 NSLock 中实现了一个简单的互斥锁。所有锁(包括 NSLock)的 接口实际上都是通过 NSLocking 协议定义的,它定义了 lock 和 unlock 方法。你使用 这些方法来获取和释放该锁。
         除了标准的锁行为,NSLock 类还增加了 tryLock 和 lockBeforeDate:方法。方法 tryLock 试图获取一个锁,但是如果锁不可用的时候,它不会阻塞线程。相反,它只 是返回 NO。而 lockBeforeDate:方法试图获取一个锁,但是如果锁没有在规定的时间 内被获得,它会让线程从阻塞状态变为非阻塞状态(或者返回 NO)。
        下面的例子显式了你可以是 NSLock 对象来协助更新一个可视化显式,它的数据 结构被多个线程计算。如果线程没有立即获的锁,它只是简单的继续计算直到它可以 获得锁再更新显式。

BOOL moreToDo = YES;
NSLock *theLock = [[NSLock alloc] init];
...
while (moreToDo) {
    /* Do another increment of calculation */
    /* until there’s no more to do. */
    if ([theLock tryLock]) {
        /* Update display used by all threads. */
        [theLock unlock];
    }
}

------使用@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 参数,那么每次都将会拥有它的锁,并持续处理,中间不被其他线程阻塞。然而,如果你传递的是同一个对象,那么多个线程中的一个线程会首先获得该锁,而其他线程将会被阻塞直到第一个线程完成它的临界区critical section。 

          作为一种预防措施,@synchronized 块隐式的添加一个exception handle来保护代码。该handler会在异常抛出的时候自动的释放互斥锁。这意味着为了使用@synchronized 指令,你必须在你的代码中启用异常处理。了如果你不想让隐式的异常处理例程带来额外的开销,你应该考虑使用the lock classes。
          关于更多@synchronized 指令的信息,参阅The Objective-C Programming Language

------使用其他Cocoa锁------

        以下个部分描述了使用 Cocoa 其他类型的锁。

------使用 NSRecursiveLock对象

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

NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init];
 
void MyRecursiveFunction(int value)
{
    [theLock lock];
    if (value != 0)
    {
        --value;
        MyRecursiveFunction(value);
    }
    [theLock unlock];
}
 
MyRecursiveFunction(5);
        注意:因为一个递归锁不会被释放直到所有锁的调用对应的解锁操作,所以你必须仔细权衡 是否决定使用锁对性能的潜在影响。长时间持有一个锁将会导致其他线程阻塞直到递归完成。如 果你可以重写你的代码来消除递归或消除使用一个递归锁,你可能会获得更好的性能。

------使用 NSConditionLock 对象

        NSConditionLock 对象定义了一个互斥锁,可以使用特定值来锁住和解锁。不要把该类型的锁和条件(参见“Conditions”小节)混淆了。它的行为和条件有点类似,但是 它们的实现非常不同。
        通常,当多线程需要以特定的顺序来执行任务的时候,你可以使用一个 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。在随后的迭代中,生产 者线程可以把到达的数据添加到队列,无论队列是否为空或依然有数据。 唯一让它进入阻塞的情况是当一个消费者线程extracting data from the queue
        因为消费者线程必须要有数据来处理,it waits on the queue using a specific condition.。当生产者把数据放入队列时,消费者线程被唤醒并获取它的锁。它可以从队列中取出数据,并更新队列的状态。下列代码显示了消费者线程处理循环的基本结构。
while (true)
{
    [condLock lockWhenCondition:HAS_DATA];
    /* Remove data from the queue. */
    [condLock unlockWithCondition:(isEmpty ? NO_DATA : HAS_DATA)];
 
    // Process the data locally.
}
------使用 NSDistributedLock 对象

        NSDistributedLock 类可以被多台主机上的多个应用程序使用来限制对某些共享 资源的访问,比如一个文件。该锁是一个高效的互斥锁,is implemented using a file-system item, such as a file or directory。对于一个可用的NSDistributedLock 对象,锁必须由所有使用它的程序写入。这通常意味着把它放在文件系统,该文件系统可以被所有运行在计算机上面的应用程序访问。
        不像其他类型的锁,NSDistributedLock 并没有实现 NSLocking 协议,所有它没 有 lock 方法。一个 lock 方法将会阻塞线程的执行,并要求系统以预定的速度轮询锁。 以其在你的代码中实现这种约束,NSDistributedLock 提供了一个 tryLock 方法,并 让你决定是否轮询poll。
        因为它使用文件系统来实现,一个 NSDistributedLock 对象不会被释放除非它的拥有者显式的释放它。如果你的程序在holding a distributed lock时候崩溃了,其他客户端简无法访问该受保护的资源。在这种情况下,你可以使用 breadLock 方法来打破现存的锁以便你可以获取它。但是通常应该避免打破锁,除非你确定拥有进程已经死亡并不 可能再释放该锁。
         和其他类型的锁一样,当你使用 NSDistributedLock 对象时,你可以通过调用 unlock 方法来释放它。

------------Using Conditions------------

        条件是一个特殊类型的锁,你可以使用它来synchronize the order in which operations must proceed.。它们和互斥锁有微妙的不同。一个线程waiting on a condition会一直处于阻塞状态直到that condition is signaled explicitly by another thread。
        由于微妙之处包含在操作系统实现上,条件锁被允许返回伪成功 spurious success,即使they were not actually signaled by your code。为了避免这些伪信号操作的问题,你应该总是在你的条件 锁里面使用一个谓词 predicate。该谓词是一个更好的方法来确定是否安全让你的线程处理。条件仅仅让你的线程保持休眠直到谓词被发送信号的线程设置了。
        以下部分介绍了如何在你的代码中使用条件。

------使用NSCondition类------

        NSCondition 类提供了和 POSIX 条件相同的语义,但是它把锁和条件数据结构封装在一个单一对象里面。结果是一个你可以像互斥锁那样使用的对象,然后等待特定条件。
        下面显示了一个code snippet,它展示了为等待一个NSCondition 对象的事件序列。cocaoCondition 变量包含了一个 NSCondition 对象,而 timeToDoWork 变量是一 个整形,它在其他线程里面发送条件信号时立即递增。
[cocoaCondition lock];
while (timeToDoWork <= 0)
    [cocoaCondition wait];
 
timeToDoWork--;
 
// Do real work here.
 
[cocoaCondition unlock];
        下面代码显示了用于给 Cocoa 条件发送信号的代码,并递增他断言变量。你应该 在给它发送信号前锁住条件
[cocoaCondition lock];
timeToDoWork++;
[cocoaCondition signal];
[cocoaCondition unlock];

------使用 POSIX 条件------

         POSIX thread condition locks require the use of both a condition data structure and a mutex. Although the two lock structures are separate, the mutex lock is intimately tied to the condition structure at runtime. Threads waiting on a signal should always use the same mutex lock and condition structures together. Changing the pairing can cause errors。
        下面代码显示了基本初始化过程,条件和predicate的使用。在初始化条件和互斥锁之后, the waiting thread enters a while loop using the ready_to_go variable as its predicate。仅当 the predicate is set and the condition subsequently signaled等待线程被唤醒和开始工作。
pthread_mutex_t mutex;
pthread_cond_t condition;
Boolean     ready_to_go = true;
 
void MyCondInitFunction()
{
    pthread_mutex_init(&mutex);
    pthread_cond_init(&condition, NULL);
}
 
void MyWaitOnConditionFunction()
{
    // Lock the mutex.
    pthread_mutex_lock(&mutex);
 
    // If the predicate is already set, then the while loop is bypassed;
    // otherwise, the thread sleeps until the predicate is set.
    while(ready_to_go == false)
    {
        pthread_cond_wait(&condition, &mutex);
    }
 
    // Do work. (The mutex should stay locked.)
 
    // Reset the predicate and release the mutex.
    ready_to_go = false;
    pthread_mutex_unlock(&mutex);
}
        signaling thread负责 setting the predicate and for sending the signal to the condition lock.。下面代码显示了实现该行为的代码。 在该例子中, the condition is signaled inside of the mutex to prevent race conditions from occurring between the threads waiting on the condition
void SignalThreadUsingCondition()
{
    // At this point, there should be work for the other thread to do.
    pthread_mutex_lock(&mutex);
    ready_to_go = true;
 
    // Signal the other thread to begin work.
    pthread_cond_signal(&condition);
 
    pthread_mutex_unlock(&mutex);
}
        注意:上述代码是显示使用 POSIX 线程条件函数的简单例子。你自己的代码应该检测这些函数返 回错误码并恰当的处理它们。

附录A:线程安全总结

        本附录描述了 Mac OS X 和 iOS 上面一些high-level thread safety of some key frameworks。本附录的信息有可能会发生改变。

------------Cocoa------------

        在 Cocoa 上面使用多线程的指南包括以下这些:
---- 不可改变的对象一般是线程安全(thread-safe)的。一旦你创建了它们,你可以把这些对象在线程间安全的传递。 另一方面,可变对象通常不是线程安全的。为了在多线程应用 里面使用可变对象,应用必须适当的同步。关于更多信息,参阅” Mutable Versus Immutable”小节。
----许多对象在多线程里面不安全的使用被视为是”线程不安全的( thread-unsafe)”。 只要同一时间只有一个线程,那么许多这些对象可以被多个线程使用。这种在应用程序的主线程的被限制对象通常被这样调用。
---- 应用的main thread负责处理事件。Although the Application Kit continues to work if other threads are involved in the event path, operations can occur out of sequence。
----如果你想使用一个线程来绘画一个视图,把所有绘画的代码放在 NSView 的 lockFocusIfCanDraw 和 unlockFocus 方法中间。
     为了在 Cocoa 里面使用 POSIX 线程,你必须首先把 Cocoa 变为多线程模式。关于更多信息,参考“ 在 Cocoa 应用里面使用 POSIX 线程”小节。

------Foundation Framework Thread Safety------

        有一种误解,认为基础框架(Foundation framework)是线程安全的,而 Application Kit 是非线程安全的。不幸的是,这是一个总的概括,从而造成一点误 导。 每个框架都包含了线程安全部分和非线程安全部分。以下部分介绍 Foundation framework 里面的线程安全部分.
---Thread-Safe Classes and Functions
       下面这些类和函数通常被认为是线程安全的。你可以在多个线程里面使用它们的 同一个实例,而无需获取一个锁。
        具体类和函数略。。。详细参考官方文档。
---Thread-Unsafe Classes
        以下这些类和函数通常被认为是非线程安全的。在大部分情况下,你可以在任何线程里面使用这些类,只要你在同一个时间只在一个线程里面使用它们。详细信息参考class documentation:
        具体类和函数略(一般是一些可变的类等)。。。详细参考官方文档
--只能用于主线程的类
         以下的类必须只能在应用的主线程类使用:NSAppleScript。
--Mutable Versus Immutable
        不可变对象通常是线程安全的。一旦你创建了它们,你可以把它们安全的在线程 间传递。当前,在使用不可变对象时,你还应该记得正确使用引用计数。如果不适当 的释放了一个你没有引用的对象,你在随后有可能造成一个异常。
        可变对象通常是非线程安全的。为了在多线程应用里面使用可变对象,应用应该 使用锁来同步访问它们(关于更多信息,参见“原子操作”部分)。通常情况下,集 合类(比如,NSMutableArray,NSMutableDictionary)是考虑多变时是非线程安全的。 这意味着,如果一个或多个线程同时改变一个数组,将会发生问题。你应该在线程读 取和写入它们的地方使用锁包围着。
        即使一个方法要求返回一个不可变对象,你不应该简单的假设返回的对象就是不可变的。依赖于方法的实现,返回的对象有可能是可变的或着不可变的。比如,一个 返回类型是 NSString 的方法有可能实际上由于它的实现返回了一个 NSMutableString。如果你想要确保对象是不可变的,你应该使用不可变的拷贝。
--Reentrancy可重入性
        可重入性是可以让同一对象或者不同对象上一个操作“中断调用("call out")”其他操作成为可 能。保持和释放对象就是一个有可能被忽视的”调用”的例子。
         以下列表列出了 Foundation framework 的部分显式的可重入对象。所有其他类 可能是或可能不是可重入的,或者它们将来有可能是可重入的。对于可重入性的一个 完整的分析是不可能完成的,而且该列表将会是无穷尽的。
         Distributed Objects、NSConditionLock 、NSDistributedLock 、NSLock、NSLog/NSLogv NSNotificationCenter 、NSRecursiveLock 、NSRunLoop 、NSUserDefaults。
--类的初始化
         Objective-C 的运行时系统在类收到其他任何消息之前给它发送一个 initialize 消息。这可以让类有机会在它被使用前设置它的运行时环境。在一个多线程应用里面, 运行时保证仅有一个线程(该线程恰好发送第一条消息给类)执行initialized 方法, 第二个线程阻塞直到第一个线程的 initialize 方法执行完成。在此期间,第一个线程可以继续调用类上其他的方法。 该 initialize 方法不应该依赖于第二个线程对这个类的调用。如果不是这样的话,两个线程将会造成死锁
--Autorelease Pools
       每个线程都维护它自己的 NSAutoreleasePool 的栈对象。Cocoa 希望在每个当前线程的栈里面有一个可用的自动释放池。 如果一个自动释放池不可用,对象将不会给释放,从而造成内存泄露。对于 Application Kit 的主线程通常它会自动创建并消耗 一个自动释放池,但是辅助线程(和其他只有 Foundationd 的程序)在使用 Cocoa 前必须自己手工创建。如果你的线程是长时间运行的,那么有可能潜在产生很多自动释放的对象,你应该周期性的销毁它们并创建自动释放池(就像 Application Kit 对 主线程那样)。否则,自动释放对象将会积累并造成内存大量占用。如果你的脱离detached线程没有使用 Cocoa,你不需要创建一个自动释放池。
--Run Loops
       每个线程都有且仅有一个run loop。然而每个run loop 和每个线程都有它自己的输入模式来决定 run loop 运行时监听哪些输入源。输入模式定义在一个 run loop 上面,不会影响定义在其他 run loop 的输入模式,即使它们的名字相同。
       如果你的线程是基于Application Kit的话,主线程的 run loop 会自动运行, 但是辅助线程(和只有 Foundation 的应用)必须自己启动它们的 run loop。如果一个脱离detached线程没有进入 run loop,那么线程在完成它们的方法执行后会立即退出。
        尽管外表显式可能是线程安全的,但是 NSRunLoop 类是非线程安全的。你只能在拥有它们的线程里面调用它实例的方法。

------Application Kit Framework Thread Safety------

--Thread-Unsafe Classes
       以下这些类和函数通常是非线程安全的。大部分情况下,你可以在任何线程使用 这些类,只要你在同一时间只有一个线程使用它们。查看这些类的文档来获得更多的 详细信息。
--NSGraphicsContext。多信息,参见“NSGraphicsContext 限制”。 
--NSImage.更多信息,参见“NSImage 限制”。
--NSResponder。
--NSWindow 和所有它的子类。更多信息,参见“Window 限制
--只能用于主线程的类
        以下的类必须只能在应用的主线程使用。
--NSCell 和所有它的子类。
--NSView 和所有它的子类。更多信息,参见“NSView 限制”。
--Window 限制
        你可以在辅助线程创建一个 window。Application Kit 确保和 window 相关的数据结构在主线程释放来避免 race conditions。在同时包含大量 windows 的应用中,window 对象有可能会发生泄漏。
        你也可以在辅助线程创建 modal window。在主线程运行 modal loop 时, Application Kit 阻塞辅助线程的调用。
--事件处理例程限制
         应用的主线程负责处理事件。主线程阻塞在 NSApplication 的 run 方法,通常该方法被包含在 main 函数里面。在Application Kit 继续工作时,如果其他线程被包含在事件路径 event path,那么操作有可能打乱顺序 (occur out of sequence)。比如,如果两个不同的线程负责关键事件, 那么关键事件有可能不是按照顺序到达。通过让主线程来处理事件,事件可以被分配到辅助线程由它们处理。
         你可以在辅助线程里面使用 NSApplication 的 postEvent:atStart 方法传递一个 事件给主线程的事件队列。然而,顺序不能保证和用户输入的事件顺序相同。应用的主线程仍然辅助处理事件队列的事件。
--绘画限制
         Application Kit 在使用它的绘画函数和类时通常是线程安全的,包括 NSBezierPath和NSString 类。关于使用这些类的详细信息,在以下各部分介绍。关于绘画的额外信息和线程可以查看 Cocoa Drawing Guide
a) NSView 限制
         NSView 通常是线程安全的,包含几个异常。你应该仅在应用的主线程里面执行对 NSView 的创建、销毁、调整大小、移动和其他操作。 在其他辅助线程里面只要你把 绘画的代码放在 lockFocusIfCanDraw 和 unlockFocus 方法之间也是线程安全的
        如果应用的辅助线程想要告知主线程重绘视图, 一定不能在辅助线程直接调用 display,setNeedsDisplay:,setNeedsDisplayInRect:,或 setViewsNeedDisplay:方 法。相反,你应该给给主线程发生一个消息让它调用这些方法,或者使用 performSelectorOnMainThread:withObject:waitUntilDone:方法。
         系统视图的图形状态(gstates)是基于每个线程不同的。使用图形状态可以在单 线程的应用里面获得更好的绘画性能,但是现在已经不是这样了。不正确使用图形状态可能导致主线程的绘画代码更低效。
b) NSGraphicsContext 限制
         NSGraphicsContext 类代表了绘画上下文,它由底层绘画系统提供。每个 NSGraphicsContext 实例都拥有它独立的绘画状态:坐标系统、裁剪、当前字体等。 该类的实例在主线程自动创建自己的 NSWindow 实例。如果你在任何辅助线程执行绘 画操作,需要特定为该线程创建一个新的 NSGraphicsContext 实例。
如果你在任何辅助线程执行绘画,你必须手工的刷新绘画调用。Cocoa 不会自动 更新辅助线程绘画的内容,所以你当你完成绘画后需要调用 NSGraphicsContext 的 flusGrahics 方法。如果你的应用程序只在主线程绘画,你不需要刷新绘画调用。
 c) NSImage 限制
        一个线程可以创建 NSImage 对象,把它绘画到图片缓冲区,还可以把它传递给主线程 来绘画。底层的图片缓存被所有线程共享。关于图片和如何缓存的更多信息,参阅 Ccocoa Drawing Guide

-----Core Data 框架-----

        Core Data 框架通常支持多线程,尽管需要注意一些使用注意事项。关于这些注意事项的更多信息,参阅 Core Data Programing Guide 的“Multi-Threading with Core Data”部分。

------------Core Foundation(核心框架)------------

         Core Foundation 是足够线程安全的,如果你的程序注意一下的话,应该不会遇到任何线程竞争的问题。通常情况下是线程安全的,比如当你查询(query)、引用 (retain)、释放(release)和传递(pass)不可变对象时。甚至在多个线程查询中央共享对象也是线程安全的。
         像 Cocoa那样,当涉及对象或它们内容可变时,Core Foundation 是非线程安全 的。比如,正如你所期望的,无论修改一个可变数据或可变数组对象,还是修改一个 可变数组里面的对象都是非线程安全的。其中一个原因是性能,这是在这种情况下的关键。此外,在该级别上实现完全线程安全是几乎不可能的。例如,你不能确定从集合中引用(retain)一个对象产生的的结果。在你来引用(retain)它所包含的对象之前集合可能已经被释放了。
          这些情况下,当你的对象被多个线程访问或修改,你的代码应该在相应的地方使用锁来保护它们不要被同时访问。例如,枚举Core Foundation 数组对象的代码,在枚举块代码周围应该使用合适的锁来保护它免遭其他线程修改。

你可能感兴趣的:(多线程,线程,线程安全,gcd)