我有三个这样的函数:
func getMyFirstItem(complete: @escaping (Int) -> Void) {
DispatchQueue.main.async {
complete(10)
}
}
func getMySecondtItem(complete: @escaping (Int) -> Void) {
DispatchQueue.global(qos:.background).async {
complete(10)
}
}
func getMyThirdItem(complete: @escaping (Int) -> Void) {
DispatchQueue.main.async {
complete(10)
}
}
我有一个变量:
var myItemsTotal: Int = 0
我想知道如何对这些项目求和,在这种情况下是10 10 10得到30。但什么是最好的方式,因为是背景和主要。
关键问题是确保线程安全。例如,以下是而不是线程安全:
func addUpValuesNotThreadSafe() {
var total = 0
getMyFirstItem { value in
total += value // on main thread
}
getMySecondItem { value in
total += value // on some GCD worker thread!!!
}
getMyThirdItem { value in
total += value // on main thread
}
...
}
可以通过不允许这些任务并行运行来解决这个问题,但您将失去异步进程及其提供的并发性的所有好处。
不用说,当您允许它们并行运行时,您可能会添加一些机制(如调度组(来知道所有这些异步任务何时完成。但我不想使这个例子复杂化,而是将重点放在线程安全问题上。(我稍后将在这个答案中展示如何使用调度组。(
无论如何,如果您有从多个线程调用的闭包,那么在不添加一些同步的情况下,就不能增加相同的total
。您可以添加与串行调度队列的同步,例如:
func addUpValues() {
var total = 0
let queue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".synchronized")
getMyFirstItem { value in
queue.async {
total += value // on serial queue
}
}
getMySecondItem { value in
queue.async {
total += value // on serial queue
}
}
getMyThirdItem { value in
queue.async {
total += value // on serial queue
}
}
...
}
有多种可供选择的同步机制(锁、GCD读写器、actor
等(。但我从串行队列的例子开始观察,实际上,任何串行队列都可以完成同样的事情。许多人使用主队列(它是一个串行队列(进行这种琐碎的同步,其中性能影响可以忽略不计,例如在本例中。
例如,可以重构getMySecondItem
,也可以像getMyFirstItem
和getMyThirdItem
那样在主队列上调用其完成处理程序。或者,如果不能做到这一点,可以简单地让getMySecondItem
调用方调度需要同步到主队列的代码:
func addUpValues() {
var total = 0
getMyFirstItem { value in
total += value // on main thread
}
getMySecondItem { value in
DispatchQueue.main.async {
total += value // now on main thread, too
}
}
getMyThirdItem { value in
total += value // on main thread
}
// ...
}
这也是线程安全的。这就是为什么许多库将确保在主线程上调用它们的所有完成处理程序,因为这可以最大限度地减少应用程序开发人员手动同步值所需的时间。
虽然我已经说明了使用串行调度队列进行同步,但有很多替代方案。例如,可以使用锁或GCD读写器模式。
关键是,在没有同步的情况下,不应该从多个线程中突变变量。
上面我提到,您需要知道三个异步任务何时完成。您可以使用DispatchGroup
,例如:
func addUpValues(complete: @escaping (Int) -> Void) {
let total = Synchronized(0)
let group = DispatchGroup()
group.enter()
getMyFirstItem { first in
total.synchronized { value in
value += first
}
group.leave()
}
group.enter()
getMySecondItem { second in
total.synchronized { value in
value += second
}
group.leave()
}
group.enter()
getMyThirdItem { third in
total.synchronized { value in
value += third
}
group.leave()
}
group.notify(queue: .main) {
let value = total.synchronized { $0 }
complete(value)
}
}
在这个例子中,我从addUpValues
:中提取了同步细节
class Synchronized<T> {
private var value: T
private let lock = NSLock()
init(_ value: T) {
self.value = value
}
func synchronized<U>(block: (inout T) throws -> U) rethrows -> U {
lock.lock()
defer { lock.unlock() }
return try block(&value)
}
}
显然,可以使用您想要的任何同步机制(例如,GCD或os_unfair_lock
或其他任何机制(。
但其想法是,在GCD世界中,当一系列异步任务完成时,调度组可以通知您。
我知道这是一个GCD问题,但为了完整性,Swift并发async
-await
模式使大部分问题变得毫无意义。
func getMyFirstItem() async -> Int {
return 10
}
func getMySecondItem() async -> Int {
await Task.detached(priority: .background) {
return 10
}.value
}
func getMyThirdItem() async -> Int {
return 10
}
func addUpValues() {
Task {
async let value1 = getMyFirstItem()
async let value2 = getMySecondItem()
async let value3 = getMyThirdItem()
let total = await value1 + value2 + value3
print(total)
}
}
或者,如果您的异步方法正在更新某个共享属性,则可以使用actor
来同步访问。请参阅使用Swift actors保护可变状态。
我可能错了,但我认为不同的队列没有太大区别,你仍然必须";等待";直到完成,例如:
var myItemsTotal: Int = 0
getMyFirstItem() { val1 in
getMySecondtItem() { val2 in
getMyThirdItem() { val3 in
myItemsTotal = val1 + val2 + val3
print(" myItemsTotal: (myItemsTotal)")
}
}
}