何时在iOS中选择序列式而不是并发队列

发布于 2025-01-20 20:30:37 字数 943 浏览 0 评论 0 原文

我被要求在 swift 中实现一个线程安全字典,我使用了常见的方法:

 class MutableDictionary {
    var dictionary: [String : Any] = [:]
    var queue = DispatchQueue(label: "queue", attributes: .concurrent)
    
    func object(for key:  String) -> Any? {
        queue.sync {
            return dictionary[key]   
        }
    }
    
    func set(_ object: Any?, for key: String) {
        queue.async(flags: .barrier) {
            self.dictionary[key] = object
        }
    }
}

但是,接下来的问题是:

  1. 使用 concurrent + barrier 与仅使用有什么区别在这种情况下使用 serialQueue 进行设置?
  2. 我在 Playground 上做了一个测试,我用 1000 次 for 循环包装了 get 和 set,结果发现串行队列和并发队列的行为几乎相同。
  3. 为什么设置总是报错? 输入图片此处的描述
  4. 与串行队列相比,并发队列在这种情况下(对于获取和设置)做得更好吗?
  5. 在 iOS 中,什么时候应该选择串行队列而不是并发队列?反之亦然?

I was asked to implement a thread safe dictionary in swift, I used the common approach:

 class MutableDictionary {
    var dictionary: [String : Any] = [:]
    var queue = DispatchQueue(label: "queue", attributes: .concurrent)
    
    func object(for key:  String) -> Any? {
        queue.sync {
            return dictionary[key]   
        }
    }
    
    func set(_ object: Any?, for key: String) {
        queue.async(flags: .barrier) {
            self.dictionary[key] = object
        }
    }
}

However, the following up question is:

  1. What's the difference with using concurrent + barrier vs just using a serialQueue for setting in this case?
  2. I did a testing on playground, I wrapped the get and set with 1000 time for loop, it turns out that the behavior for both serial and concurrent queue is almost the same.
  3. Why the setting always raise an error?
    enter image description here
  4. What does concurrent queue do better in this case (for both get and set) compared to a serial queue?
  5. In iOS, when should I choose serial queue over concurrent queue? And vice-versa?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

只是在用心讲痛 2025-01-27 20:30:37

1,使用屏障,并发队列临时可以一次执行一个任务。

但是,Serialqueue一次只能执行一个任务。

2,鉴于您仅使用队列读/写,它们显然具有相同的效果。如果您将另一项任务放在其中,显然并发队列的成本较低,那是真正的区别。

3,当您的对象是类时,将定义提交给异步对象地址的。当您给班级成员一个新值时,您将有一个不好的地址,因此您无法访问它并且错误出现。
您可以尝试结构

4,请参阅答案1

5,有时,当您希望更快地执行任务时,请首先进行consurrentqueue。如果您想有序地执行任务,Serialqueue会做得更好。

1、with barrier, the concurrent queue is temporary able to execute one task at a time.
enter image description here

However, the serialQueue can only perform one task at a time.

2、Given you only use the queue to read/write, they obviously have the same effect. if you put another kind task to it, apparently the concurrent queue cost less and that is the real difference.

3、Submitted to async object address will be defined when your object is a class. You will have a bad address when you give the class member a new value, So you cannot access it and the error comes.
You can have a try by struct
.

4、refer to answer 1.

5、Sometimes when you want the task to be performed faster, concurrentQueue first. If you want to execute tasks orderly, serialQueue do the better.

瑾夏年华 2025-01-27 20:30:37
  1. 并发 +屏障可以同时运行多个读取串行队列一次只能运行一个任务(读/写)。

  2. 结果是相同的,甚至串行队列更好,因为您只使用一个线程来运行。您只能利用并发 +屏障实现当读取/写入操作发生在多线程/队列上时。在这种情况下,串行队列更好,因为您不需要在队列/线程之间查看和切换。

  3. 请问吗?

  4. 并发 +屏障可能更好,例如(2)中,有时,如果您可以确保所有操作都在同一线程中发生,则串行队列更好。

  5. 这取决于您的情况,如(2),(4)中所述。关于并发 +障碍的另一件事,有时这不是您想象的好选择。想象:

  • 您正在实现一个需要进行重大写入操作的功能,例如,您正在阅读dict并计算新值并更新dict。您将所有这些步骤包装在 queue.async(flags:.barrier) block。

  • 您希望这些步骤在线程(背景线程)中运行,并且不会阻止主队列阅读dict以更新UI。但是它确实阻止了主队列,对吗? 读取主要队列的操作必须等待 barrier 首先完成。

  • 如果您的应用程序消耗了大量CPU,则可能需要等待OS为 Update> Update> Steass找到线程,这意味着您必须花更多的时间在上面。


  1. concurrent + barrier can run multiple read at the same time. serial queue can only run one task (read/write) at a time.

  2. the results are the same, or even that the serial queue is better because you're using only one thread to run. You can only take advantage of the concurrent + barrier implementation when read/write operations happen on multiple thread/queue. In this case, the serial queue is better because you don't need to look and switch between queue/thread.

  3. Full source code, please?

  4. Concurrent + barrier might be better or not, as in (2), sometimes if you can ensure all operations happen in the same thread, then serial queue is better.

  5. It depends on your case, as mentioned in (2), (4). One more thing about concurrent + barrier, sometimes it isn't a good choice as you think. Imagine:

  • You're implementing a feature that needs to do a heavy write operation, for example, you're reading your dict and calculating a new value, and updating the dict. You wrap all of these steps in a queue.async(flags: .barrier) block.

  • You expect these steps are running in your thread (background thread) and it doesn't block the main queue from reading the dict to update UI. But it does block the main queue, right? Read operations from the main queue have to wait for barrier block to finish first.

  • If your application consumes a lot of CPU, you may have to wait for OS to find the thread for your update steps, which means you have to spend more time on it.

败给现实 2025-01-27 20:30:37

正如@nghiahoang在答案中解释的那样,使用并发队列的优势在于,您可以同时执行多个“ get”请求。

您崩溃的原因是因为您的 set 操作正在使用屏障在紧密的环路中使用异步调度。当您在上一个请求完成之前提交新的 SET 请求时,需要一个新线程。只有有限数量的线程可用。一旦您用尽了线程池,您的应用程序就会崩溃。

我建议您对您的代码进行几次更改,以帮助您解决此问题。

首先是将您的调度队列绑定到全球调度队列之一,因为这是最佳实践。

第二个是在 set> set 而不是 async 中使用同步。这将防止线程池耗尽,但更重要的是,它可以确保一旦 set 返回字典实际上已更新。使用 async ,该更新将在未指定的未来时间进行。

class MutableDictionary {
    var dictionary: [String : Any] = [:]
    var queue = DispatchQueue(label: "queue", attributes: .concurrent, target:.global(qos: .userInitiated))
    
    func object(for key:  String) -> Any? {
        queue.sync {
            return dictionary[key] 
        }
    }
    
    func set(_ object: Any?, for key: String) {
        queue.sync(flags: .barrier) {
            self.dictionary[key] = object
        }
    }
}

As @nghiahoang explained in their answer, the advantage of using a concurrent queue is that you can perform multiple 'get' requests simultaneously.

The reason you are getting a crash is because your set operation is using asynchronous dispatch along with a barrier in a tight loop. When you submit a new set request before the previous request has completed, a new thread is required. There are only a limited number of threads available. Once you have exhausted the thread pool your app crashes.

I would suggest a couple of changes to your code to help with this.

The first is to bind your dispatch queue to one of the global dispatch queues, as this is best practice.

The second is to use sync in your set rather than async. This will prevent thread pool exhaustion, but more importantly it ensures that once set returns that the dictionary has actually been updated. With async, the update will happen at some unspecified future time.

class MutableDictionary {
    var dictionary: [String : Any] = [:]
    var queue = DispatchQueue(label: "queue", attributes: .concurrent, target:.global(qos: .userInitiated))
    
    func object(for key:  String) -> Any? {
        queue.sync {
            return dictionary[key] 
        }
    }
    
    func set(_ object: Any?, for key: String) {
        queue.sync(flags: .barrier) {
            self.dictionary[key] = object
        }
    }
}
多孤肩上扛 2025-01-27 20:30:37

你问:

  1. 在这种情况下,使用 concurrent + barrier 与仅使用 serialQueue 进行设置有何区别?

前者允许并发读取,后者则不允许。但两者都是线程安全的标准模式。

FWIW,这种“并发+屏障”方法被称为“读写器”模式。它具有两个关键行为,即读取可以并发发生(因为它是并发队列),并且调用者不必等待写入(因为我们使用带有屏障的异步调用它)。

  • 我在 Playground 上做了一个测试,我用 1000 次 for 循环包装了 get 和 set,结果发现串行队列和并发队列的行为几乎相同。
  • 首先,Playgrounds 并不是测试此类细微性能差异的理想环境。在具有优化的“发布”版本的应用程序中进行测试。 (这也使您有机会使用 “Thread Sanitizer”。)

    其次,您只会看到非常有限的性能差异(可能只有在进行数百万次迭代而不是数千次迭代时才能观察到)。但这两种模式的整体行为应该是相似的,因为它们正在做相同的事情,即同步您对此字典的访问。

    第三,两个 for 循环从单个线程调用此 MutableDictionary。如果您正在测试线程安全字典,我建议实际上从多个线程调用它。如果您仅从单个线程访问它,则引入线程安全开销是没有意义的。例如,这测试多线程行为:

    let dict = MutableDictionary()
    
    DispatchQueue.concurrentPerform(iterations: 1_000) { i in
        let result = dict.object(for: "\(i)")
        print(result)                          // this will always be `nil`, though, because your dictionary has no values yet
    }
    
    DispatchQueue.concurrentPerform(iterations: 1_000) { i in
        dict.set("a", for: "\(i)")
    }
    

    DispatchQueue.concurrentPerform 是一个并行的for 循环。

  • 为什么该设置总是引发错误?
  • “在此处输入图像描述"

    这是Playgrounds引入的,可能是它无法处理无限制调度1000个异步任务的线程爆炸。在实际的应用程序(而不是游乐场)中尝试一下,工作线程耗尽不会导致此崩溃。

    话虽如此,您可能应该完全避免允许线程爆炸的情况。

    例如,如果您确实想让一个线程更新字典中的 1,000 个值(全部来自单个线程),您可以提供一个方法,允许您通过单个同步调用更新多个值。例如,

    class MutableDictionary<Key: Hashable, Value> {
        private var dictionary: [Key: Value] = [:]
        private let queue = DispatchQueue(label: "queue", attributes: .concurrent)
    
        func object(for key: Key) -> Value? {
            queue.sync {
                dictionary[key]
            }
        }
    
        func set(_ object: Value?, for key: Key) {
            queue.async(flags: .barrier) {
                self.dictionary[key] = object
            }
        }
    
        func synchronized(block: @escaping (inout [Key: Value]) -> Void) {
            queue.async(flags: .barrier) { [self] in
                block(&dictionary)
            }
        }
    }
    
    let dictionary = MutableDictionary<Int, String>()
    
    dictionary.synchronized {
        for i in 0 ... 1_000 {
            $0[i] = "a"
        }
    }
    

    在一次同步中更新数千个字典值。这同时消除了不必要的同步并解决了线程爆炸问题。

  • 在这种情况下(对于获取和设置),与串行队列相比,并发队列在哪些方面做得更好?
  • 理论上,如果您的应用程序可能同时执行许多并发读取,则并发队列“读取器-写入器”模式可以提供更好的性能。否则,并发队列的开销不太可能没有必要/没有好处。

    在实践中,我还没有遇到并发队列明显更快的现实场景。

  • 在 iOS 中,什么时候应该选择串行队列而不是并发队列?反之亦然?
  • 我会针对您的特定用例对其进行基准测试,并查看并发队列是否产生可观察到的性能优势。如果是这样,并发队列的开销可能是值得的。否则,您可能想坚持使用简单的串行 GCD 队列。

    一般来说,您不会看到明显的差异。我发现其他方法(例如 NSLockos_unfair_lock)甚至更快。但如果您正在寻找一种简单而强大的同步机制,串行调度队列是一个很好的解决方案。


    其他一些观察:

    • 如果创建字典,我会倾向于避免使用 Any。我们拥有强类型语言,失去这些好处是一种耻辱。例如,您可以将其设为泛型,如上例所示,使用 KeyValue

    • 包装的字典应该是私有。公开底层字典是不明智的,这会破坏线程安全接口。

    • 同步机制(例如,锁或GCD队列或其他)也应该是私有的。

    You asked:

    1. What's the difference with using concurrent + barrier vs just using a serialQueue for setting in this case?

    The former allows concurrent reads and the latter does not. But both are standard patterns for thread-safety.

    FWIW, this “concurrent + barrier” approach is known as the “reader-writer” pattern. It features two key behaviors, namely that reads can happen concurrently (because it is a concurrent queue) and also that the caller does not have to wait for writes (because we invoke it with async with barrier).

    1. I did a testing on playground, I wrapped the get and set with 1000 time for loop, it turns out that the behavior for both serial and concurrent queue is almost the same.

    First, Playgrounds are not an ideal environment for testing subtle performance differences like this. Test in an app with a optimized, “release” build. (This also gives you the chance to debug with tools like the “Thread Sanitizer”.)

    Second, you will only see very modest performance differences (probably only observable if doing millions of iterations, not thousands). But the overall behavior of these two patterns should be similar, as they are doing the same thing, namely synchronizing your access to this dictionary.

    Third, your two for loops invoke this MutableDictionary from a single thread. If you are testing a thread-safe dictionary, I would suggest actually calling it from multiple threads. There is no point in introducing the thread-safety overhead if you are only accessing it from a single thread. E.g., this tests multithreaded behavior:

    let dict = MutableDictionary()
    
    DispatchQueue.concurrentPerform(iterations: 1_000) { i in
        let result = dict.object(for: "\(i)")
        print(result)                          // this will always be `nil`, though, because your dictionary has no values yet
    }
    
    DispatchQueue.concurrentPerform(iterations: 1_000) { i in
        dict.set("a", for: "\(i)")
    }
    

    DispatchQueue.concurrentPerform is a parallel for loop.

    1. Why the setting always raise an error?

    enter image description here

    This is introduced by Playgrounds, probably its inability to handle the thread explosion of the unbridled dispatching of 1,000 asynchronous tasks. Try it in an actual app, not a playground, and the worker thread exhaustion will not result in this crash.

    That having been said, you should probably avoid scenarios that permit thread explosion at all.

    For example, if you really want to have a thread update 1,000 values in the dictionary, all from a single thread, you might provide a method that allows you to update multiple values with a single synchronization call. E.g.,

    class MutableDictionary<Key: Hashable, Value> {
        private var dictionary: [Key: Value] = [:]
        private let queue = DispatchQueue(label: "queue", attributes: .concurrent)
    
        func object(for key: Key) -> Value? {
            queue.sync {
                dictionary[key]
            }
        }
    
        func set(_ object: Value?, for key: Key) {
            queue.async(flags: .barrier) {
                self.dictionary[key] = object
            }
        }
    
        func synchronized(block: @escaping (inout [Key: Value]) -> Void) {
            queue.async(flags: .barrier) { [self] in
                block(&dictionary)
            }
        }
    }
    
    let dictionary = MutableDictionary<Int, String>()
    
    dictionary.synchronized {
        for i in 0 ... 1_000 {
            $0[i] = "a"
        }
    }
    

    That updates the thousand dictionary values in a single synchronization. This simultaneously eliminates unnecessary synchronizations and solves the thread explosion issue.

    1. What does concurrent queue do better in this case (for both get and set) compared to a serial queue?

    Theoretically, the concurrent queue, “reader-writer” pattern, can offer better performance if you have an app that may be doing many concurrent reads simultaneously. Otherwise the overhead of the concurrent queue is unlikely not necessary/beneficial.

    In practice, I have yet to encountered real-world scenarios where the concurrent queue was observably faster.

    1. In iOS, when should I choose serial queue over concurrent queue? And vice-versa?

    I would benchmark it for your particular use-case and see if the concurrent queue yields observable performance benefits. If so, the overhead of the concurrent queue might be worth it. Otherwise, you might want to stick with the simple serial GCD queue.

    Generally, you will not see an observable difference. And where I have, I found that other approaches (e.g. NSLock or os_unfair_lock) were even faster. But if you are looking for a simple and robust synchronization mechanism, a serial dispatch queue is a good solution.


    A few other observations:

    • If creating a dictionary, I would be inclined to avoid the use of Any. We have a strongly-typed language and it is a shame to lose these benefits. E.g., you might make it a generic as shown in the example above with Key and Value.

    • The wrapped dictionary should be private. It is imprudent to expose the underlying dictionary which would undermine the thread-safe interface.

    • The synchronization mechanism (e.g., the lock or GCD queue or whatever) should be private, too.

    ~没有更多了~
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文