TaskUtilities is a small collection of types that help troubleshoot asynchronous Swift code and make synchronous code safe to call from an asynchronous context.
Important
RecursiveTaskLock
and LockedValue
are only intended for unusual cases
where you must make synchronous code safe to call from multiple asynchronous
tasks. Use these classes sparingly because they are blocking and subject to
deadlocks like the
dining philosopher's problem.
If you have the option of writing asynchronous code, Swift actors are a better
way to protect mutable state.
RecursiveTaskLock
provides a lock that can be locked recursively from within a
single task. Attempting to obtain the lock from a second task when it has been
locked by another will cause the second task to block until the lock is
released. This lets you make synchronous code safe to call from asynchronous
code in cases where you cannot use Swift actors.
class Names {
let lock = RecursiveTaskLock()
var names: [String] = []
func contains(name: String) -> Bool {
lock.withLock {
names.contains(name)
}
}
func add(name: String) {
lock.withLock {
names.append(name)
}
}
}
In the example above an NSLock
would also work fine. But in more complex cases
where there could be nested locks, a recursive lock is needed. For instance if
add
called contains
, or add
called an external closure that might call
contains
, a recursive lock is needed.
NSRecursiveLock
locks a thread, but Swift Tasks can move from thread to
thread. That makes NSRecursiveLock
unsafe because:
- two tasks running on the same thread could share a lock
- the same task running on a different thread might block waiting for the lock
RecursiveTaskLock
locks the operation to one task, not one thread.
LockedValue
wraps a lock around mutable data so that it can only be accessed
from one Task at a time. This is preferable to using a RecursiveTaskLock
directly, because it prevents you from accidentally accessing the data without
locking the lock.
class Names {
let names = LockedValue<[String]>([])
func contains(name: String) -> Bool {
names.withLockedValue { names in
names.contains(name)
}
}
func add(name: String) {
names.withLockedValue { names in
names.append(name)
}
}
}
TaskPath
is a debugging aid to help you understand the Task structure of
asynchronous code. It allows you to give a task a name, and retrieve that name
from an arbitrary point in your code.
Task.detached {
TaskPath.with(name: "Fetch image") {
...
}
}
// In other code called from that task:
print(TaskPath.current) // Prints "{Task Fetch image}"
If you name the same Task at different points in your code, the call structure will be preserved:
Task.detached {
TaskPath.with(name: "Fetch image") {
...
TaskPath.with(name: "Constructing request") {
...
}
}
}
// In other code called from the inner task:
print(TaskPath.current) // Prints "{Task Fetch image > Constructing request}"
Add the package https://github.com/samalone/task-utilities
to your Xcode
project, or add:
.package(url: "https://github.com/samalone/task-utilities.git", from: "1.0.0"),
to your package dependencies in your Package.swift
file. Then add:
.product(name: "TaskUtilities", package: "task-utilities"),
to the target dependencies of your package target.