-
-
Notifications
You must be signed in to change notification settings - Fork 4
Description
TL;DR: The use of context.performAndWait
can introduce deadlocks into programs which would not deadlock with normal actors.
Background
I was excited to use this library to solve some threading issues I was experiencing with an app which used CoreData and actors. While introducing the library did address the crashes I was seeing, it also introduced mysterious deadlocks. I believe this is because under certain circumstances the logic here can deadlock.
Cause
Unlike normal executors, where enqueue
queues up work to be done later, with NSModelActor
the enqueue
method is now synchronous, which means the calling thread cannot continue until the called method is complete. This became true with the move to use performAndWait
introduced in da22a14. Reverting that change addresses the issue in my app, and also in my minimal repro. I don't understand why the change was initially made so can't comment on whether it is safe to revert in the general case.
Minimal Test Case
Below is some code which demonstrates an interaction pattern which is safe with normal actors, but deadlocks with NSModelActor
import CoreData
import CoreDataEvolution
/// A helper class which causes all threads to wait until an expected number have reached
/// the synchronization point, and then allows all to continue.
/// This is used to allow us to reliably exercise the race condition to be demonstrated.
class ThreadBarrier {
private let condition = NSCondition()
private var threadCount: Int
private var currentCount = 0
init(threadCount: Int) {
self.threadCount = threadCount
}
func wait() {
condition.lock()
defer { condition.unlock() }
currentCount += 1
if currentCount < threadCount {
// Wait until all threads reach the barrier
condition.wait()
} else {
// Last thread wakes up all waiting threads
condition.broadcast()
}
}
}
/// An actor which will oinvoke an inner method on another instance of the
/// same type. To demonstrate the deadlock we will create two actors which each
/// enter their own code (taking the performAndWait lock) and then try to call each other.
protocol MutuallyInvokingActor : Actor {}
extension MutuallyInvokingActor {
func outer(barrier: ThreadBarrier, other: any MutuallyInvokingActor) async {
print("Start Outer")
barrier.wait()
print("After Barrier")
await other.inner()
print("End Outer")
}
func inner() {
print("Inner")
}
}
/// This is a normal actor which will not produce a deadlock, because the normal actor
/// `enqueue` method just queues up a method to call later
actor NormalActor : MutuallyInvokingActor {}
/// This `NSModelActor` will deadlock because enqueue calls actor methods synchronously
@NSModelActor
actor DeadlockActor : MutuallyInvokingActor {}
/// Run the two actors in parallel to attempt to demonstrate the deadlock
func attemptDeadlock(_ actorA: MutuallyInvokingActor, _ actorB: MutuallyInvokingActor) async {
print("Attempting to demonstrate actor deadlock")
let barrier = ThreadBarrier(threadCount: 2)
// Invoke DeadlockActor.outer on both actors in parallel
async let result1 = actorA.outer(barrier: barrier, other: actorB)
async let result2 = actorB.outer(barrier: barrier, other: actorA)
let _ = await (result1, result2)
print("Comlete - actors did not deadlock")
}
// With normal actors demonstrate the program does not deadlock
print("Running mutually invoking code between two normal actors")
let normalActorA = NormalActor()
let normalAactorB = NormalActor()
await attemptDeadlock(normalActorA, normalAactorB)
// With ModelActors demonstrate that the program does deadlock
let description = NSPersistentStoreDescription()
description.type = NSInMemoryStoreType
let container = NSPersistentContainer(name: "Model")
container.persistentStoreDescriptions = [description]
container.loadPersistentStores(completionHandler: { _, _ in })
print("Running mutually invoking code between two NSModelActors")
let deadlockActorA = DeadlockActor(container: container)
let deadlockAactorB = DeadlockActor(container: container)
await attemptDeadlock(deadlockActorA, deadlockAactorB)