多线程编程指南读书笔记——线程同步

应用程序里面多个线程的存在引发了多个执行线程安全访问资源的潜在问题。两 个线程同时修改同一资源有可能以意想不到的方式互相干扰。
但涉及到线程安全时,一个好的设计是最好的保护。避免共享资源,并尽量减少 线程间的相互作用,这样可以让它们减少互相的干扰。但是一个完全无干扰的设计是 不可能的。在线程必须交互的情况下,你需要使用同步工具,来确保当它们交互的时 候是安全的。

  • 同步工具
    • 1 原子操作
      原子操作是同步的一个简单的形式,它处理简单的数据类型。原子操作的优势是 它们不妨碍竞争的线程。对于简单的操作,比如递增一个计数器,原子操作比使用锁 具有更高的性能优势。
    • 2 内存屏障和 Volatile 变量
      为了达到最佳性能,编译器通常会对汇编基本的指令进行重新排序来尽可能保持 处理器的指令流水线。作为优化的一部分,编译器有可能对访问主内存的指令,如果 它认为这有可能产生不正确的数据时,将会对指令进行重新排序。不幸的是,靠编译 器检测到所有可能内存依赖的操作几乎总是不太可能的。如果看似独立的变量实际上 是相互影响,那么编译器优化有可能把这些变量更新位错误的顺序,导致潜在不不正 确结果。
      内存屏障(memory barrier)是一个使用来确保内存操作按照正确的顺序工作的 非阻塞的同步工具。内存屏障的作用就像一个栅栏,迫使处理器来完成位于障碍前面 的任何加载和存储操作,才允许它执行位于屏障之后的加载和存储操作。内存屏障同 样使用来确保一个线程(但对另外一个线程可见)的内存操作总是按照预定的顺序完 成。如果在这些地方缺少内存屏障有可能让其他线程看到看似不可能的结果(比如, 内存屏障的维基百科条目)。为了使用一个内存屏障,你只要在你代码里面需要的地 方简单的调用 OSMemoryBarrier 函数。
      Volatile 变量适用于独立变量的另一个内存限制类型。编译器优化代码通过加 载这些变量的值进入寄存器。对于本地变量,这通常不会有什么问题。但是如果一个 变量对另外一个线程可见,那么这种优化可能会阻止其他线程发现变量的任何变化。 在变量之前加上关键字 volatile 可以强制编译器每次使用变量的时候都从内存里面 加载。如果一个变量的值随时可能给编译器无法检测的外部源更改,那么你可以把该 变量声明为 volatile 变量。
      因为内存屏障和 volatile 变量降低了编译器可执行的优化,因此你应该谨慎使 用它们,只在有需要的地方时候,以确保正确性。关于更多使用内存屏障的信息,参 阅 OSMemoryBarrier 主页。
    • 3锁
      锁是最常用的同步工具。你可以是使用锁来保护临界区(critical section),这 些代码段在同一个时间只能允许被一个线程访问。比如,一个临界区可能会操作一个 特定的数据结构,或使用了每次只能一个客户端访问的资源。

锁类型

  • 使用锁
  • 1使用POSIX互斥锁
    POSIX 互斥锁在很多程序里面很容易使用。为了新建一个互斥锁,你声明并初始 化一个 pthread_mutex_t 的结构。为了锁住和解锁一个互斥锁,你可以使用 pthread_mutex_lock 和 pthread_mutex_unlock 函数。
    当你用完一个锁之后,只要简单的调用
pthread_mutex_destroy 来释放该锁的数据结构。 
pthread_mutex_t mutex;
void MyInitFunction()
{ pthread_mutex_init(&mutex, NULL);} 
void MyLockingFunction(){ 

pthread_mutex_lock(&mutex);// Do work. 
pthread_mutex_unlock(&mutex);
} 
  • 2使用NSLock类
    NSLock
  • 3使用@synchronized指令
    @synchronized 指令是在 Objective-C 代码中创建一个互斥锁非常方便的方法。 @synchronized 指令做和其他互斥锁一样的工作(它防止不同的线程在同一时间获取 同一个锁)。然而在这种情况下,你不需要直接创建一个互斥锁或锁对象。相反,你 只需要简单的使用 Objective-C 对象作为锁的令牌,如下面例子所示:
    @synchronized

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

  • 使用 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 的锁住和解锁方法可以任意组合使用。比如,你可以使用 unlockWithCondition:和 lock 消息,或使用 lockWhenCondition:和 unlock 消息。 当然,后面的组合可以解锁一个锁但是可能没有释放任何等待某特定条件值的线程。
    下面的例子显示了生产者-消费者问题如何使用条件锁来处理。想象一个应用程 序包含一个数据的队列。一个生产者线程把数据添加到队列,而消费者线程从队列中 取出数据。生产者不需要等待特定的条件,但是它必须等待锁可用以便它可以安全的 把数据添加到队列。
id condLock = [[NSConditionLock alloc]initWithCondition:NO_DATA]; 
while(true) {
[condLock lock];
[condLock unlockWithCondition:HAS_DATA];
}
  • 使用 NSDistributedLock 对象(分布锁)
    NSDistributedLock 类可以被多台主机上的多个应用程序使用来限制对某些共享
    资源的访问,比如一个文件。锁本身是一个高效的互斥锁,它使用文件系统项目来实 现,比如一个文件或目录。对于一个可用的 NSDistributedLock 对象,锁必须由所有 使用它的程序写入。这通常意味着把它放在文件系统,该文件系统可以被所有运行在 计算机上面的应用程序访问。
    不像其他类型的锁,NSDistributedLock 并没有实现 NSLocking 协议,所有它没 有 lock 方法。一个 lock 方法将会阻塞线程的执行,并要求系统以预定的速度轮询锁。 以其在你的代码中实现这种约束,NSDistributedLock 供了一个 tryLock 方法,并 让你决定是否轮询。
    因为它使用文件系统来实现,一个 NSDistributedLock 对象不会被释放除非它的 拥有者显式的释放它。如果你的程序在用户一个分布锁的时候崩溃了,其他客户端简 无法访问该受保护的资源。在这种情况下,你可以使用 breadLock 方法来打破现存的 锁以便你可以获取它。但是通常应该避免打破锁,除非你确定拥有进程已经死亡并不 可能再释放该锁。
    和其他类型的锁一样,当你使用 NSDistributedLock 对象时,你可以通过调用 unlock 方法来释放它。

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

  • 1 使用NSCondition类
    NSCondition 类 供了和 POSIX 条件相同的语义,但是它把锁和条件数据结构封 装在一个单一对象里面。结果是一个你可以像互斥锁那样使用的对象,然后等待特定 条件。
    下面显示了一个代码片段,它展示了为等待一个 NSCondition 对象的事件序 列。cocaoCondition 变量包含了一个 NSCondition 对象,而 timeToDoWork 变量是一 个整形,它在其他线程里面发送条件信号时立即递增。

[cocoaCondition lock];
while (timeToDoWork <= 0){
[cocoaCondition wait];
timeToDoWork--;
// Do real work here.
}
[cocoaCondition unlock];
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值