Skip to content

Instantly share code, notes, and snippets.

@pettermahlen
Created November 29, 2019 14:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pettermahlen/e98e51e0fd6061d314cf1962005790bc to your computer and use it in GitHub Desktop.
Save pettermahlen/e98e51e0fd6061d314cf1962005790bc to your computer and use it in GitHub Desktop.
import Foundation
/// Utility to catch invalid multithreaded access to non-thread-safe code.
///
/// `MultiThreadedAccessDetector` guards a critical region, verifying that it is never invoked by two different threads.
/// If this happens, it crashes in debug builds. In release builds, it has no effect (and also no overhead, as long as
/// it’s only used within one module).
///
/// Copies of a `MultiThreadedAccessDetector` share the same underlying mutex, and hence their critical region is the
/// union of the critical regions of all copies.
struct MultiThreadedAccessDetector {
#if DEBUG
// This is hidden in an inner final class because we want it to be an empty struct in non-debug builds
private final class State {
let lock = NSRecursiveLock()
var firstCaller: Caller?
}
private struct Caller: CustomStringConvertible {
var thread: Thread
var file: StaticString
var line: UInt
var queue: String
var description: String {
let shortFile = String(describing: file).split(separator: "/").last ?? "<unknown>"
return "\(shortFile): \(line) on “\(queue)”"
}
}
private let state = State()
func `guard`<T>(file: StaticString = #file, line: UInt = #line, _ block: () throws -> T) rethrows -> T {
let currentCaller = Caller(thread: Thread.current, file: file, line: line, queue: currentQueueLabel())
verifyAccess(currentCaller: currentCaller, file: file, line: line)
return try block()
}
private func verifyAccess(currentCaller: Caller, file: StaticString, line: UInt) {
// lock to ensure race-free access to the state
state.lock.lock()
defer { state.lock.unlock() }
if let firstCaller = self.state.firstCaller {
guard Thread.current == firstCaller.thread else {
preconditionFailure(
"""
Unpermitted access by multiple threads.
First thread \(firstCaller.thread) from \(firstCaller)
Conflicting access by \(Thread.current) from \(currentCaller)
""",
file: file,
line: line
)
}
} else {
state.firstCaller = currentCaller
}
}
private func currentQueueLabel() -> String {
let name = __dispatch_queue_get_label(nil)
return String(cString: name, encoding: .utf8) ?? ""
}
#else
// This currently needs to be explicitly annotated to be optimized out
@inline(__always)
func `guard`<T>(_ block: () throws -> T) rethrows -> T {
return try block()
}
#endif
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment