|
| 1 | +// |
| 2 | +// ------------------------------------------------ |
| 3 | +// Original project: CoreDataEvolution |
| 4 | +// Created on 2024/11/21 by Fatbobman(东坡肘子) |
| 5 | +// X: @fatbobman |
| 6 | +// Mastodon: @fatbobman@mastodon.social |
| 7 | +// GitHub: @fatbobman |
| 8 | +// Blog: https://fatbobman.com |
| 9 | +// ------------------------------------------------ |
| 10 | +// Copyright © 2024-present Fatbobman. All rights reserved. |
| 11 | + |
| 12 | +@preconcurrency import CoreData |
| 13 | +import CoreDataEvolution |
| 14 | +import Foundation |
| 15 | +import Testing |
| 16 | + |
| 17 | +/// A helper class which causes all threads to wait until an expected number have reached |
| 18 | +/// the synchronization point, and then allows all to continue. |
| 19 | +/// This is used to allow us to reliably exercise the race condition to be demonstrated. |
| 20 | +final class ThreadBarrier: @unchecked Sendable { |
| 21 | + private let condition = NSCondition() |
| 22 | + private var threadCount: Int |
| 23 | + private var currentCount = 0 |
| 24 | + |
| 25 | + init(threadCount: Int) { |
| 26 | + self.threadCount = threadCount |
| 27 | + } |
| 28 | + |
| 29 | + func wait() { |
| 30 | + condition.lock() |
| 31 | + defer { condition.unlock() } |
| 32 | + |
| 33 | + currentCount += 1 |
| 34 | + |
| 35 | + if currentCount < threadCount { |
| 36 | + // Wait until all threads reach the barrier |
| 37 | + condition.wait() |
| 38 | + } else { |
| 39 | + // Last thread wakes up all waiting threads |
| 40 | + condition.broadcast() |
| 41 | + } |
| 42 | + } |
| 43 | +} |
| 44 | + |
| 45 | +/// An actor which will oinvoke an inner method on another instance of the |
| 46 | +/// same type. To demonstrate the deadlock we will create two actors which each |
| 47 | +/// enter their own code (taking the performAndWait lock) and then try to call each other. |
| 48 | +protocol MutuallyInvokingActor: Actor {} |
| 49 | +extension MutuallyInvokingActor { |
| 50 | + func outer(barrier: ThreadBarrier, other: any MutuallyInvokingActor) async { |
| 51 | + print("Start Outer") |
| 52 | + barrier.wait() |
| 53 | + print("After Barrier") |
| 54 | + await other.inner() |
| 55 | + print("End Outer") |
| 56 | + } |
| 57 | + |
| 58 | + func inner() { |
| 59 | + print("Inner") |
| 60 | + } |
| 61 | +} |
| 62 | + |
| 63 | +/// This is a normal actor which will not produce a deadlock, because the normal actor |
| 64 | +/// `enqueue` method just queues up a method to call later |
| 65 | +actor NormalActor: MutuallyInvokingActor {} |
| 66 | + |
| 67 | +/// This `NSModelActor` will deadlock because enqueue calls actor methods synchronously |
| 68 | +@NSModelActor |
| 69 | +actor DeadlockActor: MutuallyInvokingActor {} |
| 70 | + |
| 71 | +@Test |
| 72 | +func deadLockTest() async throws { |
| 73 | + /// Run the two actors in parallel to attempt to demonstrate the deadlock |
| 74 | + func attemptDeadlock(_ actorA: MutuallyInvokingActor, _ actorB: MutuallyInvokingActor) async { |
| 75 | + print("Attempting to demonstrate actor deadlock") |
| 76 | + let barrier = ThreadBarrier(threadCount: 2) |
| 77 | + |
| 78 | + // Invoke DeadlockActor.outer on both actors in parallel |
| 79 | + async let result1 = actorA.outer(barrier: barrier, other: actorB) |
| 80 | + async let result2 = actorB.outer(barrier: barrier, other: actorA) |
| 81 | + |
| 82 | + _ = await (result1, result2) |
| 83 | + print("Comlete - actors did not deadlock") |
| 84 | + } |
| 85 | + |
| 86 | + // With normal actors demonstrate the program does not deadlock |
| 87 | + print("Running mutually invoking code between two normal actors") |
| 88 | + let normalActorA = NormalActor() |
| 89 | + let normalAactorB = NormalActor() |
| 90 | + await attemptDeadlock(normalActorA, normalAactorB) |
| 91 | + |
| 92 | + let stack = TestStack() |
| 93 | + let container = stack.container |
| 94 | + |
| 95 | + print("Running mutually invoking code between two NSModelActors") |
| 96 | + let deadlockActorA = DeadlockActor(container: container) |
| 97 | + let deadlockAactorB = DeadlockActor(container: container) |
| 98 | + await attemptDeadlock(deadlockActorA, deadlockAactorB) |
| 99 | +} |
0 commit comments