# CLI app: readLine and structured concurrency

**URL:** <https://forums.swift.org/t/cli-app-readline-and-structured-concurrency/67330>\
**Category:** Using Swift\
**Tags:** concurrency\
**Created:** [September 17, 2023, 6:47pm UTC](https://forums.swift.org/t/cli-app-readline-and-structured-concurrency/67330 "2023-09-17T18:47:20Z")\
**Posts on this page:** 16\
**Page:** 1

<div class="post-metadata">

**Author:** ![sliemeobn](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/sliemeobn/32/27698_2.png) [@sliemeobn](https://forums.swift.org/u/sliemeobn)\
**Post date:** [September 17, 2023, 6:47pm UTC](https://forums.swift.org/t/cli-app-readline-and-structured-concurrency/67330/1 "2023-09-17T18:47:20Z")

</div>

I am trying to to wrap my head around what the "proper" structure of a CLI app should be that runs async/await stuff but wants to keep an interactive UI (ie: reading keyboard input).  
It kind of boils down to mixing blocking I/O with structured concurrency, but with the spicy extra bit of main-thread-runloop-mainactor-task-deadlock fun.

I could not find any guidance or pre-made solutions on the forums or github, and even vapor's console-kit appears to do the naughty of "blocking a concurrency runtime thread" in their [async example.](https://github.com/vapor/console-kit/blob/9a12000f4064a2bdc49068d7258292ec1bdc88fc/Sources/ConsoleKitAsyncExample/DemoCommand.swift#L33)

While playing around I stumbled over this, which teaches me that I know even less than I thought:  
(macOS ventura, swift 5.8.1)

> **Foo struct with async function**
>
> ```swift
> struct Foo {
> func doStuff() async {
> do {
> print("starting inner loop")
> var i = 1
> while true {
> try await Task.sleep(for: .seconds(1))
> print("Printing \(i)")
> i += 1
> }
> } catch {
> print("inner loop goes brrr")
> }
> }
> }
> 
> ```

If I naively run this, things do not work (as in "do not run concurrently", which I expected):

```swift
@main
enum App {
    static func main() async {
        print("q to exit")

        let run = Task { await Foo().doStuff() }

        while let line = readLine() {
            if line == "q" {
                run.cancel()
                break
            }
        }

        await run.value
    }
}

```

My intuition was that the Task captures the MainActor context, but the `readLine` blocks the main thread, so `doStuff` never gets scheduled.

However, when running this things DO work (ie: run concurrently, while blocking on readLine)

```swift
@main
enum App {
    static func main() async {
        print("q to exit")

        let foo = Foo() //the only change, Foo is initialized outside the task
        let run = Task { await foo.doStuff() }

        while let line = readLine() {
            if line == "q" {
                run.cancel()
                break
            }
        }

        await run.value
    }
}

```

Why is this working? _(If doStuff is a free function it also works)_  
Why is the first one actually not working then?

And on the original assignment:

- Is it ever safe to use `readLine` (without some dispatch/background thread magic to get it off the concurrency runtime) in an async CLI app?
- Are there existing, more sophisticated "read console input" methods that do not block the current thread?
- I could just kick off `doStuff` in a `Task.detached` and keep the blocking on the main loop - is that a good idea?
- I could leave the main "non-async", but the MainActor deadlock remains just the same (see below), and I would need some other hand-made signaling to wait for the background work. Does not sound like a better option, does it?
- I could make an async version of `readLine` that runs "somewhere else" (maybe even a fancy actor with a dispatch queue executor that "hosts" the interactive frontend?) and await it all in a nice TaskGroup on the main task - but that feels quite elaborate (and I do not really know what willy-nilly readLining from background threads does.) I kind of like this one, to be honest... Ideas?

> **(sync main also does not work)**
>
> ```swift
> @main
> enum App {
> static func main() {
> print("q to exit")
> 
> let run = Task { await Foo().doStuff() }
> 
> while let line = readLine() {
> if line == "q" {
> run.cancel()
> break
> }
> }
> }
> }
> 
> ```

---

<div class="post-metadata">

**Author:** ![Jon\_Shier](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/jon_shier/32/921_2.png) [@Jon\_Shier](https://forums.swift.org/u/Jon_Shier)\
**Post date:** [September 17, 2023, 7:01pm UTC](https://forums.swift.org/t/cli-app-readline-and-structured-concurrency/67330/2 "2023-09-17T19:01:35Z")

</div>

How is `Foo` declared? That will determine where `await foo.doStuff()` runs. Remember, the fundamental rule of Swift concurrency is "callee determines context", where "callee" is the awaited function. In this case, barring any other actor context like `@MainActor`, any async work `Foo` does will execute on default executor. So if we walk through that part of your example:

```swift
// On MainActor.
let run = Task { // Task is scheduled on MainActor.
  // On MainActor.
  await foo.doStuff() // doStuff() executes on default executor, Task waits.
  // On MainActor.
}

```

However, none of this explains why your first example fails. We'll have to investigate more for that.

---

<div class="post-metadata">

**Author:** ![Jon\_Shier](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/jon_shier/32/921_2.png) [@Jon\_Shier](https://forums.swift.org/u/Jon_Shier)\
**Post date:** [September 17, 2023, 7:05pm UTC](https://forums.swift.org/t/cli-app-readline-and-structured-concurrency/67330/3 "2023-09-17T19:05:36Z")

</div>

Your first example runs fine for me, though you can see that the `Task` doesn't execute until after you break out of the `while` loop. Even in that case cancellation still works.

---

<div class="post-metadata">

**Author:** ![sliemeobn](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/sliemeobn/32/27698_2.png) [@sliemeobn](https://forums.swift.org/u/sliemeobn)\
**Post date:** [September 17, 2023, 7:05pm UTC](https://forums.swift.org/t/cli-app-readline-and-structured-concurrency/67330/4 "2023-09-17T19:05:47Z")

</div>

The Foo struct is collapsed in the original post (just a plain struct, no funny business)

---

<div class="post-metadata">

**Author:** ![Jon\_Shier](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/jon_shier/32/921_2.png) [@Jon\_Shier](https://forums.swift.org/u/Jon_Shier)\
**Post date:** [September 17, 2023, 7:08pm UTC](https://forums.swift.org/t/cli-app-readline-and-structured-concurrency/67330/5 "2023-09-17T19:08:19Z")

</div>

Yeah, this seems to work fine, it just doesn't run until the `while` loop is complete. Making it a `Task.detached` lets the `Task` start while `readLine` is blocking.

---

<div class="post-metadata">

**Author:** ![sliemeobn](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/sliemeobn/32/27698_2.png) [@sliemeobn](https://forums.swift.org/u/sliemeobn)\
**Post date:** [September 17, 2023, 7:11pm UTC](https://forums.swift.org/t/cli-app-readline-and-structured-concurrency/67330/6 "2023-09-17T19:11:11Z")

</div>

agreed, makes sense, totally expected. but why is the second example running concurrently (without detaching) - that is what surprised me.

---

<div class="post-metadata">

**Author:** ![Jon\_Shier](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/jon_shier/32/921_2.png) [@Jon\_Shier](https://forums.swift.org/u/Jon_Shier)\
**Post date:** [September 17, 2023, 7:13pm UTC](https://forums.swift.org/t/cli-app-readline-and-structured-concurrency/67330/7 "2023-09-17T19:13:13Z")

</div>

I see no difference in behavior for that form, as expected. What are you actually seeing? i.e. What does "work" mean for you there?

---

<div class="post-metadata">

**Author:** ![sliemeobn](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/sliemeobn/32/27698_2.png) [@sliemeobn](https://forums.swift.org/u/sliemeobn)\
**Post date:** [September 17, 2023, 7:26pm UTC](https://forums.swift.org/t/cli-app-readline-and-structured-concurrency/67330/8 "2023-09-17T19:26:13Z")

</div>

good point, let me clarify:

the intention is to run async code (doing whatever) while also keeping an interactive CLI with keyboard input going. so, for this example, it is ultimately about running a task "in the background" while using readLine.

I do not post this because I cannot get it to work, but rather because I find there are a few sharp edges here. if you take your "traditional" CLI app and just "make it async" in a naive way - there be dragons.

so "doesn't work" means "does not execute concurrently" (not there is a bug or anything).

however, I feel that MainActor in CLI apps, "readLine" being blocking, "never-block-concurrency-runtime-tasks", the way that "traditional" CLI apps are made, and the mantra of "do not use detached!" are a spicy mix for developers.

I am super curious how the community thinks these types of apps should best be made.

edit: I edited the original post to better reflect what "works" means for me here...

---

<div class="post-metadata">

**Author:** ![sliemeobn](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/sliemeobn/32/27698_2.png) [@sliemeobn](https://forums.swift.org/u/sliemeobn)\
**Post date:** [September 17, 2023, 7:32pm UTC](https://forums.swift.org/t/cli-app-readline-and-structured-concurrency/67330/9 "2023-09-17T19:32:17Z")

</div>

also, about the "why is that second case working?":

Image you had some app working, a dev refactors the struct assignment and puts it in the task, suddenly everything grinds to a halt - I do not want the be the one debugging this. so I am just super curious what is going on here. I would have hoped that none of the cases would work (ie: run concurrently)... but now it feels like if you are lucky enough to randomly bypass whatever should be deadlocking you can get by without understand how close you are shaving it - one minor change and things are stuck.

---

<div class="post-metadata">

**Author:** ![Jon\_Shier](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/jon_shier/32/921_2.png) [@Jon\_Shier](https://forums.swift.org/u/Jon_Shier)\
**Post date:** [September 17, 2023, 7:32pm UTC](https://forums.swift.org/t/cli-app-readline-and-structured-concurrency/67330/10 "2023-09-17T19:32:38Z")

</div>

`Task.detached` is the proper solution to getting out of a blocked actor context. For CLI apps, depending on your expectation, blocking main may be fine, and that's what many executables do today. But yeah, without proper `async` support for system APIs like `readLine` we have to manually work around things like this. Hopefully we have a full suite of proper solutions by Swift 6.

---

<div class="post-metadata">

**Author:** ![Jon\_Shier](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/jon_shier/32/921_2.png) [@Jon\_Shier](https://forums.swift.org/u/Jon_Shier)\
**Post date:** [September 17, 2023, 7:34pm UTC](https://forums.swift.org/t/cli-app-readline-and-structured-concurrency/67330/11 "2023-09-17T19:34:09Z")

</div>

Your second example changes nothing for me, so I don't know what you mean.

In general, as long as the developer understands the `readLine` is blocking, the rest of the behavior should be perfectly logical.

---

<div class="post-metadata">

**Author:** ![kabiroberai](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/kabiroberai/32/28072_2.png) [@kabiroberai](https://forums.swift.org/u/kabiroberai)\
**Post date:** [September 17, 2023, 7:40pm UTC](https://forums.swift.org/t/cli-app-readline-and-structured-concurrency/67330/12 "2023-09-17T19:40:53Z")

</div>

I haven’t tested this out but you might have luck with `FileHandle.standardInput.bytes.lines`, which is an `AsyncSequence`.

> **[bytes | Apple Developer Documentation](https://developer.apple.com/documentation/foundation/filehandle/3766681-bytes)**
>
> The file’s contents, as an asynchronous sequence of bytes.

I agree that Swift Concurrency as it stands today leaves a lot to be desired in the IO realm; for example, the above API solves reading asynchronously but there’s no FileHandle API for async backpressure-enabled writing, and if you’re dealing with non-POSIX (or Win32) IO APIs you’re still on your own. Also very relevant to this discussion is another concurrent thread (pun intended) here on the Swift Forums:

> [@Blocking I/O and concurrency](https://forums.swift.org/t/blocking-i-o-and-concurrency/67276):
>
> I'm working on using Swift in an embedded device that talks to a peripheral over [SPI](https://en.wikipedia.org/wiki/Serial_Peripheral_Interface), which only has a blocking API on Linux (at least, without writing a kernel driver). SPI supports full duplex which is [accessible](https://www.kernel.org/doc/Documentation/spi/spidev) using ioctl(2) instead of read(2) or write(2), although the latter are also supported for half-duplex operation. The rest of my code is asynchronous, using async/await and dispatch queues depending on its vintage. Ideally I'd like to poll SPI at, say, 30Hz in a thread and exchange …

---

<div class="post-metadata">

**Author:** ![ibex](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/ibex/32/30439_2.png) [@ibex](https://forums.swift.org/u/ibex)\
**Post date:** [September 18, 2023, 12:00am UTC](https://forums.swift.org/t/cli-app-readline-and-structured-concurrency/67330/13 "2023-09-18T00:00:10Z")

</div>

Out of curiosity, I tried the first case; it emitted the following output.

```swift
q to exit
starting inner loop
Printing 1
Printing 2
Printing 3
Printing 4
Printing 5
Printing 6
q
inner loop goes brrr
Program ended with exit code: 0

```

> **Code**
>
> ```swift
> // [https://forums.swift.org/t/cli-app-readline-and-structured-concurrency/67330]
> 
> @main
> enum ScratchPad {
> static func main () async {
> await run ()
> }
> }
> 
> private func run () async {
> print("q to exit")
> 
> let run = Task { await Foo().doStuff() }
> // let run = Task.detached { await Foo().doStuff() }
> 
> while let line = readLine() {
> if line == "q" {
> run.cancel()
> break
> }
> }
> 
> await run.value
> }
> 
> private struct Foo {
> func doStuff() async {
> do {
> print("starting inner loop")
> var i = 1
> while true {
> try await Task.sleep(for: .seconds(1))
> print("Printing \(i)")
> i += 1
> }
> } catch {
> print("inner loop goes brrr")
> }
> }
> }
> 
> ```

---

<div class="post-metadata">

**Author:** ![sliemeobn](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/sliemeobn/32/27698_2.png) [@sliemeobn](https://forums.swift.org/u/sliemeobn)\
**Post date:** [September 18, 2023, 5:59am UTC](https://forums.swift.org/t/cli-app-readline-and-structured-concurrency/67330/14 "2023-09-18T05:59:31Z")

</div>

fascinating... I count three different behaviors then (@Jon_Shier reported both cases behaving the same, but NOT running concurrently...)

how are you running it?  
I used `swift run` in a plain swift package with one executable target, swift 5.8.1.

I'll play around a bit more... the only thing that would remotely make sense to me is that release mode would "optimize" away the bit that run on MainActor an therefore directly short-cutting to the thread pool and debug would not - but that's just a stab in the dark...

edit:  
ok, that might actually be what is happening, both examples "work" in release, but not in debug. adding some non-optimizable thing before `doStuff` (like a print) in the closure makes it get stuck in release as well. \<_crying cat with thumbs up_\>

---

<div class="post-metadata">

**Author:** ![ibex](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/ibex/32/30439_2.png) [@ibex](https://forums.swift.org/u/ibex)\
**Post date:** [September 18, 2023, 11:06am UTC](https://forums.swift.org/t/cli-app-readline-and-structured-concurrency/67330/15 "2023-09-18T11:06:36Z")

</div>

> [@sliemeobn](#):
>
> how are you running it?  
> I used `swift run` in a plain swift package with one executable target, swift 5.8.1.

My build configuration in Xcode is set to Debug. I am running the executable from Xcode and also from the Terminal.

---

<div class="post-metadata">

**Author:** ![johannesweiss](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/johannesweiss/32/38183_2.png) [@johannesweiss](https://forums.swift.org/u/johannesweiss)\
**Post date:** [September 18, 2023, 11:14am UTC](https://forums.swift.org/t/cli-app-readline-and-structured-concurrency/67330/16 "2023-09-18T11:14:23Z")

</div>

> [@kabiroberai](#):
>
> I haven’t tested this out but you might have luck with `FileHandle.standardInput.bytes.lines`, which is an `AsyncSequence`.
> 
> [bytes | Apple Developer Documentation](https://developer.apple.com/documentation/foundation/filehandle/3766681-bytes)

It's an `AsyncSequence` but unfortunately it's not async. It'll also block a thread with synchronous I/O (`read`). To make matters worse, it'll block _exactly one_ thread. So if you, say, tried to read `FileHandle.standardInput.bytes.lines` as well as reading any other `FileHandle.bytes`, you would only get data from both streams if _both_ are readable... (rdar://105881437).
