I'm struggling to get structured concurrency to work and have two questions:
I'm generating a package with "swift package init --type executable" and do a "swift run" which works.
Trying to run with "swift run -Xfrontend -enable-experimental-concurrency" gives me an error:
"error: Missing value for '-c <configuration'"
So question number one: How to correctly add these options?
I managed to get around this with building with swiftc and running the resulting executable.
The synchronous code looks like this:
func doSomeCPUIntensiveStuff() -> Int {
return 33
}
func doSomeStuff() {
var result = Int
result.append(doSomeCPUIntensiveStuff())
result.append(doSomeCPUIntensiveStuff())
print(result)
}
doSomeStuff()
How to get this to work with Task.withGroup, async let or similar? Everything I tried ran into problems like
"error: 'async' in a function that does not support concurrency" or similar.
If you want to use the concurrency support through SwiftPM, you'll need to use
swift run -Xswiftc -Xfrontend -Xswiftc -enable-experimental-concurrency
The meaning of -Xswiftc is "SwiftPM, please hand the following argument through to the Swift compiler without interpreting it". And similarly, -Xfrontend asks the Swift compiler to hand the next argument into the compiler's frontend.
Oh, and to actually run the binaries produces with the experimental concurrency support, you'll need a
func slow() async -> Int { 42 }
func someFunc() async -> Int {
async let x = slow()
async let y = slow()
return await x + y
}
Task.runDetached { // : () async -> T
await someFunc()
}
Sure, since you never waited for anything and the code continues right away and prints y before the detached task even had a chance to run and append.
Yeah the warning is correct -- what you're doing here is not generally thread-safe; you potentially modify the variable from a different task (thread). So... don't do that, and instead what you're trying to do is much simpler:
func slow() async -> Int { return 42 }
struct A {
func run() async {
var y: [Int] = []
let x = await slow()
y.append(x)
print(y)
}
}
let a = A()
Task.runDetached {
await a.run()
}
sleep(10)
I did read (or at least glanced over) all of them. And I'm trying to get something that works. It may be clear for people who had worked on it some time but to a new user it is hard to understand when to use async/task groups/etc.
I have a working application that is basically doing the following:
Generate some read-only global state.
Spawn many threads using said state.
Merge the results from the threads back.
The results are not Int and the merged result is not an Array, the code above is just to teach me how it works.
I have working code using (concurrent) DispatchQueue.async, DispatchGroup.enter and leave, and DispatchSemaphore.wait and signal, and am trying to port it to the new world order.
I have to problems at that point:
Why do I see no print?
I don't want to use sleep. (Trying to use handle.get() results in an "error: 'async' in a function that does not support concurrency" again.)
I absolutely do not mean to come from the perspective of âthis is so clear!â, and Iâm a bit sad you think thatâs how I approached this thread... I honestly want to help out from first principles here, but the reality is that it is still early development of these features, and a lot of stuff is missing - which is what youâre hitting.
Specifically to your point of:
... not wanting to put a sleep there; Today, thereâs no way for it. And thatâs because there are missing features â the missing feature here specifically is that main() will be able to be async and then you wonât run into this trouble. Today though, you need to wait somehow for the detached task to perform itâs work, and thatâs either by some locking/blocking or the simple wait I proposed to you here.
but thatâs missing today AFAIR, thus the sleep workaround.
Please keep in mind that youâre looking at early development versions where everything is in-flight and many things are missing.
â
If youâre happy and confident with semaphoresâas you mentionâyou can do the same from the detached task to avoid the sleep(). As soon as async-main lands (I think it has not landed yet, perhaps Iâm mistaken though and it works already, you can check), the simpler version will just workâ˘.
I understand that and I'm totally happy to wait to try things out. I was under the impression that things are somewhat working now and I'd just give it a try. I thought maybe early feedback from developers is helpful in giving you a different use case that you didn't have in mind.
That's not what I had in mind; I'm totally happy with main() being single threaded. What I'd love to do is:
Single threaded main, setup some things, then...
Spawn lot of threads to do work.
Collect the results and work single threaded from then to the end.
I was surprised by a few things because I thought I could do just:
Spawn work by something like Task.withGroup
Somehow collect the results.
1 doesn't work because Task.withGroup can only be called from async functions,
2 is unclear to me.
This is resurrecting Java's public static void main problem (i.e., that you have to know what types are, what static functions are, etc., just to use concurrency features).
One of Swift's demonstrations of progressive disclosure was that you can write print("Hello, World!") and nothing else, and it's a working program.
I think the main message of this thread--and the numerous other threads about "I understand how to await from an async function, but how do I start?"--is that the async/await features are very lacking in terms of progressive disclosure. Is there no plan to allow top-level await?
I hope so, but itâs up to @Douglas_Gregor et al. Status quo though is that not even the async main exists. I donât know in which proposal this would be part of.
Huh I see, thatâs new and been added during the x-mas break (which is still ongoing, so Iâve not followed the pitch super-closely). So thatâs nice that thatâs being pitched there.
But in any case, for this thread of âget something working nowâ that doesnât really change anything, that does not work yet.
Thanks. I will try again in a few weeks. If I understand correctly it will be possible to do something like "runAsyncAndBlock" from within func/structs/classes, not only from top level.
I tried this countless times with different toolchains and different macOS versions and could never get it to work. I would export DYLD_LIBRARY_PATH=âŚ, but then the DYLD_LIBRARY_PATH variable would not show up in the environment when I ran env.
You probably don't have to disable SIP completely. It should be enough to disable only SIP's debugging restrictions, as described in this comment on an rbenv GitHub issue from 2017: reboot into recovery mode, then type csrutil enable --without debug in Terminal. But I haven't tried that myself.
To clarify, calling swift run from a toolchain directly does not work when SIP is enabled (presumably because swift qualifies as a SIP-protected process):
env DYLD_LIBRARY_PATH=/Library/Developer/Toolchains/swift-DEVELOPMENT-SNAPSHOT-2021-01-10-a.xctoolchain/usr/lib/swift/macosx/ /Library/Developer/Toolchains/swift-DEVELOPMENT-SNAPSHOT-2021-01-10-a.xctoolchain/usr/bin/swift run
But building the binary first and then running the binary directly does work when SIP is enabled:
It may be a good idea to set things up so that in swift.org toolchains, swift run itself would (optionally!) be able to set up the right DYLD_LIBRARY_PATH.
This would probably better be enabled via an explicit command-line option rather than turned on by default though. Stdlib binaries downloaded from swift.org (or compiled manually) generally aren't an exact match for the ones that come with any particular OS version -- so for example, some crucial frameworks may fail to load with dyld errors, or exhibit subtle (or not so subtle!) runtime issues. Sometimes these issues would only start triggering after installing a minor OS update.
(Overriding the OS-provided core Swift libraries is an extremely dangerous power tool. While this is definitely very useful while testing/trying new language features in tiny throwaway projects (or trying out a potential fix for a bug, learning about how Swift works, etc.), the OS is not designed to have one of its core components arbitrarily replaced -- it would be practically impossible to make sure that these custom binaries interoperate perfectly with the rest of the OS. It's a bit like installing a custom build of the Objective-C runtime -- I can't recall ever needing/wanting to do that as a Cocoa developer.
To be clear, we aren't breaking things intentionally, and for the most part simple test projects do work, but even tiny discrepancies in the list of exposed symbols or their behavior can have an outsized impact on interoperability, and the more things the project imports, the higher the chance of triggering something. The Swift stdlib is a living codebase, while OS releases are always tied to a very particular snapshot of it. E.g., consider how Foundation and the Swift stdlib cooperate on implementing bridging.)