Created
November 29, 2019 14:52
-
-
Save pettermahlen/e98e51e0fd6061d314cf1962005790bc to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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