Last active
December 12, 2017 06:03
-
-
Save deze333/82c0ee3e82fd250097449b1b200b7958 to your computer and use it in GitHub Desktop.
Multi-thread to single-thread synchronized executor pattern in Swift using run loop and a custom operation object (playground)
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
// Multi to single thread execution | |
// Implemented via a run loop and a custom operation object reference passing | |
import Foundation | |
import PlaygroundSupport | |
PlaygroundPage.current.needsIndefiniteExecution = true | |
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3000)) { | |
print("Done") | |
PlaygroundPage.current.finishExecution() | |
} | |
/// Operation class. | |
class Op : NSObject { | |
private let op: (Int) -> Int | |
var result: Int? = nil | |
static var isRandomSleep = false | |
init(op: @escaping (Int) -> Int) { | |
self.op = op | |
super.init() | |
} | |
@objc private func _execute(val: Any) { | |
guard let val = val as? Int else { return } | |
// If some sleep is needed | |
if Op.isRandomSleep { | |
let t = arc4random_uniform(1000) | |
Thread.sleep(forTimeInterval: Double(t) * 0.001) | |
} | |
result = op(val) | |
} | |
func execute(on thread: Thread, with val: Int) { | |
perform(#selector(Op._execute(val:)), on: thread, with: val, waitUntilDone: true, modes: [RunLoopMode.commonModes.rawValue]) | |
} | |
} | |
/// Sync processing in async context. | |
class BottleneckExecutor : NSObject { | |
private var thread: Thread! | |
func start() { | |
thread = Thread(target: self, selector: #selector(threadEntryPoint), object: nil) | |
thread.start() | |
} | |
@objc func threadEntryPoint() { | |
autoreleasepool { | |
Thread.current.name = "com.domain.app.background" | |
let runLoop = RunLoop.current | |
runLoop.add(NSMachPort(), forMode: .defaultRunLoopMode) | |
runLoop.run() | |
} | |
} | |
// Simple calculations | |
private func c_calcA(val: Int) -> Int { | |
return val * 3 | |
} | |
private func c_calcB(val: Int) -> Int { | |
return val * 5 | |
} | |
// Synchronous interface | |
func syncCalcA(val: Int) -> Int { | |
let op = Op(op: c_calcA) | |
op.execute(on: thread, with: val) | |
let verify = c_calcA(val: val) | |
if let r = op.result { | |
if r == verify { | |
return r | |
} else { | |
print("😡 result mismatch, \(r) != \(verify)") | |
} | |
} else { | |
print("😡 no result (nil)") | |
} | |
return -1 | |
} | |
func syncCalcB(val: Int) -> Int { | |
let op = Op(op: c_calcB) | |
op.execute(on: thread, with: val) | |
let verify = c_calcB(val: val) | |
if let r = op.result { | |
if r == verify { | |
return r | |
} else { | |
print("😡 result mismatch, \(r) != \(verify)") | |
} | |
} else { | |
print("😡 no result (nil)") | |
} | |
return -1 | |
} | |
} | |
/// Asynchronous caller thread | |
class Caller : Thread { | |
let executor: BottleneckExecutor | |
static let invocationsStarted = Date() | |
static var invocationsProcessed = 0 | |
static var invocationsBenchmark = 1_000 | |
init(executor: BottleneckExecutor) { | |
self.executor = executor | |
let _ = Caller.invocationsStarted | |
} | |
override func main() { | |
for i in 1...Caller.invocationsBenchmark { | |
switch arc4random_uniform(2) { | |
case 0: | |
let _ = executor.syncCalcA(val: i) | |
//print("calcA(\(i)) = \(r)") | |
default: | |
let _ = executor.syncCalcB(val: i) | |
//print("calcB(\(i)) = \(r)") | |
} | |
Caller.invocationsProcessed += 1 | |
if Caller.invocationsProcessed >= Caller.invocationsBenchmark { | |
let dur = Date().timeIntervalSince(Caller.invocationsStarted) | |
let invocationDuration = dur / Double(Caller.invocationsProcessed) | |
print("Invocations processed : \(Caller.invocationsProcessed)") | |
print("Invocations duration : \(dur)") | |
print("Cost per invocation, sec: \(invocationDuration)") | |
// and quit... | |
PlaygroundPage.current.finishExecution() | |
} | |
} | |
} | |
} | |
// Test it out | |
let executor = BottleneckExecutor() | |
executor.start() | |
Op.isRandomSleep = false | |
var callers: [Caller] = [] | |
for _ in 0...10 { | |
let caller = Caller(executor: executor) | |
callers.append(caller) | |
caller.start() | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment