You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
classThreadBarrier{privateletcondition=NSCondition()privatevarthreadCount:IntprivatevarcurrentCount=0init(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.
protocolMutuallyInvokingActor:Actor{}extensionMutuallyInvokingActor{func outer(barrier:ThreadBarrier, other:anyMutuallyInvokingActor)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
actorNormalActor:MutuallyInvokingActor{}
/// This `NSModelActor` will deadlock because enqueue calls actor methods synchronously
@NSModelActoractorDeadlockActor: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")letbarrier=ThreadBarrier(threadCount:2)
// Invoke DeadlockActor.outer on both actors in parallel
asyncletresult1= actorA.outer(barrier: barrier, other: actorB)asyncletresult2= 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")letnormalActorA=NormalActor()letnormalAactorB=NormalActor()awaitattemptDeadlock(normalActorA, normalAactorB)
// With ModelActors demonstrate that the program does deadlock
letdescription=NSPersistentStoreDescription()
description.type = NSInMemoryStoreType
letcontainer=NSPersistentContainer(name:"Model")
container.persistentStoreDescriptions =[description]
container.loadPersistentStores(completionHandler:{ _, _ in})print("Running mutually invoking code between two NSModelActors")letdeadlockActorA=DeadlockActor(container: container)letdeadlockAactorB=DeadlockActor(container: container)awaitattemptDeadlock(deadlockActorA, deadlockAactorB)
The text was updated successfully, but these errors were encountered:
Thank you for the information you provided. I have switched back to the call method of perform.
In fact, I hesitated between perform and performAndWait (as can be seen from the previous adjustment records of the code). At present, it seems that my final choice was wrong.
Regarding the details, I will find time to study it carefully.
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, withNSModelActor
theenqueue
method is now synchronous, which means the calling thread cannot continue until the called method is complete. This became true with the move to useperformAndWait
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
The text was updated successfully, but these errors were encountered: