iOS 进阶知识集Temp

iOS - 关于线程同步

2016-04-03  本文已影响633人  Mitchell

引用自多线程编程指南
应用程序里面多个线程的存在引发了多个执行线程安全访问资源的潜在问题。两个线程同时修改同一资源有可能以意想不到的方式互相干扰。比如,一个线程可能覆盖其他线程改动的地方,或让应用程序进入一个未知的潜在无效状态。如果你幸运的话,受损的资源可能会导致明显的性能问题或崩溃,这样比较容易跟踪并修复它。然而如果你不走运,资源受损可能导致微妙的错误,这些错误不会立即显现出来,而是很久之后才出现,或者导致其他可能需要一个底层的编码来显著修复的错误。
但涉及到线程安全时,一个好的设计是最好的保护。避免共享资源,并尽量减少线程间的相互作用,这样可以让它们减少互相的干扰。但是一个完全无干扰的设计是不可能的。在线程必须交互的情况下,你需要使用同步工具,来确保当它们交互的时候是安全的。

一、同步工具


二、同步的成本和性能


三、线程安全和信号量

当涉及到多线程应用程序时,没有什么比处理信号量更令人恐惧和困惑的了。信号量是底层 BSD 机制,它可以用来传递信息给进程或以某种方式操纵它。一些应用程序使用信号量来检测特定事件,比如子进程的消亡。系统使用信号量来终止失控进程,和作为其他类型的通信消息。
使用信号量的问题并不是你要做什么,而是当你程序是多线程的时候它们的行为。在当线程应用程序里面,所有的信号量处理都在主线程进行。在多线程应用程序里面,信号量被传递到恰好运行的线程,而不依赖于特定的硬件错误(比如非法指令)。如果多个线程同时运行,信号量被传递到任何一个系统挑选的线程。换而言之,信号量可以传递给你应用的任何线程。
在你应用程序里面实现信号量处理的第一条规则是避免假设任一线程处理信号量。如果一个指定的线程想要处理给定的信号,你需要通过某些方法来通知该线程信号何时到达。你不能只是假设该线程的一个信号处理例程的安装会导致信号被传递到同一线程里面。
关于更多信号量的信息和信号量处理例程的安装信息,参见 signal 和 sigaction主页。


四、线程安全设计的技巧

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

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

以上例子说明了想操作可变数组中的一个对象去做一些事情,当我拿到这个对象之后如果有其他线程进入到锁中删除了这个对象,那么就会出现问题,那么可能会像下面这样修改代码:

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

这样做也是不好的,因为如果 dosomething 的执行时间很长的话就会产生性能瓶颈。所以最好的办法是这样:

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

五、使用原子操作

非阻塞同步的方式是用来执行某些类型的操作而避免扩展使用锁。尽管锁是同步两个线程的很好方式,获取一个锁是一个很昂贵的操作,即使在无竞争的状态下。相比,许多原子操作花费很少的时间来完成操作也可以达到和锁一样的效果。
原子操作可以让你在 32 位或 64 位的处理器上面执行简单的数学和逻辑的运算操作。这些操作依赖于特定的硬件设施(和可选的内存屏障)来保证给定的操作在影响内存再次访问的时候已经完成。在多线程情况下,你应该总是使用原子操作,它和内存屏障组合使用来保证多个线程间正确的同步内存。


原子操作.png

六、使用锁

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

pthread_mutex_t mutex;
void MyInitFunction()
{
pthread_mutex_init(&mutex, NULL);
}
void MyLockingFunction()
{
pthread_mutex_lock(&mutex);
// Do work.
pthread_mutex_unlock(&mutex);
}

注意:上面的代码只是简单的显式了使用一个 POSIX 线程互斥锁的步骤。你自己的代码应该检查这些函数返回的错误码,并适当的处理它们。

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];
}
}
- (void)myMethod:(id)anObj
{
@synchronized(anObj)
{
// Everything between the braces is protected by the @synchronized directive.
}
}

创建给 @synchronized 指令的对象是一个用来区别保护块的唯一标示符。如果你在两个不同的线程里面执行上述方法,每次在一个线程传递了一个不同的对象给anObj 参数,那么每次都将会拥有它的锁,并持续处理,中间不被其他线程阻塞。然而,如果你传递的是同一个对象,那么多个线程中的一个线程会首先获得该锁,而其他线程将会被阻塞直到第一个线程完成它的临界区。
作为一种预防措施,@synchronized 块隐式的添加一个异常处理例程来保护代码。
该处理例程会在异常抛出的时候自动的释放互斥锁。这意味着为了使用@synchronized 指令,你必须在你的代码中启用异常处理。如果你不想让隐式的异常处理例程带来额外的开销,你应该考虑使用锁的类。

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 的锁住和解锁方法可以任意组合使用。比如,你可以使用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。在随后的迭代中,生产者线程可以把到达的数据添加到队列,无论队列是否为空或依然有数据。唯一让它进入阻塞的情况是当一个消费者线程充队列取出数据的时候。
因为消费者线程必须要有数据来处理,它会使用一个特定的条件来等待队列。当生产者把数据放入队列时,消费者线程被唤醒并获取它的锁。它可以从队列中取出数据,并更新队列的状态。下列代码显示了消费者线程处理循环的基本结构。

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 对象不会被释放除非它的拥有者显式的释放它。如果你的程序在用户一个分布锁的时候崩溃了,其他客户端无法访问该受保护的资源。在这种情况下,你可以使用 breadLock 方法来打破现存的锁以便你可以获取它。但是通常应该避免打破锁,除非你确定拥有进程已经死亡并不可能再释放该锁。
和其他类型的锁一样,当你使用 NSDistributedLock 对象时,你可以通过调用unlock 方法来释放它。


七、使用条件

条件是一个特殊类型的锁,你可以使用它来同步操作必须处理的顺序。它们和互斥锁有微妙的不同。一个线程等待条件会一直处于阻塞状态直到条件获得其他线程显式发出的信号。
由于微妙之处包含在操作系统实现上,条件锁被允许返回伪成功,即使实际上它们并没有被你的代码告知。为了避免这些伪信号操作的问题,你应该总是在你的条件锁里面使用一个断言。该断言是一个更好的方法来确定是否安全让你的线程处理。条件简单的让你的线程保持休眠直到断言被发送信号的线程设置了。

[cocoaCondition lock];
while (timeToDoWork <= 0)
[cocoaCondition wait];
timeToDoWork--;
// Do real work here.
[cocoaCondition unlock];

以下代码显示了用于给 Cocoa 条件发送信号的代码,并递增他断言变量。你应该在给它发送信号前锁住条件。

[cocoaCondition lock];
timeToDoWork++;
[cocoaCondition signal];
[cocoaCondition unlock];
- 使用 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()
{
//锁住互斥锁
pthread_mutex_lock(&mutex);
//如果断言已经被设置,那么就绕过 while 循环,如果没有,线程会一直休眠知道断言被设置。
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()
{
//在这里,应该有其他线程的工作要做。
pthread_mutex_lock(&mutex);
ready_to_go = true;
//通知其他线程开始工作
pthread_cond_signal(&condition);
pthread_mutex_unlock(&mutex);
}

注意:上述代码是显示使用 POSIX 线程条件函数的简单例子。你自己的代码应该检测这些函数返回错误码并恰当的处理它们。


八、总结

上一篇 下一篇

猜你喜欢

热点阅读