<译>iOS中的同步操作

翻译:Synchronization

同步

        应用程序中存在多个线程会导致潜在的问题,这些问题可能会导致从多个执行线程安全访问资源。修改相同资源的两个线程可能会以非预期的方式相互干扰。例如,一个线程可能会覆盖另一个线程的更改,或者将应用程序置于未知的且无效的状态。如果足够幸运的话,损坏的资源可能会导致明显的性能问题或者崩溃,这些问题相对容易追踪和修复。然而,如果不幸运,就可能会导致微妙的错误,直到很久以后才会出现,或者错误可能需要对下层的代码进行重大的检测修复。

        说到线程安全性,一个好的设计就是最好的保护。避免共享资源并尽量减少线程之间的交互使得线程不太可能相互干扰。然而,完全无干扰的设计并不总是可行的。在线程必须交互的情况下,使用同步工具来确保当他们交付时,他们可以安全地执行。

        OX X和iOS提供了许多同步工具供我们使用,包括提供互斥访问的工具,以及在应用程序中正确排序的工具。下面各节将介绍这些工具以及如何在代码中使用他们来影响对程序资源的安全访问。

一、同步工具

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

1、原子操作

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

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

 2、内存屏障(memory barrier)和易变变量(Volatile variables)

        为了达到最佳性能,编译器通常会对汇编级指令进行重新排序,以尽可能保持处理器的指令流水线。作为这种优化的一部分,编译器可能会重新排序访问主内存的指令,因为它认为这样做不会产生不正确的数据。不幸的是,编译器并不总是能够检测到所有依赖于内存的操作。如果看似单独的变量实际上相互影响,编译器优化可能会以错误的顺序更新这些变量,从而产生潜在的错误结果。

        内存屏障(memory barrier)是一种非阻塞同步工具,用于确保内存操作以正确的顺序执行。内存屏障就像栅栏一样,强制处理器完成位于栅栏前的任何加载和存储操作。内存屏障通常用于确保一个线程(但对另一个线程可见)的内存操作始终按预期的顺序进行。在这种情况下缺乏内存屏障可能会让其他线程看到看似不可能的结果。请参阅维基百科memory barriers。要使用内存屏障,只需要在代码中适当位置调用OSMemoryBarrier函数即可。

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

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

3、锁

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

        下面列出了我们常使用的一些锁。OS X和iOS为大多数这些锁提供了实现,但不是全部。对于不支持的锁定类型,说明部分解释了为什么这些锁不能直接在平台上实现的原因。

3.1、锁的类型

3.1.1、互斥锁

        互斥锁作为资源周围的保护屏障。互斥锁是一种信号量,一次只允许访问一个线程。如果一个互斥体正在使用,而另一个线程试图获取它,则该线程将阻塞,直到互斥体被其原始持有者释放。如果多个线程竞争相同的互斥量,则一次只允许一个线程访问它。

3.1.2、递归锁

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

3.1.3、读写锁

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

3.1.4、分布式锁

        分布式锁在进程级别提供互斥访问。与真正的互斥锁不同,分布式锁不会阻塞进程或阻止进程运行。它只是报告锁何时忙,并让流程决定如何继续。

3.1.5、自旋锁

        自旋锁反复轮询其锁定条件,直到该条件成立。自旋锁最常用于预计等待锁定时间较短的多处理器系统。在这些情况下,轮询通常比拦截线程更有效,后者涉及上下文切换和线程数据结构的更新。由于轮询性质,系统不提供自旋锁的任何实现,但我们可以在特定情况下轻松实现它们。有关在内核中实现自旋锁的信息,请参阅内核编程指南

3.1.6、双重检查锁

        双重检查锁试图通过锁定之前测试锁定标准来降低获取锁的开销。由于双重检查的锁可能不安全,系统不提供对他们的明确支持,因此不鼓励使用它们。

注意:大多数类型的锁还包含内存屏障(memory barrier),以确保在进入临界区之前完成任何前面加载和存储指令。有关如何使用锁定的信息,请参阅使用锁定。

3.2、条件

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

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

该系统为几种不同技术的条件提供支持。然而,正确的条件实现需要仔细的编码,所以我们应该在使用条件之前查看自己代码中的例子。

3.3、执行选择器

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

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

二、同步的代价和性能

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

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

1.互斥量和原子操作的代价对比

        <1>.互斥量采集时间,大约0.2微妙,这是无争议的情况下的锁定获取时间。如果锁由另一个线程保存,则获取时间可能会更长。这些数据是通过分析采用基于Intel的iMac(采用2 GHz Core Duo处理器和1 GB运行OS X v10.5的RAM)获取互斥量过程中生成的平均值和中值来确定的。

        <2>.原子比较和交换,大约0.05微妙,这是无争议的情况下的比较和交换时间。这些数字是通过分析操作的平均值和中值确定的,并且是基于Intel的iMac上使用2 Ghz Core Duo处理器和1 GB运行OS X v10.5的RAM生成的。

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

        如果我们正在改进现有的单线程应用程序,则应始终对关键任务的性能进行一系列基准测量。在添加额外的线程后,应该对这些相同的任务进行新的测量,并将多线程案例的性能和单线程案例进行比较。如果在调整代码之后,线程并不会提高性能,则可能需要重新考虑关键任务的实现内容。

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

三、线程安全和信号

        当涉及到线程应用时,没有什么比处理信号更令人担心或者混乱。信号是一种低级别的BSD机制,可用于向进程传递信息或以某种方式操作它。一些程序使用信号来检测某些事件,例如子进程的死亡。该系统使用信号来终止失控进程并传达其他类型的信息。

        信号问题是应用程序使用多线程时的一种行为。在单个线程应用程序中,所有的信号处理程序都在主线程上运行。在多线程应用程序中,与特定硬件错误无关的信号将传送到当时正在运行的线程。如果多个线程同时运行,则信号被传送到系统随机挑选的任何一个线程。也就是信号可以传送到应用程序的任何线程。

        在应用程序中实现信号处理程序的第一条规则是避免假设哪个线程正在处理信号。如果某个特定的线程想要处理给定的信号,则需要在信号到达时通过某种方式通知该线程。所以不能假设获取信号后信号的处理线程一定是目标处理线程。

四、线程安全设计的技巧

        同步工具是使代码线程安全的有用方法,但它们不是万能的。与非线程性能相比,使用太多的锁和其他类型的同步操作实际上会降低应用程序的线程性能。找到安全和性能之间的正确平衡是一门需要经验的艺术。下面提供的技巧可以帮助我们为应用程序选择适当的同步级别。

1、避免完全同步        

        对于任何新的项目,甚至是现有的项目,设计代码和数据结构以避免同步的需求是最好的解决方案。尽管锁和其他同步工具很有用,但他们的确会影响应用程序的性能。如果整体设计造成特定资源之间的频繁争用,线程可能会等待时间更长。

        实现并发的最好方法是减少并发任务之间的交互和相互依赖。如果每个任务都在自己的专用数据集上运行,则不需要使用锁保护该数据。即使在两个任务共享一个通用数据集的情况下,也可以查看分区的方式或为每个任务提供自己的副本。当然,复制数据集也会带来成本,因此必须在作出决定之前权衡两者的成本。

2、了解同步的限制

同步工具只有在应用程序中的所有线程一直使用它们时才有效。如果创建一个互斥体来限制对特定资源的访问,则所有线程都必须在尝试操作资源之前获取相同的互斥体。如果不这样做会破坏互斥体提供的保护。

3、注意代码正确的威胁

在使用锁和内存屏障时,应该仔细考虑它们在代码中的位置。即使看起来很好的锁也能让你陷入虚假的安全感。下面示例显示了看起来没有问题的代码之中的缺陷。基本前提是有一个包含一组不可变对象的可变数组。假设调用数组中第一个对象的方法。可以使用如下代码:


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]


        虽然上面的例子非常简单,但他们确实说明了一个非常重要的问题。说到正确性,我们必须考虑明显问题之外的其他问题。内存管理和设计的其他方面也可能会受到多线程的影响,所以必须事先考虑这些问题。另外,我们应该总是假设编译器在安全方面是一个白痴。只有时刻保持这种意识和警惕性才可以最大的避免潜在的问题并确保代码执行的安全。

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

4、注意死锁和活锁

        当我们试图在一个线程使用多个锁时,就有可能发生死锁。当两个不同的线程持有一个线程需要的锁并尝试获取另一个线程持有的锁时,会发生死锁。结果是每个线程永久阻塞,因为它永远无法获取其他锁。

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

        避免死锁和活锁情况的最好方法是一次只取一个锁。如果一次获取一个以上的锁,应该确保其他线程不会尝试做类似的事情。

 5、正确使用易变变量

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

        为避免使用互斥体,不要使用volatile变量也很重要。一般来说,互斥锁和其他同步机制是保护数据结构完整性的一种更好的方式,而不是易变变量。volatile关键字只确保变量从内存中加载而不是存储在寄存器中。它不能确保代码正确访问该变量。

五、使用原子操作

        非阻塞同步是执行某些类型的操作并避免锁的开销的一种方式。尽管锁是同步两个线程的有效方法,但即使在无争议的情况下,获取锁也是一项相对消耗性能的操作。相比之下,许多原子操作需要一小部分时间来完成,并且可以像锁一样有效。

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

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

1、原子的数学和逻辑操作

<1>.添加(Add)

OSAtomicAdd32、 OSAtomicAdd32Barrier、 OSAtomicAdd64、 OSAtomicAdd64Barrier

将两个整数值一起添加并将结果存储在指定变量的其中之一。

<2>.增量(Increment)

OSAtomicIncrement32、 OSAtomicIncrement32Barrier、 OSAtomicIncrement64 、OSAtomicIncrement64Barrier

将指定的整数值增加1.

<3>.递减(Decrement)

OSAtomicDecrement32、 OSAtomicDecrement32Barrier、 OSAtomicDecrement64、 OSAtomicDecrement64Barrier

将指定的整数值减1.

<4>.逻辑或(Logical OR)

OSAtomicOr32、 OSAtomicOr32Barrier

在指定的32位值和32位掩码之间执行逻辑或。

<5>.逻辑与(Logical AND)

OSAtomicAnd32、 OSAtomicAnd32Barrier

在指定的32位值和32位掩码之间执行逻辑与。

<6>.逻辑异或(Logical XOR)

OSAtomicXor32、OSAtomicXor32Barrier

在指定的32位值和32位掩码之间执行逻辑异或。

<7>.比较和交换

OSAtomicCompareAndSwap32、 OSAtomicCompareAndSwap32Barrier、OSAtomicCompareAndSwap64、 OSAtomicCompareAndSwap64Barrier、 OSAtomicCompareAndSwapPtr、 OSAtomicCompareAndSwapPtrBarrier、 OSAtomicCompareAndSwapInt、 OSAtomicCompareAndSwapIntBarrier、 OSAtomicCompareAndSwapLong、 OSAtomicCompareAndSwapLongBarrier 

将变量与指定的旧值进行比较。 如果两个值相等,则该函数将指定的新值赋给变量; 否则,它什么都不做。 比较和赋值是作为一个原子操作完成的,函数返回一个布尔值,指示实际上是否发生了交换。

<8>.测试并设置

OSAtomicTestAndSet、 OSAtomicTestAndSetBarrier

测试指定变量中的一位,将该位设置为1,并将旧位的值作为布尔值返回。根据字节((char *)地址+(n >> 3))的公式(0x80 >>(n&7))对位进行测试,其中n是位编号,地址是指向变量的指针。该公式将变量有效分解为8位大小的块,并将每个块中的位反向排序。例如,要测试一个32位整数的最低位(位0),实际上应该为位编号指定7;类似地,为了测试最高位(位32),应该指定24位的位数。

<9>.测试和清除

OSAtomicTestAndClear、OSAtomicTestAndClearBarrier

测试指定变量中的一位,将该位设置为0,并将旧位的值作为布尔值返回。根据字节((char *)地址+(n >> 3))的公式(0x80 >>(n&7))对位进行测试,其中n是位编号,地址是指向变量的指针。该公式将变量有效分解为8位大小的块,并将每个块中的位反向排序。例如,要测试一个32位整数的最低位(位0),实际上应该为位编号指定7;类似地,为了测试最高位(位32),应该指定24位的位数。

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

        执行原子操作:


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。


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

六、使用锁

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

1、使用POSIX互斥锁

POSIX互斥锁非常易于在任何应用程序中使用。要创建互斥锁,可以声明和解锁互斥锁,可以使用pthread_mutex_lock和pthread_mutex_unlock函数。下面显示了初始化和是使用POSIX线程互斥锁所需的基本代码。当完成锁定时,只需要调用pthread_mutex_destory释放锁定数据结构即可。

        互斥锁使用


pthread_mutex_t mutex;

void MyInitFunction()

{

    pthread_mutex_init(&mutex, NULL);

}

void MyLockingFunction()

{

    pthread_mutex_lock(&mutex);

    // Do work.

    pthread_mutex_unlock(&mutex);

}


注意:以上代码只是一个简化的互斥锁的示例,旨在显示POSIX线程互斥锁的基本用法。真实的代码中应该检查这些函数返回的错误代码,并对错误做出适当的处理。

2、使用NSLock类

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

        除了标准的锁定行为之外,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];

    }

}


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指令,还必须在代码中启用Objective-C异常处理。如果不想由隐式异常处理程序引起的额外开销,则应考虑使用锁类。

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

4、使用其他的Cocoa锁

        下面各节介绍如何使用其他几种类型的Cocoa锁的过程。

4.1、使用一个NSRecursiveLock(递归锁)对象

        NSRecursiveLock类定义了一个锁,它可以被同一个线程多次获取而不会导致线程死锁。 递归锁会记录它成功获取的次数。 每次成功获取锁必须通过相应的解锁才能进行平衡。 只有当所有的锁定和解锁调用是平衡的,锁定才会被释放,以便其他线程可以获得它。

        也就是说,这种类型的锁常用于递归函数中,以防止递归阻塞线程。 也可以类似地在非递归的情况下使用它来调用语义要求也锁定的函数。 下面是一个简单递归函数的例子,它通过递归来获取锁。 如果您没有为此代码使用NSRecursiveLock对象,则当再次调用该函数时,该线程将死锁。


NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init];

void MyRecursiveFunction(int value)

{

    [theLock lock];

    if (value != 0)

    {

        --value;

        MyRecursiveFunction(value);

    }

    [theLock unlock];

    }

MyRecursiveFunction(5);


注意:因为在所有锁定调用与解锁调用保持平衡之前,递归锁定不会释放,因此应该仔细衡量锁定的使用性能以应对潜在性能影响的决定。 长时间保持锁定会导致其他线程阻塞,直到递归完成。 如果可以重写代码以消除递归或消除使用递归锁定的需要,则可能会获得更好的性能。

4.2、使用一个NSConditionLock(条件锁)对象

一个NSConditionLock对象定义了一个可以使用特定值锁定和解锁的互斥锁。不应该将这种类型的锁与条件混淆。行为与条件有些类似,但实施方式差异很大。

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

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

下面显示了如何使用条件锁来处理生产者 - 消费者问题。想象一下,应用程序包含一个数据队列。生产者线程将数据添加到队列中,消费者线程从队列中提取数据。生产者不需要等待特定条件,但它必须等待锁定可用,以便它可以安全地将数据添加到队列中


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.

}


4.3、使用NSDistributedLock(分布式锁)对象

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

        与其他类型的锁不同,NSDistributedLock不符合NSLocking协议,因此没有锁定方法。锁定方法会阻止线程的执行,并要求系统以预定的速率轮询锁定。 NSDistributedLock提供了一个tryLock方法,并且可以决定是否轮询,而不是在代码中施加处罚。

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

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

七、使用条件

        条件是一种特殊类型的锁,可以使用它来同步操作执行的顺序。 它们以微妙的方式不同于互斥锁。 一个等待条件的线程将一直处于阻塞状态,直到该条件由另一个线程显式指示。

        由于实现操作系统所涉及的细微之处,即使条件锁实际上没有被代码实际发送信号,也允许条件锁返回一个虚假信号。 为了避免由这些虚假信号引起的问题,应该始终将谓词与条件锁一起使用。 谓词是确定线程是否安全进行的更具体的方法。 条件只是保持你的线程休眠,直到谓词可以由信号线程设置。

下面展示如何在代码中使用条件。

1、使用NSCondition类

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

        下面显示了一段代码片段,演示了在NSCondition对象上等待的事件序列。 cocoaCondition变量包含一个NSCondition对象,timeToDoWork变量是一个从另一个线程发送信号之前立即递增的整数。


[cocoaCondition lock];

while (timeToDoWork <= 0)

[cocoaCondition wait];

timeToDoWork--;

// Do real work here.

[cocoaCondition unlock];


        下面代码用来表示Cocoa条件并递增谓词变量的代码。在发出信号之前,应该始终锁定状态。


[cocoaCondition lock];

timeToDoWork++;

[cocoaCondition signal];

[cocoaCondition unlock];


2、使用POSIX条件

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

下面显示了条件和谓词的基本初始化和用法。 初始化条件和互斥锁后,等待线程使用ready_to_go变量作为谓词进入while循环。 只有当谓词被设置并且条件随后发出时,等待线程才会唤醒并开始工作。


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);

}


        信号线程负责设置谓词并将信号发送到条件锁定。 下面显示了实现这种行为的代码。 在这个例子中,该条件在互斥体内部发出信号,以防止在等待条件的线程之间发生竞争条件。


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线程条件函数的基本用法。 实际的代码应该检查这些函数返回的错误代码并适当地处理它们。

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

推荐阅读更多精彩内容