Strange behavior with "guard let" and "if let" stament in async/await context

Hi all,

I'm rewring my app with new async/await keywords, I found a really strange behavior when "guard let" and "if let" statement is used in an asynchronous context, it cause the @MainActor to stop working, see the demo code here:

import Foundation

@MainActor
class MainActorTester {
   func failedTest() {
       let actor = TestActor()

       Task {
           // Before guard let await, Thread.isMainThread is "true"
           print("Task started ->>> in main thread: \(Thread.isMainThread)")

           guard let result = await actor.result else {
               // After guard let await, Thread.isMainThread is "false"
               print("Task ended ->>> in main thread: \(Thread.isMainThread)")
               return
           }
       }
   }

   func successTest() {
       let actor = TestActor()

       Task {
           print("Task started ->>> in main thread: \(Thread.isMainThread)")

           // Before await, Thread.isMainThread is "true"
           let result = await actor.result

           guard result != nil else {
               // After await, Thread.isMainThread is still "true"
               print("Task ended ->>> in main thread: \(Thread.isMainThread)")
               return
           }
       }
   }
}

actor TestActor {
   var result: Bool? = nil
}

MainActorTester().failedTest()
MainActorTester().successTest()

The success & failed tests is doing exactly same thing(At least in my case...), I expect them both running in Main thread, but the failed test isn't.

in the failed test, I write:

// Before await, we are in main thread
guard let result = await actor.result else {
    // Here, we are not longer in the main thread
}

in the successful test, I write:

// Before await, we are in main thread
let result = await actor.result
guard let result = result else {
    // Here, we are still in the main thread
}

This strange behavior looks like a bug to me, or maybe I break some rules of async/await and actors?

Envorinments:
XCode 13.2
2020 Macbook pro with M1 chip
macOS Monterey 12.1

I create a demo repo for this too, you can download and test it directly:

I'm not sure any Task is guaranteed to run on main thread, what you saw in successTest() might be luck.

I put your code in a command-line program and it exited immediately without running any test code. I then tried to add await in front of 2 test methods but the compiler warned me there was nothing to wait for.

I think when you write Task { /* ... */ }, you're basically creating an async block that can be called later, from anywhere, and transferring it to the system to call at its discretion. @MainActor only guarantees that code before Task is run on the main thread; that does not apply to any further Tasks you create. It so happens that your successTest() runs on main thread entirely; it's not guaranteed.

Since your code did not await those two tasks, they wouldn't get a chance to be scheduled if wrote in a command-line program, which exited as soon as nothing else to run on the main thread.

To illustrate my point, I changed your successTest() to this:

func successTest() async {
    let actor = TestActor()

    async let task1 = Task {
        // Before guard let await, Thread.isMainThread is "true"
        print("Task started ->>> in main thread: \(Thread.isMainThread)")

        guard let result = await actor.result else {
            // After guard let await, Thread.isMainThread is "false"
            print("Task ended ->>> in main thread: \(Thread.isMainThread)")
            return
        }
        print(result)
    }.result

    async let task2 = Task {
        // Before guard let await, Thread.isMainThread is "true"
        print("Task started ->>> in main thread: \(Thread.isMainThread)")

        guard let result = await actor.result else {
            // After guard let await, Thread.isMainThread is "false"
            print("Task ended ->>> in main thread: \(Thread.isMainThread)")
            return
        }
        print(result)
    }.result

    print(await [task1, task2])
}

It runs if awaited in a command-line program but no longer succeeds.

Sorry Ding Yi, you are right, the demo code can't run in command-line program, so I attached a repo in the origin post that wrap the code into a Swfit-UI app, with two buttons you can click to start and wait for the tasks to complete.

Back to your reply, you mention that:

@MainActor only guarantees that code before Task is run on the main thread, that does not apply to any further Tasks you create.

This is somehow confuse me because I'm not using Task.detached, I'm using Task.

Acording to Apple's document: https://developer.apple.com/documentation/swift/task/3856791-init

The task created by Task.init(priority:operation:) inherits the priority and actor context of the caller, so the operation is treated more like an asynchronous extension to the synchronous operation.

So in my understanding, if I create a new non-detached Task inside MainActor, then that task should be guaranteed to run in MainActor's context, which should be the main thread.

Terms of Service

Privacy Policy

Cookie Policy