Concurrency is the entry point for the most complicated and bizarre bugs a programmer will ever experience. Because we at the application level have no real control over the threads and the hardware, there's no perfect way of creating unit tests that guarantee your systems will behave correctly when used by multiple threads at the same time.
并发是程序员可能会遇到的最复杂和奇怪的错误的入口点。由于在应用程序层面上,我们对线程和硬件没有真正的控制,因此没有完美的方法来创建单元测试,以确保在同时被多个线程使用时系统会正确行为。
You can, however, make some very educated guesses. In this article, we'll take a look at what thread safety is, which tools iOS provides to help us achieve it, and how they compare in terms of performance.
然而,你可以做一些非常明智的猜测。在这篇文章中,我们将看看什么是线程安全,iOS 提供了哪些工具来帮助我们实现线程安全,以及它们在性能方面的比较。
What is Thread Safety?
I personally define thread safety as a system's ability to ensure "correctness" when multiple threads attempt to use it at the same time. Look at a specific class you have that contains logic that can be accessed by background threads and ask yourself: Is it possible for any two lines of code in this class to run in parallel? If so, would that be fine or would that be reallybad?
One thing I've noticed is that while developers generally don't have trouble understanding the concept of thread safety, a lot of people have trouble applying it in practice. Consider the following example of a class that stores a person's full name:
我个人将线程安全定义为系统在多个线程尝试同时使用时确保“正确性”的能力。看看你拥有的包含可以被后台线程访问的逻辑的特定类,并问自己:这个类中的任何两行代码是否可能并行运行?如果可能的话,这样做是否可以接受,还是会导致严重问题?
我注意到开发人员通常不难理解线程安全的概念,但很多人在实践中应用它时遇到了困难。考虑以下一个存储个人全名的类的示例:
final class Name {
private(set) var firstName: String = ""
private(set) var lastName: String = ""
func setFullName(firstName: String, lastName: String) {
self.firstName = firstName
self.lastName = lastName
}
}
Try asking yourself the same question as before. What would happen if two threads called
setFullName
at the same time? Would that work fine or would that be really bad?The answer is the latter. Because we're not synchronizing the threads' access to this class's state, you could have the following scenario happen:
如果两个线程同时调用 setFullName
方法会发生什么?这样做会顺利进行还是会产生问题呢?
答案是后者。因为我们没有同步线程对这个类状态的访问,可能会发生以下情况:
Thread 1: Call
setFullName("Bruno", "Rocha")
Thread 2: Call
setFullName("Onurb", "Ahcor")
Thread 1: Sets
firstName
to "Bruno"Thread 2: Sets
firstName
to "Onurb"Thread 2 (Again) : Sets
lastName
to "Ahcor"Thread 1: Sets
lastName
to "Rocha"Final name: "Onurb Rocha". That's not right...
This is called a race condition, and it's the least-worst scenario in this case. In reality, what would probably happen is that having two threads attempt to access the strings' memory addresses at the same time would trigger a EXC_BAD_ACCESS exception and crash your app.
In short, this means that the
Name
class is not thread-safe. To fix the above race condition, we need to synchronize how threads get to access and modify the state of this class. If we make it so that Thread 2 cannot start runningsetFullName
until Thread 1 finishes doing so, the scenario above would become impossible.In practice, many developers have trouble getting this right because they confuse atomicity with thread safety. Consider the following attempt to fix the race condition:
这被称为竞态条件(race condition) ,在这种情况下是最不理想的情形。实际上,可能发生的情况是,两个线程尝试同时访问字符串的内存地址,可能会触发 EXC_BAD_ACCESS
异常,并导致应用程序崩溃。
简而言之,这意味着 Name
类不是线程安全的。为了修复上述竞态条件,我们需要同步线程如何访问和修改该类的状态。如果我们确保线程2在线程1完成 setFullName
操作之前无法开始运行该方法,上述情景就不会发生。
实际上,许多开发者在此方面遇到困难,因为他们将原子性与线程安全混淆在一起。考虑以下尝试修复竞态条件的方法:
var dontLetSomeoneElseInPlease = false
func setFullName(firstName: String, lastName: String) {
guard !dontLetSomeoneElseInPlease else {
return
}
dontLetSomeoneElseInPlease = true
self.firstName = firstName
self.lastName = lastName
dontLetSomeoneElseInPlease = false
}
Many developers would look at this and think it solves the problem, while in reality, it achieves quite literally nothing. First of all, booleans in Swift are not atomic like in Obj-C, meaning that this code would give you the exact same memory corruption crash you'd have if you didn't have this logic in place. You need to use OS-level synchronization APIs, which we'll mention further below in the article in detail.
Second of all, even if you did create your own custom
AtomicBool
class, you'd still not be solving the race condition. While makingdontLetSomeoneElseInPlease
atomic would result in the boolean itself being thread-safe, that doesn't mean that theName
class as a whole is. What's difficult to grasp here is that thread safety is relative; while something might be thread-safe in relation to itself, it might not be in relation to something else. When evaluating thread safety from the point of view ofName
,setFullName
is still unsafe because it's still possible for multiple threads to go past the guard check at the same time and cause the same race condition scenario from before.To prevent a race condition in the state of the
Name
class, you need to prevent the entiresetFullName
logic from running in parallel. There are many different APIs which you can use to achieve this (iOS 15's async/await set of features being the most popular one as of writing), but here's one example that uses basic synchonization locks:
许多开发者可能看到这段代码并认为它解决了问题,但实际上,它实际上什么都没有做。首先,Swift 中的布尔值不像在 Obj-C 中那样是原子的,这意味着这段代码仍然会导致与没有这段逻辑的情况下相同的内存损坏崩溃。你需要使用操作系统级别的同步 API,我们将在本文后面详细讨论这些内容。
其次,即使你确实创建了自己的自定义 AtomicBool
类,你仍然没有解决竞态条件。虽然将 dontLetSomeoneElseInPlease
设为原子的会使布尔值本身是线程安全的,但这并不意味着 Name
类作为一个整体就是线程安全的。这里难以理解的是,线程安全是相对的;尽管某些东西相对于自身可能是线程安全的,但相对于其他东西可能不是。从 Name
的角度来评估线程安全,setFullName
仍然是不安全的,因为仍然有可能多个线程同时通过守卫检查,导致之前发生竞态条件的情景。
为了防止 Name
类的状态发生竞态条件,你需要防止整个 setFullName
逻辑并行运行。有许多不同的 API 可以实现这一点(截至本文写作时,iOS 15 的异步/等待功能集是最流行的之一),但这里是一个使用基本同步锁的例子:
var stateLock = OSAllocatedUnfairLock()
func setFullName(firstName: String, lastName: String) {
stateLock.lock()
self.firstName = firstName
self.lastName = lastName
stateLock.unlock()
}
In theoretical terms, what we did by wrapping the logic around calls to
lock()
andunlock()
was to establish a critical region withinsetFullName
which only one thread can access at any given time (a guarantee made by theOSAllocatedUnfairLock
API in this case). The logic withinsetFullName
is now thread-safe.Does this mean that the
Name
class itself is now thread-safe? It depends on the point of view. While thesetFullName
method itself is safe from race conditions, we still technically could have a race condition if some external object attempted to read the user's name in parallel with a new name being written. This is why the most important thing for you to keep in mind is the relativity of this problem: While you could say thatName
is technically thread-safe in relation to itself, it could very well be the case that whatever class that would be using this in a real application could be doing so in a way that is not. Even the reverse can happen: Although the strings in this example are technically not thread-safe themselves, they are if you consider that they cannot be modified outsidesetFullName
. To fix thread safety issues in the real world, you'll need to first rationalize the problem this way to determine what exactly needs to be made thread-safe in order to fix the problem.
在理论层面上,通过将调用 lock()
和 unlock()
包裹在 setFullName
中,我们建立了一个临界区域,在这个区域内一次只能由一个线程访问(在这种情况下由 OSAllocatedUnfairLock
API 提供的保证)。setFullName
内部的逻辑现在是线程安全的。
这是否意味着 Name
类本身现在是线程安全的呢?这取决于观点。 尽管 setFullName
方法本身免受竞态条件的影响,但从技术上讲,如果某个外部对象尝试与写入新名称并行读取用户名称,我们仍然可能发生竞态条件。这就是为什么你需要牢记这个问题的相对性:尽管你可以说 Name
在技术上相对于自身是线程安全的,但很可能在实际应用中使用这个类的任何类都可能以一种不安全的方式使用它。甚至相反的情况也可能发生:尽管在这个示例中,字符串在技术上本身不是线程安全的,但如果考虑到它们在 setFullName
之外不能被修改,它们就是线程安全的。在现实世界中解决线程安全问题,你首先需要通过这种方式理性化问题,以确定究竟需要使什么线程安全以解决问题。
Other concurrency problems
In the example above we talked about race conditions specifically, but in practice there are many different types of problems that you might encounter when working with thread safety and concurrency in general. We won't go into deep details of all of them, but here's a summary that covers the most common ones:
- Data Race: Two threads accessing shared data at the same time, leading to unpredictable results
- Race Condition: Failing at synchronize the execution of two or more threads, leading to events happening in the wrong order
- Deadlock: Two threads waiting on each other, meaning neither is able to proceed
- Priority Inversion: Low-priority task holding a resource needed by a high-priority task, causing delays in execution
- Thread Explosion: Excessive number of threads in the program, leading to resource exhaustion and decreased system performance
- Thread Starvation: Thread is unable to access a resource it needs because other threads are monopolizing it, causing delays in execution
的确,理解与线程安全和并发相关的各种挑战对于开发健壮且可靠的软件至关重要。以下是一些常见问题的简要总结:
- 数据竞争(Data Race): 当两个或更多线程同时访问共享数据时,至少其中一个线程修改数据,可能导致不可预测和错误的行为。
- 竞态条件(Race Condition): 未能正确同步两个或更多线程的执行,导致事件以意外或不正确的顺序发生。
- 死锁(Deadlock): 两个或更多线程无限期地被阻塞,每个线程都在等待其他线程释放资源。因此,没有一个线程能够继续执行。
- 优先级反转(Priority Inversion): 低优先级任务持有高优先级任务需要的资源,可能导致高优先级任务执行延迟。
- 线程爆炸(Thread Explosion): 在程序中创建过多的线程,导致资源耗尽和系统性能下降。
- 线程饥饿(Thread Starvation): 线程无法访问其需要的资源,因为其他线程正在独占,导致执行延迟。
这些问题突显了在软件中管理并发执行的复杂性,并强调了采用适当的同步机制和策略以确保线程安全的重要性。
Thread Safety costs
Before going further, it's good to be aware that any form of synchronization comes with a performance hit. This is not something you can run away from if you need to run code in parallel, but you can control how bad the impact will be. Different APIs have different performance costs, so by picking the right API for your problem, you can at least make sure you're not "over paying" for its thread safety. It's very common for developers to not be aware of these performance costs and proceed to pick extremely expensive APIs for relatively simple problems (here's one great example, look at the top two answers), so I strongly recommend you to be aware of these costs (which we'll see in the next section).
往下深入之前,值得注意的是任何形式的同步都会带来性能损失。如果你需要并行运行代码,这是无法避免的,但你可以控制影响的程度。不同的 API 具有不同的性能成本,因此通过为问题选择正确的 API,你至少可以确保不会为线程安全付出过高的代价。开发者很常见地不了解这些性能成本,然后为相对简单的问题选择极其昂贵的 API(这里有一个很好的例子,看一下前两个答案),因此我强烈建议你了解这些成本(我们将在下一节中看到)。
Thread Safety APIs in iOS
As mentioned in the example, there are many different ways in which you can achieve thread safety in Swift. The right API to use depends on the issue you're facing, so in this section we're going to cover all of them and provide some examples that shows what they're supposed to be used for.
正如在示例中提到的,你可以使用许多不同的方式在 Swift 中实现线程安全。要使用的正确 API 取决于你面临的问题,因此在这一节中,我们将涵盖所有这些并提供一些示例,展示它们应该用于什么情况。
async/await
If your app has a minimum target of at least iOS 15, you should probably go for async/await and ignore everything else in this article.
async/await doesn't solve the problem of concurrency, but it does make it a bit less problematic. By far the biggest problem with concurrency is how easy it is for you to make dangerous mistakes, and while async/await doesn’t protect you from logic mistakes / straight-up incorrect code, the way the feature works makes it safe from deadlocks, thread explosion, and data races, which is a major achievement for Swift as a programming language. You still need to be careful about race conditions and wrong code in general though, especially because the feature has many "gotchas" to it.
For more information on async/await, how to use it to implement thread safety, and what are those "gotchas" that you need to be careful about, check out my deep dive on async/await.
If you cannot use async/await, here are some of the "old school" synchronization APIs in iOS:
如果你的应用至少支持 iOS 15,那么你可能应该选择使用 async/await 并忽略本文中的其他内容。
async/await 并不能解决并发的问题,但它确实使问题稍微变得不那么棘手。在并发中最大的问题之一是很容易犯危险的错误,虽然 async/await 不能保护你免受逻辑错误/直接错误代码的影响,但该功能的工作方式使其免受死锁、线程爆炸和数据竞争等问题的困扰,这对于 Swift 作为一种编程语言来说是一项重大成就。然而,你仍然需要小心处理竞态条件和错误的代码,特别是因为该功能有许多需要注意的地方。
有关 async/await、如何使用它实现线程安全以及需要小心的一些问题,可以查看我对 async/await 的深入探讨。
如果你无法使用 async/await,在 iOS 中有一些“老派”的同步 API:
Serial DispatchQueues
Despite not being generally connected to the topic of thread safety,
DispatchQueues
can be great tools for thread safety. By creating a queue of tasks where only one task can be processed at any given time, you are indirectly introducing thread safety to the component that is using the queue.
尽管与线程安全的主题通常没有直接关联,但DispatchQueues
可以是实现线程安全的强大工具。通过创建一个任务队列,每次只能处理一个任务,你间接地为使用该队列的组件引入了线程安全。
let queue = DispatchQueue(label: "my-queue", qos: .userInteractive)
queue.async {
// Critical region 1
}
queue.async {
// Critical region 2
}
The greatest feature of
DispatchQueue
is how it completely manages any threading-related tasks like locking and prioritization for you. Apple advises you to never create your ownThread
types for resource management reasons -- threads are not cheap, and they must be prioritized between each other.DispatchQueues
handle all of that for you, and in the case of a serial queue specifically, the state of the queue itself and the execution order of the tasks will also be managed for you, making it perfect as a thread safety tool.Queues, however, are only great if the tasks are meant to be completely asynchronous. If you need to synchronously wait for a task to finish before proceeding, you should probably be using one of the lower-level APIs mentioned below instead. Not only running code synchronously means that we have no use for its threading features, resulting in wasted precious resources, but the
DispatchQueue.sync
synchronous variant is also a relatively dangerous API as it cannot deal with the fact that you might already be inside the queue:
DispatchQueue
的最大优势在于它完全为你管理与线程相关的任务,如锁定和优先级。苹果建议不要为了资源管理原因创建自己的 Thread
类型,因为线程不是廉价的,而且它们必须在彼此之间设置优先级。DispatchQueues
为你处理了所有这些,特别是在串行队列的情况下,队列本身的状态和任务的执行顺序也将被管理,使其成为一个完美的线程安全工具。
然而,队列只在任务被完全异步执行时表现得很好。如果需要在继续之前同步等待任务完成,你可能应该使用下面提到的较低级别的 API 之一。不仅同步运行代码意味着我们对其线程特性没有用处,从而浪费了宝贵的资源,而且DispatchQueue.sync
的同步变体也是一个相对危险的 API,因为它不能处理你可能已经在队列内的事实:
func logEntered() {
queue.sync {
print("Entered!")
}
}
func logExited() {
queue.sync {
print("Exited!")
}
}
func logLifecycle() {
queue.sync {
logEntered()
print("Running!")
logExited()
}
}
logLifecycle() // Crash!
Recursively attempting to synchronously enter the serial
DispatchQueue
will cause the thread to wait for itself to finish, which doesn't make sense and will result in the app freezing for eternity. This scenario is called a deadlock.It's technically possible to fix this, but we will not go into details of that as it's simply not a good idea to use
DispatchQueues
for synchronous purposes. For synchronous execution, we can have better performance and more predictable safety by using an old-fashioned mutual exclusion (mutex) lock.Note: When using
DispatchQueues
, make sure to follow these guidelines from @tclementdev in order to use them efficiently. Misusing queues can lead to serious performance and efficiency problems in your app, and it's so easy that even Apple has been moving away from it.
尝试在串行DispatchQueue
中使用同步方法进行递归调用,将导致线程等待自身完成,这是没有道理的,会导致应用永远冻结。这种情况被称为死锁。
从技术上讲,理论上可以修复这个问题,但我们不会详细讨论,因为使用DispatchQueues
来实现同步的需求,并不是一个好主意。对于同步执行,我们可以通过使用老式的互斥锁(mutex)获得更好的性能和更可预测的安全性。
注意:在使用DispatchQueues
时,请确保按照tclementdev 的这些指南 高效使用它们。错误使用队列可能导致应用严重的性能和效率问题,这很容易发生,以至于即使是 Apple 也一直在摆脱这种使用方式。
os_unfair_lock
The
os_unfair_lock
mutex is currently the fastest lock in iOS. If your intention is to simply establish a critical region like in our originalName
example, then this lock will get the job done with great performance.Note: Due to how Swift's memory model works, you should never use this API directly. If you're targeting iOS 15 or below, use the
UnfairLock
abstraction below when using this lock in your code. If you're targetting iOS 16, use the new built-in OSAllocatedUnfairLock type.
os_unfair_lock
互斥锁目前是 iOS 中最快的锁。如果你的意图只是像我们最初的 Name
示例中那样建立一个临界区域,那么这个锁将以出色的性能完成任务。
注意:由于 Swift 的内存模型的工作方式,你不应该直接使用这个 API。如果你的目标是 iOS 15 或更低版本,请在代码中使用下面的UnfairLock
抽象。如果你的目标是 iOS 16,请使用新的内置OSAllocatedUnfairLock类型。
// Read http://www.russbishop.net/the-law for more information on why this is necessary
final class UnfairLock {
private var _lock: UnsafeMutablePointer<os_unfair_lock>
init() {
_lock = UnsafeMutablePointer<os_unfair_lock>.allocate(capacity: 1)
_lock.initialize(to: os_unfair_lock())
}
deinit {
_lock.deallocate()
}
func locked<ReturnValue>(_ f: () throws -> ReturnValue) rethrows -> ReturnValue {
os_unfair_lock_lock(_lock)
defer { os_unfair_lock_unlock(_lock) }
return try f()
}
}
let lock = UnfairLock()
lock.locked {
// Critical region
}
It's not a surprise that it's faster than a
DispatchQueue
-- despite being low-level C code, the fact that we are not dispatching the code to an entirely different thread is saving us a lot of time.One very important thing you should be aware of mutexes in iOS is that all of them are unfair. This means that unlike a serial
DispatchQueue
, locks likeos_unfair_lock
make no guarantees in terms of the order in which the different threads get to access it. This means that one single thread could theoretically keep acquiring the lock over and over indefinitely and leave the remaining threads waiting in a process referred to as starving. When evaluating locks, you should first determine whether or not this can be a problem in your case.Note also that this lock specifically also cannot handle the recursion scenario we've shown in the
DispatchQueue
example. If your critical region is recursive, you'll need to use a recursive lock (shown further below).
它比DispatchQueue
更快并不奇怪——尽管它是低级别的 C 代码,但我们不将代码分派到完全不同的线程,这节省了很多时间。
在 iOS 中,你应该意识到关于互斥锁的一个非常重要的事情是它们都是不公平的。这意味着与串行DispatchQueue
不同,像os_unfair_lock
这样的锁在不同线程访问它的顺序方面没有任何保证。这意味着一个单一的线程理论上可以一遍又一遍地无限次获得锁,并使其余线程在一个被称为饥饿的过程中等待。在评估锁时,你应首先确定在你的情况下这是否可能成为一个问题。
还要注意,这个锁也不能处理我们在DispatchQueue
示例中展示的递归场景。如果你的临界区域是递归的,你需要使用递归锁(在下面进一步展示)。
NSLock
Despite also being a mutex,
NSLock
is different fromos_unfair_lock
in the sense that it's an Obj-C abstraction for a completely different locking API (pthread_mutex
, in this case). While the functionality of locking and unlocking is the same, you might want to chooseNSLock
overos_unfair_lock
for two reasons. The first one is that, well, unlikeos_unfair_lock
, you can actually use this API without having to abstract it.But lastly and perhaps more interestingly,
NSLock
contains additional features you might find very handy. The one I like the most is the ability to set a timeout:
尽管NSLock
也是一个互斥锁,但它与os_unfair_lock
不同,因为它是一个针对完全不同锁定 API(在这种情况下是pthread_mutex
)的 Obj-C 抽象。虽然锁定和解锁的功能相同,但你可能希望出于两个原因选择NSLock
而不是os_unfair_lock
。第一个原因是,与os_unfair_lock
不同,你实际上可以在不抽象它的情况下使用此 API。
但最后,也许更有趣的是,NSLock
包含了一些你可能会发现非常方便的附加功能。我最喜欢的一个是设置超时的能力:
let nslock = NSLock()
func synchronize(action: () -> Void) {
if nslock.lock(before: Date().addingTimeInterval(5)) {
action()
nslock.unlock()
} else {
print("Took to long to lock, did we deadlock?")
reportPotentialDeadlock() // Raise a non-fatal assertion to the crash reporter
action() // Continue and hope the user's session won't be corrupted
}
}
We saw that deadlocking yourself with a
DispatchQueue
at least will eventually cause the app to crash, but that's not the case with your friendly neighborhood mutexes. If you fall into a deadlock with them, they will do nothing and leave you with a completely unresponsive app. Yikes!However, in the case of
NSLock
, I actually find this to be a good thing. This is because the timeout feature allows you to be smarter and implement your own fallbacks when things don't go as planned; in the case of a potential deadlock, one thing I've done in the past with great success was to report this occurrence to our crash reporter and actually allow the app to proceed as an attempt to not ruin the user's experience with a crash. Note however that the reason why I could gamble on that was that what was being synchronized was purely some innocent client-side logic that doesn't get carried over to the next session. For anything serious and persistent, this would be a horrible thing to do.
我们看到,使用DispatchQueue
自己陷入死锁至少最终会导致应用崩溃,但对于你友好的互斥锁来说并非如此。如果你陷入死锁,它们将什么都不做,使你的应用完全无响应。哎呀!
然而,在NSLock
的情况下,我实际上认为这是一件好事。这是因为超时功能允许你更加聪明地在事情不按计划进行时实现自己的备用方案;在潜在死锁的情况下,我过去曾经非常成功地报告了此情况给我们的崩溃报告器,并实际上允许应用程序继续进行,以尝试不通过崩溃来破坏用户的体验。然而,请注意,我之所以能够赌博这一点的原因是,进行同步的是一些纯粹无害的客户端逻辑,不会传递到下一个会话。对于任何重要且持久的事务,这将是一件可怕的事情。
Despite being the same type of lock as
os_unfair_lock
, you'll findNSLock
to be slightly slower due to the hidden cost of Obj-C's messaging system.
尽管是与os_unfair_lock
相同类型的锁,由于 Obj-C 的消息传递系统的隐藏成本,你会发现NSLock
略慢一些。
NSRecursiveLock
If your class is structured in a way where claiming a lock can cause a thread to recursively try to claim it again, you'll need to use a recursive lock to prevent your app from deadlocking.
NSRecursiveLock
is exactlyNSLock
but with the additional ability to handle recursion:
如果你的类结构使得在试图再次获取锁时可能导致线程递归地尝试再次获取它,你将需要使用递归锁来防止你的应用程序陷入死锁。NSRecursiveLock
正是 NSLock
,但额外具有处理递归的能力:
let recursiveLock = NSRecursiveLock()
func synchronize(action: () -> Void) {
recursiveLock.lock()
action()
recursiveLock.unlock()
}
func logEntered() {
synchronize {
print("Entered!")
}
}
func logExited() {
synchronize {
print("Exited!")
}
}
func logLifecycle() {
synchronize {
logEntered()
print("Running!")
logExited()
}
}
logLifecycle() // No crash!
While regular locks will cause a deadlock when recursively attempting to claim the lock in the same thread, a recursive lock allows the owner of the lock to repeatedly claim it again. Because of this additional ability,
NSRecursiveLock
is slightly slower than the normalNSLock
.
普通的锁在同一线程中递归尝试获取锁时会导致死锁,而递归锁允许锁的所有者重复地再次获取它。由于这种额外的能力,NSRecursiveLock
稍微比普通的 NSLock
慢一些。
DispatchSemaphore
So far we've only looked at the problem of preventing two threads from running conflicting code at the same time, but another very common thread safety issue is when you need one thread to wait until another thread finishes a particular task before it can continue:
到目前为止,我们只关注了防止两个线程在同一时间运行冲突代码的问题,但另一个非常常见的线程安全问题是,当一个线程需要在另一个线程完成特定任务之前等待,然后它才能继续:
getUserInformation {
// Done
}
// Pause the thread until the callback in getUserInformation is called
print("Did finish fetching user information! Proceeding...")
Although this may sound very similar to a lock, you'll find that you cannot implement such a thing with them. This is because what you're looking for here is the opposite of a lock: instead of claiming a region and preventing other threads from doing so, we want to intentionallyblock ourselves and wait for a completely different thread to release us. This is what a semaphore is for:
尽管这听起来与锁非常相似,但你会发现使用锁无法实现这样的功能。这是因为在这里你寻找的是锁的相反:我们不是要声明一个区域并阻止其他线程这样做,而是有意地阻塞自己并等待完全不同的线程释放我们。这就是信号量的作用:
let semaphore = DispatchSemaphore(value: 0)
mySlowAsynchronousTask {
semaphore.signal()
}
semaphore.wait()
print("Did finish fetching user information! Proceeding...")
The most common example of a semaphore in iOS is
DispatchQueue.sync
-- we have some code running in another thread, but we want to wait for it to finish before continuing our thread. The example here is exactly whatDispatchQueue.sync
does, except we're building the semaphore ourselves.
DispatchSemaphore
is generally quick and contains the same features thatNSLock
has. You can additionally use thevalue
property to control the number of threads that are allowed to go throughwait()
before they're blocked andsignal()
has to be called; in this case, a value of 0 means that they will always be blocked.
在 iOS 中最常见的信号量示例是 DispatchQueue.sync
——我们在另一个线程中运行一些代码,但我们希望等待它完成后再继续我们的线程。这里的示例与 DispatchQueue.sync
的功能完全相同,只是我们自己构建了信号量。
DispatchSemaphore
通常很快,具有与 NSLock
相同的功能。你还可以使用 value
属性来控制允许通过 wait()
的线程数量,在它们被阻塞并调用 signal()
之前;在这种情况下,值为 0 意味着它们将始终被阻塞。
DispatchGroup
A
DispatchGroup
is exactly like aDispatchSemaphore
, but for groups of tasks. While a semaphore waits for one event, a group can wait for an infinite number of events:
DispatchGroup
与 DispatchSemaphore
完全相似,但用于任务组。虽然信号量等待一个事件,但组可以等待无限数量的事件:
let group = DispatchGroup()
for _ in 0..<6 {
group.enter()
mySlowAsynchronousTask {
group.leave()
}
}
group.wait()
print("ALL tasks done!")
In this case, the thread will only be unlocked when all 6 tasks have finished.
One really neat feature of
DispatchGroups
is that you have the additional ability to wait asynchronously by callinggroup.notify
:
在这种情况下,线程只有在所有6个任务都完成时才会解锁。
DispatchGroups
的一个非常棒的特性是,你还可以通过调用 group.notify
异步等待:
group.notify(queue: .main) {
print("ALL tasks done!")
}
This lets you be notified of the result in a
DispatchQueue
instead of blocking the thread, which can be extremely useful if you don't need the result synchronously.Because of the group mechanism, you'll find groups to be usually slower than plain semaphores:
这允许你在 DispatchQueue
中接收到结果的通知,而不是阻塞线程,如果你不需要同步结果,这可能非常有用。
由于组的机制,你会发现组通常比普通的信号量慢一些:
This means you should generally use
DispatchSemaphore
if you're only waiting for a single event, but in practice, the presence of thenotify
API makes a lot of people useDispatchGroups
for individual events as well.
这意味着如果你只等待单个事件,通常应该使用 DispatchSemaphore
,但在实践中,notify
API 的存在使得很多人也会将 DispatchGroups
用于单个事件。
网友评论