TaskLocal loose values when using with RunLoop

I'm trying to implment SerialExecutor based on RunLoop. So I can manage old non GCD models to the actor world. ( such as CoreLocation )

What I've found is that sometimes. my RunLoop thead jumps to the strange stack point and lose all of my TaskLocals.

I have my thread's main like below. Which I expect TaskLocal is always available inside my runLoop.

    // This is the main of runloop thread
    //ExecutorContext contains JobQueue and RunLoopSource
    static func controlRunLoop(context:ExecutorContext) {
        guard Self.myLocal == nil,
              RunLoop.current.currentMode == nil,
              RunLoop.current != RunLoop.main
        else { return }
        defer {
            while true {
                let (exchanged, _ ) = context.queue.sanityCheck.compareExchange(expected: false, desired: true, ordering: .releasing)
                if exchanged {
                    break
                }
            }
            // last safety check
            Self.$myLocal.withValue(context) {
                Self.processEvents()
            }
        }
        Self.$myLocal.withValue(context) {
            CFRunLoopAddSource(CFRunLoopGetCurrent(), context.source, .commonModes)
            while CFRunLoopSourceIsValid(context.source) {
                let passed = RunLoop.current.run(mode: .default, before: .distantFuture)
                if !passed {
                    break
                }
            }
        }

    }

However when I actaully use this as a SerialExecutor. where actual Task exist, my TaskLocal is loosed.

This is the stack when I access my RunLoop from traditional RunLoop API

Thread 10#0	0x00000001039ccb08 in closure #1 in static RunLoopPriorityExecutor.controlRunLoop(context:) at /Users/bagbyeong-gwan/IDE/XcodeWorkSpace/Tetra/Sources/CriticalSection/PriorityRunLoop.swift:308
#1	0x00000001039cf968 in partial apply for closure #1 in static RunLoopPriorityExecutor.controlRunLoop(context:) ()
#2	0x0000000265252264 in TaskLocal.withValue<τ_0_0>(_:operation:file:line:) ()
#3	0x00000001039cc780 in static RunLoopPriorityExecutor.controlRunLoop(context:) at /Users/bagbyeong-gwan/IDE/XcodeWorkSpace/Tetra/Sources/CriticalSection/PriorityRunLoop.swift:305
#4	0x00000001039ce358 in executeRunloop(setupHandle:) at /Users/bagbyeong-gwan/IDE/XcodeWorkSpace/Tetra/Sources/CriticalSection/PriorityRunLoop.swift:478
#5	0x000000010392c090 in closure #1 in closure #1 in RunLoopActor.init(qos:) at /Users/bagbyeong-gwan/IDE/XcodeWorkSpace/Tetra/Tests/TetraTests/TetraTests.swift:166
#6	0x000000010392d1fc in thunk for @escaping @callee_guaranteed @Sendable () -> () ()
#7	0x000000018e17ede8 in __NSThread__start__ ()
#8	0x000000018cee02e4 in _pthread_start ()

And This is the stacktrace, when my RunLoop is servicing the actor. Strange ... Same Thread but different StackTrace.

Thread 10#0	0x000000010392d33c in RunLoopActor.spawn2() at /Users/bagbyeong-gwan/IDE/XcodeWorkSpace/Tetra/Tests/TetraTests/TetraTests.swift:231
#1	0x000000010392a610 in TetraTests.sfasdfasdfasdfasf() at /Users/bagbyeong-gwan/IDE/XcodeWorkSpace/Tetra/Tests/TetraTests/TetraTests.swift:36
#2	0x000000010392a2cc in static TetraTests.$s10TetraTestsAAV17sfasdfasdfasdfasf4TestfMp_28funcsfasdfasdfasdfasf__asyncfMu0_@Sendable () at /Users/bagbyeong-gwan/IDE/XcodeWorkSpace/Tetra/.swiftpm/xcode/@__swiftmacro_10TetraTestsAAV17sfasdfasdfasdfasf4TestfMp_.swift:4
#3	0x000000010392a9e0 in implicit closure #1 in static TetraTests.$s10TetraTestsAAV17sfasdfasdfasdfasf4TestfMp_58__🟠$test_container__function__funcsfasdfasdfasdfasf__asyncfMu_.__tests.getter at /Users/bagbyeong-gwan/IDE/XcodeWorkSpace/Tetra/.swiftpm/xcode/@__swiftmacro_10TetraTestsAAV17sfasdfasdfasdfasf4TestfMp_.swift:18
#4	0x00000001033c393c in ___lldb_unnamed_symbol5952 ()
#5	0x00000001032ff978 in ___lldb_unnamed_symbol2777 ()
#6	0x00000001032feaa8 in ___lldb_unnamed_symbol2748 ()
#7	0x00000001033c2e40 in ___lldb_unnamed_symbol5947 ()
#8	0x00000001032ff978 in ___lldb_unnamed_symbol2777 ()
#9	0x0000000265251f68 in TaskLocal.withValueImpl<τ_0_0>(_:operation:file:line:) ()
#10	0x0000000265225db0 in TaskLocal.withValue<τ_0_0>(_:operation:file:line:) ()
#11	0x00000001033babf8 in ___lldb_unnamed_symbol5859 ()
#12	0x00000001033c28d0 in ___lldb_unnamed_symbol5942 ()
#13	0x00000001033c235c in ___lldb_unnamed_symbol5938 ()
#14	0x00000001032ff978 in ___lldb_unnamed_symbol2777 ()
#15	0x00000001033be924 in ___lldb_unnamed_symbol5903 ()
#16	0x00000001033c1b1c in ___lldb_unnamed_symbol5928 ()
#17	0x00000001033c1274 in ___lldb_unnamed_symbol5919 ()
#18	0x00000001032ff978 in ___lldb_unnamed_symbol2777 ()
#19	0x00000001032feaa8 in ___lldb_unnamed_symbol2748 ()
#20	0x00000001033c0ccc in ___lldb_unnamed_symbol5916 ()
#21	0x00000001032ff978 in ___lldb_unnamed_symbol2777 ()
#22	0x0000000265251f68 in TaskLocal.withValueImpl<τ_0_0>(_:operation:file:line:) ()
#23	0x0000000265225db0 in TaskLocal.withValue<τ_0_0>(_:operation:file:line:) ()
#24	0x00000001033ba2c0 in ___lldb_unnamed_symbol5852 ()
#25	0x00000001033bfe64 in ___lldb_unnamed_symbol5908 ()
#26	0x00000001033c1f1c in ___lldb_unnamed_symbol5933 ()
#27	0x00000001032ff978 in ___lldb_unnamed_symbol2777 ()
#28	0x00000001033bd48c in ___lldb_unnamed_symbol5891 ()
#29	0x00000001033c02f0 in ___lldb_unnamed_symbol5910 ()
#30	0x00000001033c1f1c in ___lldb_unnamed_symbol5933 ()
#31	0x00000001032ff978 in ___lldb_unnamed_symbol2777 ()
#32	0x00000001033bd48c in ___lldb_unnamed_symbol5891 ()
#33	0x00000001033c02f0 in ___lldb_unnamed_symbol5910 ()
#34	0x00000001033c1f1c in ___lldb_unnamed_symbol5933 ()
#35	0x00000001032ff978 in ___lldb_unnamed_symbol2777 ()
#36	0x00000001033bd48c in ___lldb_unnamed_symbol5891 ()
#37	0x00000001033c02f0 in ___lldb_unnamed_symbol5910 ()
#38	0x00000001033c1f1c in ___lldb_unnamed_symbol5933 ()
#39	0x00000001032ff978 in ___lldb_unnamed_symbol2777 ()
#40	0x00000001033bd48c in ___lldb_unnamed_symbol5891 ()
#41	0x00000001033c02f0 in ___lldb_unnamed_symbol5910 ()
#42	0x00000001033c6390 in ___lldb_unnamed_symbol5980 ()
#43	0x00000001032ff978 in ___lldb_unnamed_symbol2777 ()
#44	0x000000010336e9b8 in ___lldb_unnamed_symbol4919 ()
#45	0x00000001032ff978 in ___lldb_unnamed_symbol2777 ()

Here is the code that can reproduce this behavior link

Strange, normal TaskLocal using does works as expected

@TaskLocal let c1 = 0

enum MainEntry1 {
    
    static func main() async throws {
        $c1.withValue(10) {
            Task {
                // 10
                print(c1)
            }
        }
    }
    
}


enum MainEntry2 {
    
    static func main() throws {
        RunLoop.main.perform {
            Task{ @MainActor in
                //10
                print(c1)
            }
        }
        $c1.withValue(10) {
            while RunLoop.main.run(mode: .default, before: .distantFuture) {
                
            }
        }
    }
    
}

But this code loose taskLocal

@main
enum MainEntry {
    
    static func main() async throws {
        let myActor = await RunLoopActor()
        let block = { (act: isolated RunLoopActor) in
            let _:Void = await withUnsafeContinuation { continuation in
                act.assertIsolated()
                RunLoop.current.perform {
                    // print true, we have taskLocal!
                    print(RunLoopPriorityExecutor.myLocal != nil)
                    act.assertIsolated()
                    continuation.resume()
                }
            }
        }
        await block(myActor)
        let block2 = { (act: isolated RunLoopActor) in
            // nil missing taskLocal
            print(RunLoopPriorityExecutor.myLocal != nil)
            act.assertIsolated()
        }
        await block2(myActor)
    }
    
}

@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)

actor RunLoopActor {
    
    let executor:RunLoopPriorityExecutor
    
    init() async {
        let ref:RunLoopPriorityExecutor = await withUnsafeContinuation { continuation in
            let th = Thread{
                executeRunloop {
                    continuation.resume(returning: $0)
                }
            }
            th.qualityOfService = .default
            th.start()
        }
        self.executor = ref
    }
    
    nonisolated var unownedExecutor: UnownedSerialExecutor {
        executor.asUnownedSerialExecutor()
    }
    
    
}

Now I've find the reason.
I'm explaining to myself.

ExecutorJob or UnownedJob is often created outside of executor. And rarely created inside of Executor. This what the stack trace is saying.

Since the Builtin.Job is created outside from outer task-local-value scope. TaskLocal is pointing to the defaultValue.

So this result is intended

1 Like