Locks

Atomic

原子操作是一种简单的同步形式,适用于简单的数据类型。原子操作的优点是它们不会阻塞竞争线程。对于简单的操作(例如增加计数器变量),这可以比锁获得更好的性能。

Memory Barriers and Volatile Variables

内存屏障是一种非阻塞同步工具,用于确保内存操作以正确的顺序发生。内存屏障的作用类似于围栏,迫使处理器在允许执行位于屏障之后的加载和存储操作之前,完成位于屏障前面的所有加载和存储操作。内存屏障通常用于确保一个线程(但对另一线程可见)的内存操作始终按预期顺序发生。在这种情况下,缺少内存屏障可能会使其他线程看到看似不可能的结果。(例如,请参阅 Wikipedia 以了解 memory barriers)要使用内存屏障,只需在代码中的适当位置调用 OSMemoryBarrier 函数即可。

Volatile variables 将另一种类型的内存约束应用于单个变量。编译器通常通过将变量的值加载到寄存器中来优化代码。对于局部变量,这通常不是问题。但是,如果从另一个线程可见该变量,则这种优化可能会阻止另一个线程注意到该变量的任何更改。将 volatile 关键字应用于变量会强制编译器每次使用时从内存中加载该变量。如果可以通过编译器可能无法检测到的,外部源随时更改其值,则可以将变量声明为 volatile。

Locks

互斥(或 mutex)锁充当资源周围的保护屏障。互斥锁是一种信号量,它一次只能授予对一个线程的访问权限。如果正在使用互斥锁,而另一个线程试图获取该互斥锁,则该线程将阻塞,直到该互斥锁被其原始持有者释放为止。如果多个线程竞争同一个互斥锁,则一次只能允许一个访问它。

递归锁是互斥锁的一种变体。递归锁允许单个线程在释放它之前多次获取该锁。其他线程将保持阻塞状态,直到锁的所有者以获取锁的相同次数释放锁。递归锁主要在递归迭代期间使用,但也可以在多个方法各自需要分别获取锁的情况下使用。

读写锁也称为共享独占锁。这种类型的锁通常用于较大规模的操作,如果经常读取受保护的数据结构并且仅偶尔进行修改,则可以显着提高性能。在正常操作期间,多个读取器可以同时访问数据结构。但是,当线程要写入结构时,它将阻塞,直到所有读取器都释放锁为止,此时,它获取了锁并可以更新结构。当写入线程正在等待锁定时,新的读取线程将阻塞,直到写入线程完成。系统仅支持使用 POSIX 线程的读写锁。有关如何使用这些锁的更多信息,请参见 pthread 手册页。

自旋锁反复轮询其锁定条件,直到该条件变为 true。自旋锁最常用于锁的预期等待时间较短的多处理器系统上。在这些情况下,轮询通常比阻塞线程更有效,这涉及到上下文切换和线程数据结构的更新。由于其轮询性质,该系统不提供自旋锁的任何实现,但是您可以在特定情况下轻松实现它们。有关在内核中实现自旋锁的信息,请参见 Kernel Programming Guide

1. pthread_mutex
2. NSLock

NSLock 对象为 Cocoa 应用程序实现了基本互斥量。
除了标准的锁定行为外,NSLock 类还添加了 tryLocklockBeforeDate: 方法。tryLock 方法尝试获取锁,但如果锁不可用则不会阻塞; 相反,该方法仅返回 NO。lockBeforeDate: 方法尝试获取锁,但如果在指定的时间限制内未获取锁,则取消阻塞线程(并返回NO)。

3. @synchronize

@synchronized 指令是在 Objective-C 代码中动态创建互斥锁的便捷方法。 @synchronized 指令执行任何其他互斥锁将执行的操作 - 它防止不同的线程同时获取同一锁。
作为预防措施,@synchronized 块会为受保护的代码隐式添加一个异常处理程序。如果抛出异常,此处理程序将自动释放互斥量。这意味着为了使用 @synchronized 指令,您还必须在代码中启用 Objective-C 异常处理。如果您不希望由隐式异常处理程序引起的额外开销,则应考虑使用锁类。

4. NSRecursiveLock

递归锁是一种锁,它允许锁定它的同一线程再次锁定它。这样做的好处是您不必担心什么被锁定以及什么不在特定线程上。它们还使将锁改型进现有代码变得容易。
NSRecursiveLock 类定义了一个锁,同一线程可以多次获取该锁,而不会导致线程死锁。递归锁跟踪成功获取了多少次。每次成功获取锁都必须通过相应的调用来平衡,以解锁该锁。仅当所有锁定和解锁调用均达到平衡时,才实际释放该锁定,以便其他线程可以获取它。
顾名思义,这种类型的锁通常在递归函数内部使用,以防止递归阻塞线程。

5. NSConditionLock

NSConditionLock 对象定义一个互斥锁,该互斥锁可以使用特定值进行锁定和解锁。您不应将这种类型的锁与条件混淆(请参见 Conditions)该行为在某种程度上类似于条件,但实现方式却大不相同。

通常,当线程需要以特定顺序执行任务时(例如当一个线程产生另一个消耗的数据时),您可以使用 NSConditionLock 对象。生产者执行时,消费者使用特定于您的程序的条件来获取锁。(条件本身只是您定义的整数值)生产者完成时,它将解锁锁,并将锁定条件设置为适当的整数值以唤醒使用者线程,然后消费者线程继续处理数据。

NSConditionLock 对象响应的锁定和解锁方法可以任意组合使用。例如,您可以将锁定消息与 unlockWithCondition: 配对,或者将 lockWhenCondition: 消息与 unlock 配对。当然,后一种组合可以解锁该锁,但可能不会释放等待特定条件值的任何线程。

Conditions

条件是信号量的另一种类型,当某个条件为真时,它允许线程彼此发信号。条件通常用于指示资源的可用性或确保任务以特定顺序执行。当线程测试条件时,除非该条件已经为真,否则它将阻塞。它保持阻塞状态,直到其他线程显式更改并发出条件信号为止。条件和互斥锁之间的区别在于,可以允许多个线程同时访问该条件。条件更多是看门人,它根据某些指定的标准让不同的线程通过门。

条件是一种特殊类型的锁,可用于同步操作必须进行的顺序。它们与互斥锁有一个微妙的区别。等待条件的线程保持阻塞状态,直到该条件被另一个线程显式发出信号为止。

您应始终将谓词与条件锁结合使用。谓词是确定线程继续执行是否安全的更具体方法。该条件只是让您的线程处于睡眠状态,直到可以由信号线程设置谓词为止。

NSCondition

NSCondition 类提供与 POSIX 条件相同的语义,但是将所需的条件数据结构都包装在一个对象中。结果是可以像互斥锁一样锁定对象,然后像条件一样等待。

pthread_cond_t

POSIX 线程条件锁要求同时使用条件数据结构互斥锁。尽管两个锁结构是分开的,但互斥锁在运行时与条件结构密切相关。等待信号的线程应始终一起使用相同的互斥锁和条件结构。更改配对会导致错误。

==========================================================

OSSpinLock -> os_unfair_lock

自旋锁反复轮询其锁定条件,直到该条件变为 true。自旋锁最常用于 锁的预期等待时间较短的多处理器系统上。在这些情况下,轮询通常比阻塞线程更有效,这涉及到上下文切换和线程数据结构的更新。由于其轮询性质,该系统不提供自旋锁的任何实现,但是您可以在特定情况下轻松实现它们。有关在内核中实现自旋锁的信息,请参见 Kernel Programming Guide

Dispatch semaphores

Dispatch semaphore 与传统信号量相似,但通常更有效。仅当由于信号量不可用而需要阻塞调用线程时,dispatch semaphores 才调用内核。如果信号量可用,则不进行内核调用。有关如何使用 dispatch semaphores 的示例,如下:

使用 Dispatch Semaphores 来调节有限资源的使用

如果要提交给 dispatch queues 的任务访问某些有限资源,则可能要使用 dispatch semaphore 来调节同时访问该资源的任务数。dispatch semaphore 的作用类似于常规信号量,但有一个例外。当资源可用时,获取 dispatch semaphore 所需的时间少于获取传统系统信号量所需的时间。这是因为对于这种特殊情况,GCD 不会调用内核。它唯一调用内核的时间是当资源不可用时,系统需要停放线程,直到发信号为止。

使用 dispatch semaphore 的语义如下:

  • 创建信号量时(using the dispatch_semaphore_create function),可以指定一个正整数,指示可用资源的数量
  • 在每个任务中,调用 dispatch_semaphore_wait 来等待信号量
  • 等待调用返回时,获取资源并进行工作
  • 完成资源处理后,释放它,并通过调用 dispatch_semaphore_signal 函数来发出信号量。

有关这些步骤如何工作的示例,请考虑在系统上使用文件描述符。每个应用程序都可以使用有限数量的文件描述符。如果您有一个处理大量文件的任务,那么您不想一次打开这么多文件而导致文件描述符用尽。相反,您可以使用信号量来限制文件处理代码一次使用的文件描述符数量。您将合并到任务中的基本代码如下:

// 创建信号量,指定初始池大小
dispatch_semaphore_t fd_sema = dispatch_semaphore_create(getdtablesize() / 2);

// 等待一个文件描述符
dispatch_semaphore_wait(fd_sema, DISPATCH_TIME_FOREVER);
fd = open("/etc/services", O_RDONLY);

// 完成后释放文件描述符
close(fd);
dispatch_semaphore_signal(fd_sema);

创建信号量时,请指定可用资源的数量。此值成为信号量的初始计数变量。每次您等待信号量时,dispatch_semaphore_wait 函数都会将对变量的计数减 1。如果结果值为负,则该函数告诉内核阻塞线程。另一方面,dispatch_semaphore_signal 函数将 count 变量增加 1,以指示资源已被释放。如果有任务被阻塞并等待资源,则随后其中一个将被解除阻塞并被允许执行其工作。

==========================================================

More

dispatch_barrier_async

提交该块后,始终会立即返回对此函数的调用,并且永远不要等待该块被调用。当 barrier 块到达私有并发队列的前面时,不会立即执行。而是,队列等待直到其当前正在执行的块完成执行。此时,barrier 块将自行执行。在 barrier 块完成之前,不会执行在 barrier 块之后提交的任何块。

您指定的队列应该是您使用 dispatch_queue_create 函数创建的并发队列。如果传递给此函数的队列是串行队列或全局并发队列之一,则此函数的行为类似于 dispatch_async 函数。

dispatch barrier 创建一个读写锁,为实现多读者单一写者提供了一个优雅的解决方案。

std::lock_guard

在最近读的 Texture 源码中,采用了 std::lock_guard 这种 C++ 的特性。

lock_guard 是互斥体包装器,为在作用域块期间占有互斥提供便利 RAII 风格 机制。
创建 lock_guard 对象时,它试图接收给定互斥的所有权。控制离开创建 lock_guard 对象的作用域时,销毁 lock_guard 并释放互斥。

与传统方式相比较,示例如下:

std::mutex m;

void bad() 
{
  m.lock();                      // 请求互斥体
  f();                           // 若 f() 抛异常,则互斥体永远不被释放
  if(!everything_ok()) return;   // 提早返回,互斥体永远不被释放
  m.unlock();                    // 若 bad() 抵达此语句,互斥才被释放
}

void good()
{
  std::lock_guard<std::mutex> lk(m);  // RAII类:互斥体的请求即是初始化
  f();                                // 若 f() 抛异常,则释放互斥体
  if(!everything_ok()) return;        // 提早返回,互斥体被释放
}                                     // 若 good() 正常返回,则释放互斥体

References

Threading Programming Guide
http://www.zaval.org/resources/library/butenhof1.html
http://www.fieryrobot.com/blog/2008/10/14/recursive-locks-will-kill-you/
http://tutorials.jenkov.com/java-concurrency/index.html

推荐阅读更多精彩内容