IOS多线程编程指南二之同步

一、同步概要


The presence of multiple threads in an application opens up potential issues regarding safe access to resources from multiple threads of execution. Two threads modifying the same resource might interfere with each other in unintended ways.

二、同步工具


2.1 Atomic Operations 线程安全修饰符

Atomic operations are a simple form of synchronization that work on simple data types. The advantage of atomic operations is that they do not block competing threads. For simple operations, such as incrementing a counter variable, this can lead to much better performance than taking a lock.
atomic 是最简单的同步方式适用于简单类型的数据对象,与lock不同的是它是不阻塞相互竞争的线程的。所以它具备更好的性能。

For a list of supported atomic operations, see the/usr/include/libkern/OSAtomic.hheader file or see theatomicman page.

具体的实现机制可以看/usr/include/libkern/OSAtomic.h头文件或者使用man命令。

2.2 Memory Barriers and Volatile Variables(内存栅栏与Volatile变量)

In order to achieve optimal performance, compilers often reorder assembly-level instructions to keep the instruction pipeline for the processor as full as possible. As part of this optimization, the compiler may reorder instructions that access main memory when it thinks doing so would not generate incorrect data. Unfortunately, it is not always possible for the compiler to detect all memory-dependent operations. If seemingly separate variables actually influence each other, the compiler optimizations could update those variables in the wrong order, generating potentially incorrect results.
编译器在编译的时候会对汇编指令重新排序,来尽可能的和处理器指令流水线保持一致,来提高应用性能。甚至会调整主内存的访问顺序(编译器不能完全保障相关的内存问题),这将会导致这些变量以错误的顺序更新,产生错误。

2.2.1 memory barrier 内存栅栏

A memory barrier is a type of nonblocking synchronization tool used to ensure that memory operations occur in the correct order.A memory barrier acts like a fence, forcing the processor to complete any load and store operations positioned in front of the barrier before it is allowed to perform load and store operations positioned after the barrier. Memory barriers are typically used to ensure that memory operations by one thread (but visible to another) always occur in an expected order.
内存栅栏是一种非阻塞同步工具,用于确保内存操作以正确的顺序进行。
内存栅栏就像一个栅栏一样强制处理器完成栅栏前所有的加载存储工作后执行栅栏后相关的加载存储函数操作。
使用OSMemoryBarrier函数即可使用内存栅栏功能。

2.2.1 Volatile variables

Volatile variables apply another type of memory constraint to individual variables. The compiler often optimizes code by loading the values for variables into registers. For local variables, this is usually not a problem. If the variable is visible from another thread however, such an optimization might prevent the other thread from noticing any changes to it. Applying the volatile keyword to a variable forces the compiler to load that variable from memory each time it is used. You might declare a variable as volatile if its value could be changed at any time by an external source that the compiler may not be able to detect 。
编译器通常会把全局变量优化到一个注册器中(局部变量不存在这样的问题),这可能会导致要用到这个全局变量的线程无法获取这个变量的变化状态。Volatile 修饰符可以强制编译器将这个变量放入内存中而不是注册器中。

Because both memory barriers and volatile variables decrease the number of optimizations the compiler can perform, they should be used sparingly and only where needed to ensure correctness.
因为这两种方式一定程度上阻止了编译器进相关的优化,所以尽可能的在必要的地方使用。

2.3 locks 线程锁

Locks are one of the most commonly used synchronization tools. You can use locks to protect a critical section of your code, which is a segment of code that only one thread at a time is allowed access.
线程锁是最常用的一种同步工具。你可以用它来保护一段可能出现竞争危险的代码块,来确保同一时间只有一个线程可以访问它。

线程锁类型

类型 描述
Mutex 相关的资源只允许一个线程访问,在当前的线程未释放对它的访问权时,其他想访问该资源的线程将会被阻塞直到当前线程释放
Recursive lock 递归锁是互斥锁的变体。 递归锁定允许单个线程在释放之前多次获取锁定。 其他线程会一直处于阻塞状态,直到锁的所有者释放该锁的次数与获取它的次数相同。 递归锁主要在递归迭代期间使用,但也可能在多个方法需要分别获取锁的情况下使用。
Read-write lock 读写锁也被称为共享排他锁。 这种类型的锁通常用于较大规模的操作(频繁的数据读取和偶尔的数据修改),可以获得很好的性能。 在正常操作期间,多个线程可以同时读取共享数据。 然而,当一个线程想要写入结构时,它会阻塞,直到所有读者释放锁,在此时它才会获取锁来修改数据。 当这个写入线程正在等待锁时,新的读取器线程将阻塞,直到写入线程完成。 系统仅支持使用POSIX线程的Read-write lock。
Distributed lock 分布式锁在进程级提供互斥访问。 与真正的互斥锁不同,分布式锁不会阻止进程或阻止进程运行。 它只是报告锁何时忙,并让进程决定如何继续。
Spin lock 自旋锁反复轮询其锁定条件,直到该条件成立。 自旋锁最常用于预计等待锁定时间较短的多处理器系统。 在这些情况下,轮询通常比阻塞线程更有效,后者涉及上下文切换和线程数据结构的更新。 由于轮询性质,系统不提供自旋锁的任何实现,但是您可以在特定情况下轻松实现它们。 有关在内核中实现自旋锁的信息,请参阅内核编程指南。
Double-checked lock 双重检查锁试图通过在锁定之前测试锁定标准来降低获取锁的开销。 由于双重检查的锁可能不安全,系统不提供对它们的明确支持,因此不鼓励使用它们。

Note: Most types of locks also incorporate a memory barrier to ensure that any preceding load and store instructions are completed before entering the critical section.

通常线程锁会配合内存栅栏一起使用

2.4Conditions

The difference between a condition and a mutex lock is that multiple threads may be permitted access to the condition at the same time. The condition is more of a gatekeeper that lets different threads through the gate depending on some specified criteria.

One way you might use a condition is to manage a pool of pending events. The event queue would use a condition variable to signal waiting threads when there were events in the queue. If one event arrives, the queue would signal the condition appropriately. If a thread were already waiting, it would be woken up whereupon it would pull the event from the queue and process it. If two events came in to the queue at roughly the same time, the queue would signal the condition twice to wake up two threads.

Conditions相比互斥锁更像是一个gatekeeper ,多个线程可以同时访问Conditions,但是由Conditions特定的规则来决定哪个线程可以运行。

2.5 Perform Selector Routines

Each request to perform a selector is queued on the target thread’s run loop and the requests are then processed sequentially in the order in which they were received.

2.6 Synchronization Costs and Performance

同步将会带来一定性能上的损耗,这里官网指出了一个列表,测试环境是在不考虑竞争的情况,光创建互锁和atomic引起的时间消耗。


跟别说互斥的情况下(会让线程等待),所消耗的性能时间了。所以在开发多线程的时候需谨慎设置所需。

三、Tips for Thread-Safe Designs (线程安全指南)


Synchronization tools are a useful way to make your code thread-safe, but they are not a panacea. Used too much, locks and other types of synchronization primitives can actually decrease your application’s threaded performance compared to its non-threaded performance.

错误的同步设置,将会使多线程应用的性能不如单线程应用。

3.1 Avoid Synchronization Altogether (完全避免同步)

The best way to implement concurrency is to reduce the interactions and inter-dependencies between your concurrent tasks. If each task operates on its own private data set, it does not need to protect that data using locks. Even in situations where two tasks do share a common data set, you can look at ways of partitioning that set or providing each task with its own copy. Of course, copying data sets has its costs too, so you have to weigh those costs against the costs of synchronization before making your decision.

最好的方式是尽量减少并发任务之间的交互。

1.即便是两个任务享有一段共有的的数据,也可以通过分开数据或者copy这段数据分别给两个任务。

理解具体同步工具的特性

3.2 Be Aware of Threats to Code Correctness 预知代码正确性带来的风险

When using locks and memory barriers, you should always give careful thought to their placement in your code. Even locks that seem well placed can actually lull you into a false sense of security.

注意lock和栅栏的位置,它可能会使你产生错误的安全感。

Memory management and other aspects of your design may also be affected by the presence of multiple threads, so you have to think about those problems up front. In addition, you should always assume that the compiler will do the worst possible thing when it comes to safety. This kind of awareness and vigilance should help you avoid potential problems and ensure that your code behaves correctly.

3.3 Watch Out for Deadlocks and Livelocks注意死锁和活锁

Any time a thread tries to take more than one lock at the same time, there is a potential for a deadlock to occur. A deadlock occurs when two different threads hold a lock that the other one needs and then try to acquire the lock held by the other thread. The result is that each thread blocks permanently because it can never acquire the other lock.
死锁是由于两个线程皆拥有锁导致两个线程都block的现象。
A livelock is similar to a deadlock and occurs when two threads compete for the same set of resources. In a livelock situation, a thread gives up its first lock in an attempt to acquire its second lock. Once it acquires the second lock, it goes back and tries to acquire the first lock again. It locks up because it spends all its time releasing one lock and trying to acquire the other lock rather than doing any real work.
活锁

避免方法:
The best way to avoid both deadlock and livelock situations is to take only one lock at a time. If you must acquire more than one lock at a time, you should make sure that other threads do not try to do something similar.

3.4 Use Volatile Variables Correctly

If the mutex alone is enough to protect the variable, omit the volatile keyword.

It is also important that you do not use volatile variables in an attempt to avoid the use of mutexes. In general, mutexes and other synchronization mechanisms are a better way to protect the integrity of your data structures than volatile variables. Thevolatilekeyword only ensures that a variable is loaded from memory rather than stored in a register. It does not ensure that the variable is accessed correctly by your code.

四、线程同步实践


4.1 Using Atomic Operations

Nonblocking synchronization is a way to perform some types of operations and avoid the expense of locks. Although locks are an effective way to synchronize two threads, acquiring a lock is a relatively expensive operation, even in the uncontested case. By contrast, many atomic operations take a fraction of the time to complete and can be just as effective as a lock.
不阻塞线程是一种避免使用lock这种消耗新能的同步方式。

These operations rely on special hardware instructions (and an optional memory barrier) to ensure that the given operation completes before the affected memory is accessed again. In the multithreaded case, you should always use the atomic operations that incorporate a memory barrier to ensure that the memory is synchronized correctly between threads.

Atomic这些操作依赖于特殊的硬件指令,以确保在受影响的内存再次访问之前完成给定的操作。 在多线程的情况下,您应始终使用包含内存屏障的原子操作来确保内存在线程之间正确同步。

4.2 Using the NSLock Class

In addition to the standard locking behavior, theNSLockclass adds the tryLock and lockBeforeDate: methods. The tryLock method attempts to acquire the lock but does not block if the lock is unavailable; instead, the method simply returnsNO. The lockBeforeDate: method attempts to acquire the lock but unblocks the thread (and returnsNO) if the lock is not acquired within the specified time limit.

4.3 Using the @synchronized Directive

This means that in order to use the @synchronized directive, you must also enable Objective-C exception handling in your code. If you do not want the additional overhead caused by the implicit exception handler, you should consider using the lock classes。

4.4 Using an NSRecursiveLock Object

The NSRecursiveLock class defines a lock that can be acquired multiple times by the same thread without causing the thread to deadlock. A recursive lock keeps track of how many times it was successfully acquired. Each successful acquisition of the lock must be balanced by a corresponding call to unlock the lock. Only when all of the lock and unlock calls are balanced is the lock actually released so that other threads can acquire it.
递归锁,加锁要和解锁的次数一样。

4.5 Using an NSConditionLock Object

Typically, you use an NSConditionLock object when threads need to perform tasks in a specific order, such as when one thread produces data that another consumes.

4.6 Using an NSDistributedLock Object

The NSDistributedLock class can be used by multiple applications on multiple hosts to restrict access to some shared resource, such as a file.

4.7 Using Conditions

Conditions are a special type of lock that you can use to synchronize the order in which operations must proceed. They differ from mutex locks in a subtle way. A thread waiting on a condition remains blocked until that condition is signaled explicitly by another thread.

Using the NSCondition Class

The NSCondition class provides the same semantics as POSIX conditions, but wraps both the required lock and condition data structures in a single object. The result is an object that you can lock like a mutex and then wait on like a condition.

参考文献:


iOS中保证线程安全的几种方式与性能对比

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

推荐阅读更多精彩内容

  • rljs by sennchi Timeline of History Part One The Cognitiv...
    sennchi阅读 7,279评论 0 10
  • 凌空飞度那条河,那座山 却灵光乍现的断弦 牵引的羁绊从中折断 那个人开始迷惘地追赶 草丛田野中传来懊悔咒骂 青丝的...
    楚舞阅读 295评论 0 0
  • 今天晚上自觉加班了一个小时,晚上回家等车很顺利,下班就有种身轻如燕的感觉。下班高峰已经错过了,虽然没有座位,但车上...
    灰姑娘的梧桐树阅读 307评论 6 4
  • 我是日本人。我十八岁在日本高中毕业之后赴中国报语言班学了八个月的汉语。之后,自己找大学参加入学考试,入学后学了从小...
    小谷爱美阅读 1,086评论 11 5