Thread.current cannot be used from async contexts

Hello everyone. When I try to debug code which uses async/await and use Thread.current or Thread.isMainThread, I get error message "Thread.current cannot be used from async contexts.". What is the point of such errors? I have heard an opinion that this is because Thread.current provides incorrect information when called from structured concurrency code. Is it so? Or is it just an attempt to discourage people from using this API? In other words, does anyone know if Thread.current provides correct information about the current thread?

While I don't know the specific reason for this leading to a diagnostic error, I would personally say there's a high probability that using these methods in an async context is a) pointless and b) dangerous.

The first one, Thread.current would give you a thread that is currently managed by the concurrency runtime. What would you do with that? It's an implementation detail, part of the infrastructure that allows you to call async code the way you're supposed to. Were you to somehow mess with it you would only confuse the runtime, probably eventually corrupting your app.

The second one goes in a similar direction: Thread.isMainThread only returns a boolean, but writing code based on this is more likely to mess with the async runtime than anything else.

My hunch is that you're making conclusions about which actor you're running on, and while at the moment it's probably true that Thread.isMainThread == true implies that you're on the MainActor, this is basically just an implementation detail. The runtime doesn't promise you (AFAIK) that the MainActor uses the main thread exclusively, it just promises you that a) it's a serial executor and b) that you can do stuff on there that is reserved to be run on the main thread. While the latter requires it to run on the main thread, there is a tiny semantic difference.

That being said, if it's just for debugging, you obviously see which thread you're on when hitting a breakpoint. I realize, though, that this might not help with whatever you're trying to achieve, can you maybe elaborate a little?

1 Like

That’s exactly the reason. Swift Concurrency does not make any guarantees about threads, as this completely internal to the implementation, async function between suspension points can be resumed on different threads, based on what’s runtime scheduler will decide. That’s why using thread APIs isn’t allowed — they would just give you the wrong picture of what’s happening. That’s also why mutex APIs also has only lock/unlock disallowed in async contexts — because you can eventually lock on one thread and try to unlock on another.

Do you use the latest version of Swift? I call Thread.isMainThread from a Task in Swift 6.0.2 without any warnings or errors, but see a warning similar to what you describe in Swift 5.9.2 (macOS).

Edit: No warnings about Thread.isMainThread on Windows and Linux; Seems that macOS version of Swift is different in this regard.

Yes, my purpose was debugging. I was trying to understand what was the reason of random crashes happening in the code, and the idea was to check if there are multiple threads changing the same property. And I was wondering whether the threads are really different or it is just an incorrect information provided by Thread.current.

If I understand you right, I can rely on the information from Thread for debugging purposes. So, thank you so much for your explanation, it helped a lot

Thank you. I realise that there are no guarantees, my question is whether Thread.current returns a correct information about the very moment of calling it or not?

swiftlang-6.0.0.9.10

The question is what are you considering as correct information? Swift Concurrency operates on a limited pool of threads created internally by runtime. What information are do you want to get from this?

The fact how we manage threads in the runtime (or in dispatch) does not have relation with the fact of such api being “wrong” and impossible to call.

The workaround is to make a synchronous function, call that from your async function and call that Thread current from there.

As others have said though, this will be of very limited utility because the runtime only really have guarantees in terms of task. So it should be more useful to think about “on what actor am I?” Since the actor isolation then implies where a task runs, or if not isolated then you can indeed check it on some expected executor or not etc. asking about threads does not help much but I can see it sometimes useful to check if you want to observe if you ended up hopping threads or not etc.

4 Likes

Well, yes and no. The API isn't probably outright lying to you in the sense that it would give you the wrong thread. But you cannot rally do anything useful with that information, I am afraid. The concurrency runtime lives "above" threads, so a lot of the deductions you could make from tracking which piece of code runs on which thread may be wrong. @vns gave a perfect example for locks and how you could mess up were it possible to use lock/unlock.

But okay, you're just trying to debug and understand, so you're not destroying anything, but said understanding might be harder than you think: You basically have to take into account all the internal mechanics the runtime uses (and that's, as said, an implementation detail and could change over time, not to mention you'd have to understand all the compiler code...).

A contrived example:

  1. During debugging you observe that a certain piece of code runs on thread A and another, later, on thread B.
  2. Since it runs on two different threads, you deduce that there could be data races, as you don't know which thread is "faster"
  3. Actually this is a wrong deduction, because the concurrency runtime isolates the code pieces to the same actor and thus guarantees to you that the two code pieces run sequentially (but, depending on how it's written, not necessarily in order).

This is, as said, contrived and I don't actually know if such a thing could happen (one actor isolation domain using two different threads). My point is: Nothing in how concurrency is "defined" says anything about how these threads are used, so looking at them without knowing its internals is dangerous.

In practice, some things are obvious (e.g. that anything that is MainActor isolated runs on the main thread), so this can help when debugging, but it's not "court proff", so to speak. :smiley:

Thank you, I absolutely agree that having information about the current thread is not enough to predict how the entire concurrency system works. I was only unsure about that idea which claimed that Thread.current provided wrong information when called from async/await and that is why it has been prohibited to be used

1 Like

If you are using Swift Concurrency, do you have strict concurrency checks on? Another question whether you use unchecked capabilities such as nonisolated(unsafe) or @unchecked Sendable?

I would be happy to give more details about that case, but it was not my code, I was only helping my friend to fix code from his project, so I don't have any idea about the exact settings there. Probably strict checks are not enabled because one object with mutable state could be called from different threads without any warnings

I wonder if the error prompting this discussion stems from this prior discussion:

Pitch: Unavailability from asynchronous contexts

Just to verify, my mental model was that a span of code between suspension points always runs on the same Thread. I concluded that Thread derivatives (.current and local's) would be consistent and correct within that span, but not across suspension points.

But your workaround requiring a refactoring of code suggests that Thread derivatives could be invalid in an async context even within a code span. I can imagine async runtime's where that's true, so perhaps that's the safest assumption (even if it's viable now on some implementations). That would would make it important to identify (implicitly) Thread-dependent API's to avoid in async code.

Is the error message coming from the interactive debugger, the runtime, or the compiler?

I think the question wrt runtime has been addressed in the discussion, but I read your question as usability of Thread.current when issuing debugger commands, and I'm not sure that's been answered. I can seem Thread.current being usable at runtime (at least between suspension points) but incorrect in the debugger (in the sense of not producing the same result that the code itself would).

It is a compilation error, yes.

Do you mean we can rely on its results when called from code and cannot rely on when we manually call it in lldb while being on a breakpoint?

Right, I need this in almost every larger project. Thread.current is just useful for debugging certain issues and there's no real reason why it wouldn't be available.

Filed `Thread.current` is unnecessarily `noasync` · Issue #5139 · swiftlang/swift-corelibs-foundation · GitHub

3 Likes