Skip to content

Instantly share code, notes, and snippets.

@Enricoza
Last active April 26, 2024 10:15
Show Gist options
  • Save Enricoza/49df0dbd8a6f13c5db738f69f888950e to your computer and use it in GitHub Desktop.
Save Enricoza/49df0dbd8a6f13c5db738f69f888950e to your computer and use it in GitHub Desktop.
[iOS - Swift] Organize serial DispatchQueue (Trees) in a small DispatchForest to improve performance and avoid Thread explosion

This code is my opinionated interpretation of an idea expressed by eskimo on this swift forum thread, to which I briefly partecipated. I'll try to summarize his viewpoint in this premise, as well as some of the suggestions from this gist.

  1. You should virtually never create a queue with DispatchQueue.global() nor with the concurrent flag.
  2. You should create a limited number (a small Forest) of SERIAL queues (the Trees), one for each subsystem in your program.
  3. Apart from those base queues (the Trees), you should never create any more queues without specifying the target queue.
  4. You can, instead, create as many queues (the Branches) as you want, as long as they are targeting those base queues (the Trees).
  5. You should dispatch large chunks of code, in order to reduce the overhead of the dispatch operations.
  6. Therefore, you should move your concurrency control out of small primitives, and force the caller to serialize their work on that primitive by using a serial queue. (This makes the primitive code cleaner and naturally encourages to dispatch larger chunks of code)
  7. Should you, instead, need more symmetric parallelism you can use DispatchQueue.concurrentPerform(iterations:execute:) while still reminding yourself that the larger the parallelized blocks, the better.
  8. Should you, then, still need more concurrency within a subsystem you can create more Trees for that subsystem. Just keep in mind that the amount of those Trees should be kept small. The rule of thumb is: start with one and than evaluate and measure performance to prove that more concurrency actually helps.

The main benefits of this approach are:

  • actually improving performances (while concurrent queues do the opposite)
  • avoiding Thread explosion (thanks to the limited number of Trees and having no concurrent queue)
  • having different base QoS for each subsystem (thanks to each Tree)
  • rising the base QoS of a small part of a subsystem (thanks to each Branch)
  • having a correct queue label when debugging or analysing crash reports (thanks to each Tree and Branch)
  • working with dispatchPrecondition (thanks to each Tree and Branch)
//
// DispatchForest.swift
// DispatchForest
//
// Created by Enrico Zannini on 26/01/2021.
// Copyright © 2021 Enrico Zannini.
//
/**
* A limited forest of serial dispatch queues (Trees), each used for one subsystem.
*
* Trees and Branches are named following the Reverse-DNS naming convention.
* Just name every branch with it's limited scope and the reverse-DNS name will be concatenated
* by following the Branches up to the base Tree.
*
* If you wish to add more Trees to the forest you can do so by adding them to this class as `static let`.
* Just be sure not to create more than it's necessary. Usually 3 to 4 Trees are all it's needed for a simple app.
*
* Should you need other Queues onto which dispatch different kind of work, or all the work of a smaller part of a subsystem,
* you can branch one of the Trees, or even branch from another Branch if that's the case, without any concern.
* You can create as many branches as you want.
*
* Every work scheduled on a Tree, or on any branch of that tree, will execute in a mutually exclusive manner,
* on the queue of the base Tree.
* The quality of service for a single work item can never be lower than the qos of both the Branch
* on which the work is scheduled, and every ancestor of his, up to the base Tree itself.
* Of course the system optimizes this process and takes care of resolving the priority inversion
* in case that a priority inversion happens.
*
* As an example, in this class we added 4 Trees: the obvious main (which is ultimately just the main queue)
* and network, database and analytics.
* You can create your own Trees and call them as you want.
*/
final class DispatchForest {
fileprivate static let nomenclature = DispatchNomenclature(baseName: "com.dispatchForest")
/// The main Tree, wich is just the main thread, added here just for reference.
static let main: DispatchQueue = DispatchQueue.main
/// A Tree used for a network subsystem.
static let network: DispatchQueue = {
return newTree(name: "network", qos: .default)
}()
/// A Tree used for a local database subsystem.
static let database: DispatchQueue = {
return newTree(name: "database", qos: .default)
}()
/// A Tree used for an analytics subsystem. QoS here is probably lower since analytics is often less important than other stuff.
static let analytics: DispatchQueue = {
return newTree(name: "analytics", qos: .utility)
}()
/**
* Create a new Serial DispatchQueue Tree, without any other target queue.
*
* Every work scheduled on this Tree, or on any Branch of this Tree, will execute in a mutually exclusive manner.
*
* - parameter name: The name of this Tree, that will be concatenated with the baseName of every tree
* - parameter qos: The qos of this Tree, which will be the minimum qos of the blocks scheduled to this queue and to it's Branches.
*/
private class func newTree(name: String, qos: DispatchQoS = .default) -> DispatchQueue {
let label = nomenclature.newTreeName(name)
return DispatchQueue(label: label, qos: qos)
}
private init() {}
}
extension DispatchQueue {
/**
* Create a Serial Branch from this Tree, concatenating the name to the rest of the Tree name.
*
* Every work scheduled to this Branch will execute in a mutually exclusive manner from any other work scheduled to this Branch,
* to its ancestors and to any branches that originated from them, up to the base Tree.
*
* - parameter name: The name is concatenated with the current label with a ".", following the reverse-DNS naming style
* - parameter qos: The optional quality of service for this branch. If `nil` it will use the qos of self.
* Use `unspecified` if you wish the system to infere the qos from the qos of the queue from which every block is scheduled.
*/
func newBranch(name: String, qos: DispatchQoS? = nil) -> DispatchQueue {
return DispatchQueue(label: DispatchForest.nomenclature.compositeName(base: self.label, name: name),
qos: qos ?? self.qos,
target: self)
}
}
/**
* A private class used to concatenate name of the Branches using the reverse-DNS naming convention.
*/
private class DispatchNomenclature {
let baseName: String
init(baseName: String) {
self.baseName = baseName
}
func newTreeName(_ name: String) -> String {
return compositeName(base: baseName, name: name)
}
func compositeName(base: String, name: String) -> String {
return "\(base).\(name)"
}
}
MIT License
Copyright (c) 2021 DispatchForest
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment