码迷,mamicode.com
首页 > 其他好文 > 详细

同步(Synchronization)

时间:2015-03-18 09:06:00      阅读:238      评论:0      收藏:0      [点我收藏+]

标签:ios开发   同步   线程安全   多线程      

应用中多线程的存在打开了一个潜在的关于执行多线程安全访问资源问题。两个线程修改相同的资源可能会以意想不到的方式相互妨碍。例如,一个线程可能覆盖另一个线程的更改或让应用进入一个未知潜在无效状态。如果你幸运,毁坏的资源也能导致明显的性能问题或相对容易追踪和修复的崩溃。如果你不幸,然而,毁坏的资源可能导致微妙的错误,一直不显现直到很久以后,或者错误可能需要对底层编码设计进行彻底检查。

当涉及到线程安全时,好的设计是最好的保护。避免资源共享和减少线程间的交互使它们不太相互干扰。一个完全抗干扰的设计并不存在,然而。线程必须交互的情况下,你需要使用同步工具以确保当它们相互作用是他们这样做是安全的。

OSX和iOS提供大量的同步工具,延伸到提供互斥访问应用中序列事件的工具。以下章节描述这些工具以及如何在你的代码中使用它们来安全访问程序中的资源。

同步工具

为了防止不同线程意外的更改数据,你可以设计你的应用没有同步问题或者你可以使用同步工具。尽管避免同步问题是完全可取的,这并不总是可能。以下章节描述供你使用的同步工具的基本类别。

原子操作

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

OSX和iOS包含许多操作来执行32位和64位值的基本数学和逻辑操作。这些操作是比较-交换、测试-设置、测试-清除操作的原子版本。关于支持的原子操作的列表,见 /usr/include/libkern/OSAtomic.h 头文件或原子(atomic)手册页。

内存屏障和不稳定变量

为了实现最优性能,编译器常常重新排序汇编级别指令来尽可能保持处理器指令管道完整。作为这种优化的一部分,编译器可能重新排序指令,当它认为这样做不会产生不正确的数据,这些指令会访问主要内存。不幸的是,检测所有依赖内存的操作对编译器来说不可能。如果看似独立的变量相互影响,编译器优化可能以错误的顺序更新这些变量,产生不正确的结果。

内存屏障是一种非阻塞同步工具用来确保内存操作以正确的顺序发生。内存屏障就像一个栅栏,迫使处理器完成任何在栅栏前面的加载和存储操作,然后才允许执行栅栏后面的加载和存储操作。内存屏障通常用于确保线程(但看上去是另一个线程)的内存操作以预期的顺序发生。在这种情况下没有内存屏障可能让其他线程看到貌似不可能的结果。(例如,见维基百科的内存屏障(memorybarriers)条目。)为了使用内存屏障,你只需在你代码适当的位置调用OSMemoryBarrier函数。

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

因为内存屏障和不稳定变量减少编译器可执行的优化,应该谨慎使用它们并只在需要的地方使用以确保正确性。关于使用内存屏障的更多信息,参见OSMemoryBarrier 手册页。

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

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

表4-1 锁类型

描述

Mutex

互斥锁

一个互斥锁(或互斥)作为保护资源的一个屏障。互斥锁是一种信号,一次只授予一个线程访问权限。如果使用互斥锁并且另一个线程试图获取它,该线程阻塞直到互斥锁被原持有人释放。如果多个线程竞争同一互斥锁,一次只允许一个访问。

Recursive lock

递归锁

递归锁是互斥锁的一个变体。递归锁允许一个线程在锁释放前多次获取锁。其他线程阻塞直到锁的所有者多次释放锁,且释放锁的次数与获取锁的次数一样。递归锁主要在递归迭代中使用,但也可以在多个方法需要分别获取锁的情况下使用。

Read-write lock

读写锁

读写锁也称为共享-专有锁。这种类型的锁通常用于大规模操作,如果只是保护频繁读取但偶尔修改的数据结构,可以极大的提高性能。在正常操作期间,多个读取者可以同时访问数据结构。当一个线程想要修改这个结构,但是,它阻塞直到所有读取者释放该锁,此时它获取锁并更新数据结构。而当一个写入线程等待锁,新的读取线程阻塞直到写入线程完成。系统仅支持使用POSIX线程的读写锁。关于如何使用这些锁的更多信息,参见pthread 手册页。

Distributed lock

分布式锁

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

Spin lock

自旋锁

自旋锁多次轮询自己锁条件直到条件变为真。自旋锁最常用于多处理器系统,预期等待锁的时间很小。在这些情况下,相比于阻塞线程,它通常可以更高效的轮询,包括环境切换和线程数据结构的更新。系统不提供任何自旋锁的实现,因为它们的轮询性质,但在特定情况下可以很简单的实现它们。关于内核中实现自旋锁的信息,参见内核编程指南(Kernel Programming Guide)。

Double-checked lock

双重检查锁

双重检查锁试图通过在采用锁之前测试锁来降低锁的开销。因为双重检查锁可能不安全,系统不显式的提供支持,不鼓励使用。

注意:大多数类型的锁包括内存屏障确保加载和存储指令在进入关键部分之前完成。

关于如何使用锁,见使用锁(Using Locks)。

条件

条件是另一种类型的信号,允许线程在某个特定条件是真的时候互相发信号。条件通常用来表示一个资源的可用性或者确保任务按照特定的顺序执行。当一个线程测试条件时,线程阻塞除非条件变为真。它仍然阻塞直到其他线程显式的更改并发送条件。条件和互斥锁的区别是多个线程可以同时访问条件。条件更加像看门人,根据一些特定的标准让不同线程通过门。

你可以使用条件的一个方法是管理等待事件池。当队列中有事件,事件队列使用一个条件变量来发送信号给等待线程。如果事件到达,队列将适当的给条件发送信号。如果线程已经在等待,它会醒来并将事件放到队列中并处理它。如果两个事件在大致相同的时间进入队列,队列将两次发送信号给条件来唤醒两个线程。

系统以几种不同的技术提供条件支持。条件的正确实现需要仔细编码,然而,在代码中使用条件前你应该看看使用条件(Using Conditions)中的例子。

执行选择器程序

Cocoa应用有一个方便的方法以同步的方式来传递消息到一个线程。NSObject 类声明的方法来执行应用活动线程的选择器。这些方法让你的线程异步的交付消息保证目标线程同步执行这些消息。例如,你可以使用执行选择器消息来交付分布式计算的结果到你的应用主线程或指定协调线程。每个请求执行选择器在目标线程的运行循环上排队,然后按照请求收到的顺序处理这些请求。

关于执行选择器程序概要和如何使用它们的更多信息,参加Cocoa执行选择器来源(Cocoa Perform Selector Sources)。

同步成本和性能

同步帮助确保你的代码正确性,但要牺牲性能。使用同步工具会带来延迟甚至无竞争情况。锁和原子操作通常涉及内存屏障和内核级同步的使用,以确保代码正确的被保护。如果锁有竞争,你的线程会阻塞并经历更大的延迟。

表4-2 列出无竞争情况下互斥锁互原子操作的近似成本。这些测量值代表几千个样本的平均时间。与线程创建时间一样,互斥锁获取时间(即便在无竞争情况下)根据处理器负载、计算机速度、可用系统的数量和程序内存的数量有很大的不同。

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

项目

大概成本

说明

Mutex acquisition time

互斥锁获取时间

大约0.2微秒

这是在无竞争情况下锁的获取时间。如果锁被两一个线程持有,获取时间会更长。数据由分析基于Intel、2GHz酷睿双核处理器、1GB的RAM运行在OS X v10.5上的iMac上互斥锁获取时间生成的平均值和中位值来决定的。

Atomic compare-and-swap

原子 比较-交换

大约0.05微秒

这是在无竞争情况下的比较-交换时间。数据由分析基于Intel、2GHz酷睿双核处理器、1GB的RAM运行在OS X v10.5上的iMac上互斥锁获取时间生成的平均值和中位值来决定的。

当设计你的并发任务,正确性始终是最重要的因素,但你还应该考虑性能因素。在多线程中能正确的执行代码,但比在单线程中运行相同的代码慢,这并不是一种进步。

如果你在改造现有的单线程应用,你应该采用一组基线作为关键任务性能的测量值。在添加额外线程,你必须为相同的任务采取新的测量值,并比较多线程与单线程情况下的性能。如果采用你的代码,线程并没有提高性能,你可能要考虑具体的实现或整个的使用线程。

关于性能和工具来收集度量标准的更多信息,参见性能概述(Performance Overview)。关于锁和原子操作的成本的信息,参见线程成本(Thread Costs)。

线程安全和信号

当到线程应用时,没什么比处理信号问题引起更多的恐惧和混乱。信号时一种低级别的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 方法需要很长时间来执行,这将导致你的代码长时间持有锁,这将出现性能瓶颈。

代码的问题不是临界区缺乏定义,而是不理解实际问题。真正的问题是内存管理问题,锁只被其他存在的线程触发。因为锁可以被其他线程释放,一个更好的解决方案是在释放锁之前retainanObject  。对象被释放并且不引起一个潜在的性能损失,这个解决方案解决了这样的实际问题。


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

当心死锁和活动锁

任何时间一个线程试图同时获得多个锁,有发生死锁的可能性。当有两个不同线程,一个需要锁并尝试获取到另外一个线程持有的锁时,会发生死锁。其结果是每个线程永久阻塞因为它不能获取其他的锁。

活动锁类似死锁,当两个线程竞争相同的一组资源时发生。在活动锁的情况下,一个线程为了获取第二个锁而放弃它的第一个锁。一旦它获取到第二个锁,它可以返回并尝试再次获取第一个锁。因为它花费了所有时间来释放一个锁并试图获取其他锁而非做任何实际工作,所以它会锁起来。

避免死锁和活动锁的情况最好的方法就是一个时间只采用一个锁。如果你必须在时间内获取多个锁,你应该确保其他线程不尝试类似的事情。

正确使用不稳定变量

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

同样重要的是,你不要为了避免使用互斥锁而不用不稳定变量。一般来说,互斥锁和其他同步机制比不稳定变量更好的保护数据结构的完整性。Volatile关键字只确保从内存加载一个变量而不是存储在寄存器中。它不保证代码访问变量的正确性。

使用原子操作

非阻塞同步是执行某些类型的操作并避免浪费锁的一种方式。虽然锁是同步两个线程的一种有效方式,获取一个锁是一个相对昂贵的操作,即使在无竞争的情况下。相比之下,许多原子操作只花费一小部分时间来完成并且与锁一样有效。

原子操作让你在32位或64位值上执行简单的数学和逻辑操作。这些操作依赖于特殊指令(和一个可选的内存屏障)来保证在再次访问内存前完成给定的操作。在多线程的情况下,你应该总是使用原子操作,包含内存屏障来保证线程间内存同步正确。

表4-3 列出了可用原子数学和逻辑操作以及对应的函数名称。这些函数都定义在/usr/include/libkern/OSAtomic.h 头文件中,你可以找到完整的语法。这些函数的64位版本只能用在64位进程中。

表4-3 原子数学和逻辑操作

操作

函数名

描述

Add

加法

OSAtomicAdd32

OSAtomicAdd32Barrier

OSAtomicAdd64

OSAtomicAdd64Barrier

两个数值相加并将结果存储在一个指定变量中。

Increment

增量

OSAtomicIncrement32

OSAtomicIncrement32Barrier

OSAtomicIncrement64

OSAtomicIncrement64Barrier

指定整数值增量为1

Decrement

减量

OSAtomicDecrement32

OSAtomicDecrement32Barrier

OSAtomicDecrement64

OSAtomicDecrement64Barrier

指定整数值减量为1

Logical OR

逻辑或

OSAtomicOr32

OSAtomicOr32Barrier

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

Logical AND

逻辑与

OSAtomicAnd32

OSAtomicAnd32Barrier

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

Logical XOR

逻辑异或

OSAtomicXor32

OSAtomicXor32Barrier

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

Compare and swap

比较并交换

OSAtomicCompareAndSwap32

OSAtomicCompareAndSwap32Barrier

OSAtomicCompareAndSwap64

OSAtomicCompareAndSwap64Barrier

OSAtomicCompareAndSwapPtr

OSAtomicCompareAndSwapPtrBarrier

OSAtomicCompareAndSwapInt

OSAtomicCompareAndSwapIntBarrier

OSAtomicCompareAndSwapLong

OSAtomicCompareAndSwapLongBarrier

将一个变量与指定的旧值比较。如果这两个值相等,这个函数将指定新值赋给变量,否则,它什么都不做。比较和分配是一个原子操作,函数返回一个布尔值表明交换是否实际发生。

Test and set

测试并设置

OSAtomicTestAndSet

OSAtomicTestAndSetBarrier

测试指定变量的bit,设置bit为1,并返回旧bit的值作为一个布尔值。Bit根据 ((char*)address + (n >> 3)) 公式的(0x80 >> (n & 7)) 字节来测试,公式中n是一个bit数,address是一个指针变量。这个公式有效的将变量分解为8bit大小的数据块并逆序排列每个数据块中的bit。例如,测试32bit整数最低顺序bit(bit 0),你实际上指定bit数为7;同样,测试最高位顺序bit(bit 32),你为bit数指定为24。

Test and clear

测试并清除

OSAtomicTestAndClear

OSAtomicTestAndClearBarrier

测试指定变量的bit,设置bit为0,并返回旧bit的值作为一个布尔值。Bit根据 ((char*)address + (n >> 3)) 公式的(0x80 >> (n & 7)) 字节来测试,公式中n是一个bit数,address是一个指针变量。这个公式有效的将变量分解为8bit大小的数据块并逆序排列每个数据块中的bit。例如,测试32bit整数最低顺序bit(bit 0),你实际上指定bit数为7;同样,测试最高位顺序bit(bit 32),你为bit数指定为24。

大多数原子函数行为应该相对简单并符合你的期望。然而,清单4-1中展示了原子的测试-设置和比较-交换操作的行为,这些更加复杂些。前三个调用OSAtomicTestAndSet 函数演示bit操作公式如何运用到一个整数值上,结果可能不同于你的预期。最后两个调用展示了OSAtomicCompareAndSwap32 函数的行为。在所有情况下,这些函数在没有其他线程操作该值的情况下被调用。

清单4-1 执行原子操作


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

使用锁

锁是线程编程的一个基本同步工具。锁让你可以很容易的保护大部分代码,这样你就可以保证代码的正确性。OS X和iOS为所有运用类型和基础框架提供基本的互斥锁,定义了一些额外变量的互斥锁的特殊情况。以下章节向你展示如何使用这些类型的锁。

使用POSIX互斥锁

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);
    // Do work.
    pthread_mutex_unlock(&mutex);
}

注意:前面的代码是为了展示POSIX线程互斥锁函数的基本用法的一个简化例子。你自己的代码应该检查这些函数返回的错误代码并适当的处理它们。

使用NSLock类

NSLock对象实现Cocoa应用中基本互斥锁。所有锁(包括NSLock)的接口实际上是由 NSLocking协议来定义的,它定义了lockunlock 方法。类似互斥锁,你使用这些方法来获取和释放锁。

除了标准锁定行为,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指令

在Objective-C代码上创建互斥锁的一个方便的方式是@synchronized指令。@synchronized指令做其他互斥锁做的事,防止不同的线程同时获取相同的锁。然而,在这种情况下,你不必直接创建互斥锁或锁对象。相反,你只需使用任何Objective-C对象作为锁记号,如示例所示:


- (void)myMethod:(id)anObj
{
    @synchronized(anObj)
    {
        // Everything between the braces is protected by the @synchronized directive.
    }
}

传递到@synchronized 指令的对象是一个独特的标识符用来区分protectblock。如果你在两个不同线程上执行前面的方法,传递不同的对象作为每个线程上的anObj 参数,每个线程使用自己的锁并继续处理而不被其他线程阻塞。然而,如果在这两种情况下,你传递相同对象,其中一个线程获取锁,其他的线程将阻塞直到第一个线程完成关键部分。

作为防范措施,@synchronized  block隐式的将一个异常处理程序添加到受保护的代码。这个处理程序自动释放互斥锁时会抛出异常。这意味着未来使用@synchronized指令,你必须启用Objective-C代码中的异常处理。如果你不希望隐式异常处理程序引起额外的开销,你应该考虑使用锁类。

关于@synchronized指令的更多信息,参见Objective-C编程语言(TheObjective-C Programming Language)。

使用其他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 对象定义了一个互斥锁,该锁可以锁定或解锁为特定值。你不应该将这种类型的锁与条件(见条件( Conditions))混淆。该行为有点类似条件,但实现是完全不同的。

通常,当线程需要以特定顺序执行任务时,你可以使用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.
}

使用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--;
 
// Do real work here.
 
[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()
{
    // 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);
}

发送信号线程负责设置断言并发送条件锁信号。清单4-6展示实现该行为的代码。在这个例子中,条件在互斥锁内部发送信号,以防止等待条件的线程间发生竞争条件。

清单4-6 发送条件锁信号


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线程条件函数的基本用法。你自己的代码应该返回错误代码。

 

官方原文地址:

https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Multithreading/ThreadSafety/ThreadSafety.html#//apple_ref/doc/uid/10000057i-CH8-SW1http://www.apple.com/legal/policies/ideas.html -//apple_ref/doc/uid/_blank

同步(Synchronization)

标签:ios开发   同步   线程安全   多线程      

原文地址:http://blog.csdn.net/iosswift/article/details/44395101

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!