You can show this with the following program. The program creates two pipes (pipe1 & pipe2). It never writes into pipe1 but it does write to pipe2. Then it triggers a child task which reads from pipe1 and the main task read from pipe2.
The expected behaviour is: The read from pipe2 succeeds immediately and our program returns.
What happens in reality is that FileHandle.bytes (due to the aforementioned implementation problem) starts a single blocking read from pipe1 (which will never return of course, because there's nothing to read) and the would-be-successful read from pipe2 never even gets started -> deadlock.
import Foundation
func go() async throws {
let pipe1 = Pipe()
let pipe2 = Pipe()
var it2 = pipe2.fileHandleForReading.bytes.makeAsyncIterator()
async let _ = {
// child task that reads from pipe1 (which we never write into)
// so it'll just sit there
var it1 = pipe1.fileHandleForReading.bytes.makeAsyncIterator()
let chunk1 = try await it1.next() // will never complete
print("weird, unexpectedly read from pipe1", chunk1.debugDescription)
return chunk1
}()
try await Task.sleep(nanoseconds: 500_000_000) // some time for it1 to start
pipe2.fileHandleForWriting.write(Data("hello\n".utf8))
print("written into pipe2, waiting for bytes to be readable (this should be instant...")
fflush(stdout)
let chunk2 = try await it2.next() // should immediately complete
print("read from pipe2 (expected to work instantly)", chunk2.debugDescription)
// returning from this function Should cancel chunk1's child task
}
try await go()
actual output:
$ swiftc -o AsyncBytesRead test.swift && ./AsyncBytesRead
written into pipe2, waiting for bytes to be readable (this should be instant...
[... hang forever ...]
blocking read:
We can check what's going on if we sample the program by running sample AsyncBytesRead which contains
8628 Thread_1558316
8628 completeTask(swift::AsyncContext*, swift::SwiftError*) (in libswift_Concurrency.dylib) + 1 [0x271f767bd]
8628 partial apply for implicit closure #1 in go() (in AsyncBytesRead) + 1 [0x1023a9d7d]
8628 implicit closure #1 in go() (in AsyncBytesRead) + 1 [0x1023a9bc5]
8628 closure #1 in implicit closure #1 in go() (in AsyncBytesRead) + 1 [0x1023aa0fd]
8628 NSFileHandle.AsyncBytes.Iterator.next() (in Foundation) + 1 [0x18b4db1ad]
8628 _AsyncBytesBuffer.reloadBufferAndNext() (in Foundation) + 1 [0x18b4d9805]
8628 partial apply for closure #1 in NSFileHandle.AsyncBytes.Iterator.init(file:) (in Foundation) + 1 [0x18b4da665]
8628 closure #1 in NSFileHandle.AsyncBytes.Iterator.init(file:) (in Foundation) + 76 [0x18b4da090]
8628 read (in libsystem_kernel.dylib) + 8 [0x18969a7dc] <<<--- BLOCKING READ
and swift inspect dump-concurrency AsyncBytesRead can show that we indeed have the expected two tasks (the main task and the async let child task) and Foundation.IOActor.
$ sudo swift inspect dump-concurrency AsyncBytesRead
TASKS
Task 0x1 - flags=future|enqueued enqueuePriority=0x15 maxPriority=0x0 address=0x12c804ff0
async backtrace: partial apply for closure #1 in NSFileHandle.AsyncBytes.Iterator.init(file:)
_AsyncBytesBuffer.reloadBufferAndNext()
NSFileHandle.AsyncBytes.Iterator.next()
go()
async_MainTQ0_
thunk for @escaping @convention(thin) @async () -> ()
partial apply for thunk for @escaping @convention(thin) @async () -> ()
completeTaskWithClosure(swift::AsyncContext*, swift::SwiftError*)
resume function: closure #1 in NSFileHandle.AsyncBytes.Iterator.init(file:) in Foundation
task allocator: 3048 bytes in 6 chunks
* 1 child task
`--Task 0x2 - flags=childTask|future|asyncLetTask|running enqueuePriority=0x15 maxPriority=0x0 address=0x12d009a90
current task on thread 0x17c72c
parent: 0x12c804ff0
waiting on thread: port=0x2003 id=0x17c721
resume function: closure #1 in NSFileHandle.AsyncBytes.Iterator.init(file:) in Foundation
task allocator: 1272 bytes in 4 chunks
ACTORS
0x600002f9c0e0 Foundation.IOActor state=idle flags=0 maxPriority=0x0
no jobs queued
THREADS
Thread 0x17c72c - current task: 0x2