线程编程指南翻译第四篇(同步)

同步

应用程序中存在多个线程会打开有关从多个执行线程安全访问资源的潜在问题。修改相同资源的两个线程可能会以非预期的方式相互干扰。例如,一个线程可能会覆盖另一个线程的更改,或者将应用程序置于未知且可能无效的状态。如果幸运的话,损坏的资源可能会导致明显的性能问题或崩溃,相对容易追踪和修复。但是,如果运气不好,则损坏可能会导致细微错误,这些错误直到很久之后才会显现,或者错误可能需要对基础编码进行重大改进。

在线程安全方面,良好的设计是的最佳保护。避免共享资源并最小化线程之间的交互使得这些线程不太可能相互干扰。但是,并非总能实现完全无干扰的设计。如果线程必须进行交互,则需要使用同步工具来确保它们在交互时安全地进行交互。

OS X和iOS提供了许多同步工具,从提供互斥访问的工具到在应用程序中正确排序事件的工具。以下部分介绍了这些工具以及如何在代码中使用它们来影响对程序资源的安全访问。

同步工具

为防止不同的线程意外更改数据,可以将应用程序设计为不存在同步问题,也可以使用同步工具。尽管完全避免同步问题是可取的,但并不总是可行的。以下部分介绍了可供使用的同步工具的基本类别。

原子操作

原子操作是一种简单的同步形式,适用于简单的数据类型。原子操作的优点是它们不会阻止执行线程。对于简单的操作,例如递增计数器变量,这可以带来比获取锁定更好的性能。

OS X和iOS包含许多操作,可对32位和64位值执行基本的数学和逻辑运算。这些操作包括比较和交换,测试和设置以及测试和清除操作的原子版本。有关受支持的原子操作的列表,请参阅/usr/include/libkern/OSAtomic.h头文件或参见atomic手册页

内存障碍和volatile变量

为了获得最佳性能,编译器通常会重新排序汇编级指令,以使处理器的指令流尽可能完整。作为优化的一部分,编译器可能会重新排序访问主内存的指令,因为它认为这样做不会生成不正确的数据。遗憾的是,编译器并不总是能够检测所有与内存相关的操作。如果看似单独的变量实际上相互影响,则编译器优化可能会以错误的顺序更新这些变量,从而产生可能不正确的结果。

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

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

因为内存屏障和volatile变量都会减少编译器可以执行的优化次数,所以应该谨慎使用它们,并且只在需要确保正确性的地方使用它们。有关使用内存屏障的信息,请参见 OSMemoryBarrier手册页

锁是最常用的同步工具之一。可以使用锁来保护代码的关键部分,这一段代码,一次只允许一个线程访问。例如,关键部分可能操纵特定数据结构或使用一次最多支持一个客户端的某些资源。通过在此部分周围放置一个锁,可以排除其他线程进行可能影响代码正确性的更改。

表4-1列出了常用的一些锁。OS X和iOS提供了大多数这些锁类型的实现,但不是全部。对于不受支持的锁类型,说明列说明了直接在平台上未实现这些锁的原因。

表4-1 锁定类型

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

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

有关如何使用锁的信息,请参阅使用锁

条件

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

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

该系统为几种不同技术的条件提供支持。但是,正确实现条件需要仔细编码,因此在使用条件之前,应该查看使用条件中的示例。

执行选择器例程

Cocoa应用程序具有以同步方式将消息传递到单个线程的便捷方式。在NSObject类声明对应用程序的活动线程的一个执行选择器方法。这些方法允许线程异步传递消息,并保证它们将由目标线程同步执行。例如,您可以使用执行选择器消息将分布式计算的结果传递到应用程序的主线程或指定的辅助线程。每个执行选择器的请求都在目标线程的运行循环中排队,然后按接收顺序依次处理请求。

有关执行选择器例程的摘要以及有关如何使用它们的更多信息,请参阅Cocoa执行选择器源

同步成本和性能

同步有助于确保代码的正确性,但这样做会牺牲性能。即使在无争议的情况下,使用同步工具也会引入延迟。锁和原子操作通常涉及使用内存屏障和内核级同步来确保代码得到适当保护。如果存在锁定争用,您的线程可能会阻塞并经历更大的延迟。

表4-2列出了与无争议情况下的互斥和原子操作相关的一些近似成本。这些测量值代表数千个样本的平均时间。与线程创建时间一样,互斥锁获取时间(即使在无争议的情况下)也会因处理器负载,计算机速度以及可用系统和程序存储器的数量而有很大差异。

表4-2 互斥和原子操作成本

条目 近似成本 说明
互斥获取时间 大约0.2微秒 这是无争议情况下的锁定获取时间。如果锁定由另一个线程保持,则获取时间可以更长。这些数据是通过分析在基于Intel的iMac上使用2 GHz Core Duo处理器和运行OS X v10.5的1 GB RAM的互斥锁获取时产生的平均值和中值来确定的。
原子比较和交换 大约0.05微秒 这是无争议情况下的比较和交换时间。这些数字是通过分析操作的平均值和中值来确定的,并且是在基于Intel的iMac上生成的,该iMac具有2 GHz Core Duo处理器和运行OS X v10.5的1 GB RAM。

在设计并发任务时,正确性始终是最重要的因素,但您也应该考虑性能因素。在多个线程下正确执行但比在单个线程上运行的相同代码慢的代码几乎不是改进。

如果要对现有的单线程应用程序进行改造,则应始终对关键任务的性能进行一组基线测量。添加其他线程后,您应该对这些相同的任务进行新的测量,并将多线程案例的性能与单线程案例进行比较。如果在调整代码之后,线程不会提高性能,您可能需要重新考虑特定的实现或完全使用线程。

有关性能和收集指标的工具的信息,请参阅性能概述。有关锁和原子操作成本的特定信息,请参阅线程成本

线程安全和信号

当涉及到线程应用程序时,没有什么比处理信号的问题引起更多的恐惧或困惑。信号是一种低级BSD机制,可用于将信息传递给进程或以某种方式对其进行操作。有些程序使用信号来检测某些事件,例如子进程的死亡。系统使用信号终止失控过程并传达其他类型的信息。

信号问题不是它们的作用,而是当应用程序有多个线程时它们的行为。在单线程应用程序中,所有信号处理程序都在主线程上运行。在多线程应用程序中,与特定硬件错误(例如非法指令)无关的信号将被传送到当时正在运行的任何线程。如果同时运行多个线程,则信号将传递给系统正在选择的任何一个。换句话说,信号可以传递到应用程序的任何线程。

在应用程序中实现信号处理程序的第一条规则是避免假设哪个线程正在处理信号。如果特定线程想要处理给定信号,则需要在信号到达时通过某种方式通知该线程。您不能只假设从该线程安装信号处理程序将导致信号传递到同一线程。

有关信号和安装信号处理程序的更多信息,请参见signal和sigaction手册页

线程安全设计提示

同步工具是使代码线程安全的有用方法,但它们不是灵丹妙药。使用过多,与其非线程性能相比,锁和其他类型的同步实际上可以降低应用程序的线程性能。在安全性和性能之间找到适当的平衡是一种需要经验的艺术。以下部分提供了一些提示,可帮助您为应用程序选择适当的同步级别。

完全避免同步

对于您正在处理的任何新项目,甚至对于现有项目,设计代码和数据结构以避免同步是最佳解决方案。尽管锁和其他同步工具很有用,但它们确实会影响任何应用程序的性能。如果整体设计导致特定资源之间存在高争用,那么您的线程可能会等待更长时间。

实现并发的最佳方法是减少并发任务之间的交互和相互依赖关系。如果每个任务都在自己的私有数据集上运行,则不需要使用锁来保护该数据。即使在两个任务共享公共数据集的情况下,您也可以查看设置分区的方法或为每个任务提供自己的副本。当然,复制数据集也有其成本,因此在做出决定之前,您必须权衡这些成本与同步成本。

了解同步的限制

只有当应用程序中的所有线程一致地使用同步工具时,它们才有效。如果创建互斥锁以限制对特定资源的访问,则在尝试操作资源之前,所有线程都必须获取相同的互斥锁。如果不这样做会失败互斥锁提供的保护,并且是程序员错误。

意识到编码正确性的威胁

使用锁和内存屏障时,应始终仔细考虑它们在代码中的位置。即使看起来很好的锁也会让你陷入虚假的安全感。下面的一系列示例试图通过指出看似无害的代码中的缺陷来说明这个问题。基本前提是你有一个包含一组不可变对象的可变数组。假设您要调用数组中第一个对象的方法。您可以使用以下代码执行此操作:

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方法需要很长时间才能执行,这可能会导致您的代码长时间保持锁定,这可能会造成性能瓶颈。

代码的问题不是关键区域定义不明确,而是实际问题没有被理解。真正的问题是仅由其他线程的存在触发的内存管理问题。因为它可以被另一个线程释放,所以更好的解决方案是anObject在释放锁之前保留。该解决方案解决了被释放对象的真正问题,并且没有引入潜在的性能损失。

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

虽然前面的例子非常简单,但它们确实说明了非常重要的一点。说到正确性,你必须超越明显的问题。内存管理和设计的其他方面也可能受到多线程的影响,因此您必须预先考虑这些问题。此外,你应该总是假设编译器在安全方面会做最糟糕的事情。这种意识和警惕应该可以帮助您避免潜在的问题,并确保您的代码正常运行。

有关如何使程序具有线程安全性的其他示例,请参阅“线程安全摘要”。

注意死锁和活锁

每当线程同时尝试获取多个锁时,就有可能发生死锁。当两个不同的线程持有另一个需要的锁,然后尝试获取另一个线程持有的锁时,就会发生死锁。结果是每个线程永久阻塞,因为它永远不会获得另一个锁。

活锁类似于死锁,当两个线程竞争同一组资源时发生。在活锁情况下,线程放弃其第一个锁以尝试获取其第二个锁。一旦获得第二个锁,它就会返回并尝试再次获取第一个锁。它锁定,因为它花费所有时间释放一个锁并试图获得另一个锁,而不是做任何实际的工作。

避免死锁和活锁情况的最佳方法是一次只能锁定一个锁。如果您必须一次获得多个锁,则应确保其他线程不会尝试执行类似的操作。

正确使用volatile变量

如果您已经使用互斥锁来保护代码段,请不要自动假设您需要使用volatile关键字来保护该部分中的重要变量。互斥锁包括一个内存屏障,以确保正确的加载和存储操作顺序。将volatile关键字添加到关键部分中的变量会强制每次访问时从内存加载该值。在特定情况下,两种同步技术的组合可能是必要的,但也会导致显着的性能损失。如果仅使用互斥锁足以保护变量,则省略volatile关键字。

同样重要的是,不要使用volatile变量来避免使用互斥锁。通常,互斥体和其他同步机制是比volatile变量更好的保护数据结构完整性的方法。的volatile关键字仅确保一个变量被从存储器加载,而不是存储在寄存器中。它不能确保代码正确访问变量

使用原子操作

非阻塞同步是一种执行某些类型操作并避免锁定费用的方法。尽管锁是同步两个线程的有效方法,但获取锁是一种相对昂贵的操作,即使在无争议的情况下也是如此。相比之下,许多原子操作只需要一小部分时间就可以完成,并且可以像锁一样有效。

通过原子操作,您可以对32位或64位值执行简单的数学和逻辑运算。这些操作依赖于特殊的硬件指令(和可选的内存屏障),以确保在再次访问受影响的内存之前完成给定的操作。在多线程情况下,您应始终使用包含内存屏障的原子操作,以确保在线程之间正确同步内存。

表4-3列出了可用的原子数学和逻辑运算以及相应的函数名称。这些函数都在/usr/include/libkern/OSAtomic.h头文件中声明,您也可以在其中找到完整的语法。这些功能的64位版本仅在64位进程中可用。

表4-3 原子数学和逻辑运算

操作 函数名 描述
OSAtomicAdd32,OSAtomicAdd32Barrier,OSAtomicAdd64,OSAtomicAdd64Barrier 将两个整数值一起添加,并将结果存储在其中一个指定变量中。
递增 OSAtomicIncrement32,OSAtomicIncrement32Barrier,OSAtomicIncrement64,OSAtomicIncrement64Barrier 将指定的整数值递增1。
递减 OSAtomicDecrement32,OSAtomicDecrement32Barrier,OSAtomicDecrement64,OSAtomicDecrement64Barrier 将指定的整数值减1
逻辑或 OSAtomicOr32,OSAtomicOr32Barrier 在指定的32位值和32位掩码之间执行逻辑OR。
逻辑和 OSAtomicAnd32,OSAtomicAnd32Barrier 在指定的32位值和32位掩码之间执行逻辑AND。
逻辑异或 OSAtomicXor32,OSAtomicXor32Barrier 在指定的32位值和32位掩码之间执行逻辑XOR。
比较和交换 OSAtomicCompareAndSwap32,OSAtomicCompareAndSwap32Barrier,OSAtomicCompareAndSwap64,OSAtomicCompareAndSwap64Barrier,OSAtomicCompareAndSwapPtr,OSAtomicCompareAndSwapPtrBarrier,OSAtomicCompareAndSwapInt,OSAtomicCompareAndSwapIntBarrier,OSAtomicCompareAndSwapLong,OSAtomicCompareAndSwapLongBarrier 将变量与指定的旧值进行比较。如果两个值相等,则此函数将指定的新值分配给变量; 否则,它什么都不做。比较和赋值作为一个原子操作完成,函数返回一个布尔值,指示交换是否实际发生。
测试和设置 OSAtomicTestAndSet,OSAtomicTestAndSetBarrier 在指定变量中测试一下,将该位设置为1,并将旧位的值作为布尔值返回。比特根据(0x80 >> (n & 7)) 字节的公式进行测试,((char*)address + (n >> 3))其中n是位数,address是指向变量的指针。该公式有效地将变量分解为8位大小的块,并反向排序每个块中的位。例如,要测试32位整数的最低位(位0),实际上将为位号指定7; 同样,要测试最高位(位32),您可以为位号指定24。
测试清除 OSAtomicTestAndClear,OSAtomicTestAndClearBarrier 在指定变量中测试一下,将该位设置为0,并将旧位的值作为布尔值返回。比特根据(0x80 >> (n & 7)) 字节的公式进行测试,((char*)address + (n >> 3))其中n是位数,address是指向变量的指针。该公式有效地将变量分解为8位大小的块,并反向排序每个块中的位。例如,要测试32位整数的最低位(位0),实际上将为位号指定7; 同样,要测试最高位(位32),您可以为位号指定24。

大多数原子函数的行为应该是相对简单的,你会期望的。但是,清单4-1显示了原子测试和设置以及比较和交换操作的行为,这些操作稍微复杂一些。对该OSAtomicTestAndSet函数的前三次调用演示了如何在整数值上使用位操作公式,其结果可能与您期望的不同。最后两个调用显示了该OSAtomicCompareAndSwap32函数的行为。在所有情况下,当没有其他线程正在操作值时,在无争议的情况下调用这些函数。

清单4-1 执行原子操作

int32_t  theValue = 0;
OSAtomicTestAndSet(0, &theValue);
// theValue现在是128。
 
theValue = 0;
OSAtomicTestAndSet(7, &theValue);
// theValue现在是1。
 
theValue = 0;
OSAtomicTestAndSet(15, &theValue)
// theValue现在是256。
 
OSAtomicCompareAndSwap32(256, 512, &theValue);
// theValue现在是512。
 
OSAtomicCompareAndSwap32(256, 1024, &theValue);
// theValue仍然是512。

有关原子操作的信息,请参见atomic手册页和/usr/include/libkern/OSAtomic.h头文件。

使用锁

锁是线程编程的基本同步工具。使用锁可以轻松保护大部分代码,从而确保代码的正确性。OS X和iOS为所有应用程序类型提供基本的互斥锁,而Foundation框架为特殊情况定义了互斥锁的一些其他变体。以下部分介绍如何使用其中几种锁类型。

使用POSIX Mutex锁

POSIX互斥锁从任何应用程序都非常容易使用。要创建互斥锁,请声明并初始化pthread_mutex_t结构。要锁定和解锁互斥锁,请使用pthread_mutex_lock 和 pthread_mutex_unlock功能。清单4-2显示了初始化和使用POSIX线程互斥锁所需的基本代码。完成锁定后,只需拨打电话即可pthread_mutex_destroy 释放锁数据结构。

清单4-2 使用互斥锁

pthread_mutex_t mutex;
void MyInitFunction()
{
    pthread_mutex_init(&mutex, NULL);
}
 
void MyLockingFunction()
{
    pthread_mutex_lock(&mutex);
    // 做工作
    pthread_mutex_unlock(&mutex);
}

注意: 前面的代码是一个简化的示例,旨在显示POSIX线程互斥函数的基本用法。您自己的代码应该检查这些函数返回的错误代码并适当地处理它们。

使用NSLock类

一个 NSLock对象为Cocoa应用程序实现了一个基本的互斥锁。所有锁(包括NSLock)的接口实际上由NSLocking协议定义,协议定义lock和unlock方法。您可以像使用任何互斥锁一样使用这些方法来获取和释放锁定。

除了标准的锁定行为,NSLock该类还添加了tryLock 和 lockBeforeDate:方法。该tryLock方法尝试获取锁定但不阻止锁定是否不可用; 相反,该方法只是返回NO。如果在指定的时间限制内未获取锁定,则lockBeforeDate:该方法尝试获取锁定但取消阻塞线程(并返回NO)。

以下示例显示如何使用NSLock对象协调可视显示的更新,其数据由多个线程计算。如果线程无法立即获取锁定,它只会继续计算,直到它可以获取锁定并更新显示。

BOOL moreToDo = YES;
NSLock *theLock = [[NSLock alloc] init];
...
while (moreToDo) {
    /* 做另一个计算增量 */
    /* 直到没有其他事可做。 */
    if ([theLock tryLock]) {
        /* 更新所有线程使用的显示。*/
        [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在每个线程上为参数传递一个不同的对象,每个线程都会锁定并继续处理,而不会被另一个阻塞。但是,如果在两种情况下都传递相同的对象,则其中一个线程将首先获取锁定,另一个线程将阻塞,直到第一个线程完成关键部分。

作为预防措施,该@synchronized块隐式地向受保护代码添加异常处理程序。如果抛出异常,此处理程序会自动释放互斥锁。这意味着为了使用该@synchronized指令,还必须在代码中启用Objective-C异常处理。如果您不希望由隐式异常处理程序引起额外开销,则应考虑使用锁类。

有关该@synchronized指令的更多信息,请参阅Objective-C编程语言

使用其他可可锁
以下部分描述了使用其他几种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对象响应的锁定和解锁方法可以任意组合使用。例如,您可以将lock消息与unlockWithCondition:或者a lockWhenCondition:消息unlock。当然,后一种组合解锁了锁,但可能不释放任何等待特定条件值的线程。

以下示

id condLock = [[NSConditionLock alloc] initWithCondition:NO_DATA];
 
while(true)
{
    [condLock lock];
    /* 将数据添加到队列中。 */
    [condLock unlockWithCondition:HAS_DATA];
}

因为锁的初始条件设置为NO_DATA,生产者线程最初应该没有问题获取锁。它用数据填充队列并将条件设置为HAS_DATA。在后续迭代期间,生产者线程可以在到达时添加新数据,无论队列是空还是仍有一些数据。它阻止的唯一时间是消费者线程从队列中提取数据。

由于使用者线程必须具有要处理的数据,因此它使用特定条件在队列上等待。当生产者将数据放入队列时,消费者线程会唤醒并获取其锁定。然后,它可以从队列中提取一些数据并更新队列状态。以下示例显示了使用者线程的处理循环的基本结构。

while (true)
{
    [condLock lockWhenCondition:HAS_DATA];
    /* 从队列中删除数据。 */
    [condLock unlockWithCondition:(isEmpty ? NO_DATA : HAS_DATA)];
 
    // 在本地处理数据。
}

使用NSDistributedLock对象

该NSDistributedLock类 可以由多个主机上的多个应用程序使用,以限制对某些共享资源(如文件)的访问。锁本身实际上是一个互斥锁,它使用文件系统项(例如文件或目录)实现。对于NSDistributedLock可以使用的对象,锁必须可由所有使用它的应用程序写入。这通常意味着将其放在一个文件系统上,该文件系统可供运行该计算机的所有应用程序访问。

与其他类型的锁不同,NSDistributedLock不符合NSLocking协议,因此没有lock方法。lock方法将阻塞线程的执行,并要求系统以预定的速率来轮询锁。而不是对你的代码施加这种惩罚,NSDistributedLock提供了一个tryLock 方法,让您决定是否轮询。

因为它是使用文件系统实现的,所以NSDistributedLock除非所有者明确释放它,否则不会释放对象。如果您的应用程序在持有分布式锁定时崩溃,则其他客户端将无法访问受保护资源。在这种情况下,你可以使用breakLock打破现有锁定的方法,以便您可以获取它。但是,通常应该避免断开锁定,除非您确定拥有过程已经死亡并且无法释放锁定。

与其他类型的锁一样,当您使用NSDistributedLock完对象时,可以通过调用该unlock方法来释放它。

使用条件

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

由于实现操作系统所涉及的微妙之处,条件锁允许以虚假的成功返回,即使它们实际上没有通过您的代码发出信号。为避免这些虚假信号引起的问题,您应始终将谓词与条件锁一起使用。谓词是一种更具体的方法,用于确定线程是否安全。该条件只是让您的线程保持睡眠状态,直到可以通过信令线程设置谓词。

以下部分介绍如何在代码中使用条件。

使用NSCondition类

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

清单4-3显示了一个代码片段,演示了等待NSCondition对象的事件序列。该cocoaCondition变量包含NSCondition对象和timeToDoWork变量是从另一个线程递增之前立即信令的条件的整数。

清单4-3 使用Cocoa条件

[cocoaCondition lock];
while (timeToDoWork <= 0)
    [cocoaCondition wait];
 
timeToDoWork--;
 
// 在这里做真正的工作
 
[cocoaCondition unlock];

清单4-4显示了用于表示Cocoa条件并增加谓词变量的代码。在发出信号之前,您应该始终锁定条件。

清单4-4 发出Cocoa信号的信号

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

使用POSIX条件

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

清单4-5显示了条件和谓词的基本初始化和用法。在初始化条件和互斥锁之后,等待线程使用ready_to_go变量作为其谓词进入while循环。只有在设置了谓词并且随后发出条件信号时,等待线程才会唤醒并开始执行其工作。

清单4-5 使用POSIX条件

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()
{
    // 锁定互斥锁。
    pthread_mutex_lock(&mutex);
 
    // 如果已设置谓词,则绕过while循环;
    // 否则,线程会一直睡眠,直到设置了谓词。
    while(ready_to_go == false)
    {
        pthread_cond_wait(&condition, &mutex);
    }
 
    // 做工作。(互斥锁应保持锁定状态。)
 
    // 重置谓词并释放互斥锁。
    ready_to_go = false;
    pthread_mutex_unlock(&mutex);
}

信号线程负责设置谓词和将信号发送到条件锁。 清单4-6显示了实现此行为的代码。在此示例中,在互斥锁内部发出条件信号,以防止在等待条件的线程之间发生竞争条件。

清单4-6 发出条件锁定信号

void SignalThreadUsingCondition()
{
    // 此时,应该有其他线程可以做的工作。
    pthread_mutex_lock(&mutex);
    ready_to_go = true;
 
    // 发信号通知另一个线程开始工作。
    pthread_cond_signal(&condition);
 
    pthread_mutex_unlock(&mutex);
}

注意: 前面的代码是一个简化的示例,旨在显示POSIX线程条件函数的基本用法。您自己的代码应该检查这些函数返回的错误代码并适当地处理它们。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容