-
Notifications
You must be signed in to change notification settings - Fork 28
Introduce AsyncIO for Windows and Linux #117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
b6181ea
e374669
61508ad
a72d35f
74995e4
44a9e97
22e4b38
ba8c6c8
9c6703f
6f030af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -118,11 +118,7 @@ final class AsyncIO: Sendable { | |
shutdownFileDescriptor: shutdownFileDescriptor | ||
) | ||
let threadContext = Unmanaged.passRetained(context) | ||
#if os(FreeBSD) || os(OpenBSD) | ||
var thread: pthread_t? = nil | ||
#else | ||
var thread: pthread_t = pthread_t() | ||
#endif | ||
rc = pthread_create(&thread, nil, { args in | ||
func reportError(_ error: SubprocessError) { | ||
_registration.withLock { store in | ||
|
@@ -175,11 +171,13 @@ final class AsyncIO: Sendable { | |
} | ||
|
||
// Notify the continuation | ||
_registration.withLock { store in | ||
let continuation = _registration.withLock { store -> SignalStream.Continuation? in | ||
if let continuation = store[targetFileDescriptor] { | ||
continuation.yield(true) | ||
return continuation | ||
} | ||
return nil | ||
} | ||
continuation?.yield(true) | ||
} | ||
} | ||
|
||
|
@@ -194,16 +192,10 @@ final class AsyncIO: Sendable { | |
return | ||
} | ||
|
||
#if os(FreeBSD) || os(OpenBSD) | ||
let monitorThread = thread! | ||
#else | ||
let monitorThread = thread | ||
#endif | ||
|
||
let state = State( | ||
epollFileDescriptor: epollFileDescriptor, | ||
shutdownFileDescriptor: shutdownFileDescriptor, | ||
monitorThread: monitorThread | ||
monitorThread: thread | ||
) | ||
self.state = .success(state) | ||
|
||
|
@@ -222,6 +214,8 @@ final class AsyncIO: Sendable { | |
_ = _SubprocessCShims.write(currentState.shutdownFileDescriptor, &one, MemoryLayout<UInt64>.stride) | ||
// Cleanup the monitor thread | ||
pthread_join(currentState.monitorThread, nil) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this will go badly wrong if we do this more than once. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
close(currentState.epollFileDescriptor) | ||
iCharlesHu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
close(currentState.shutdownFileDescriptor) | ||
} | ||
|
||
|
||
|
@@ -394,7 +388,7 @@ extension AsyncIO { | |
resultBuffer.removeLast(resultBuffer.count - readLength) | ||
return resultBuffer | ||
} else { | ||
if errno == EAGAIN || errno == EWOULDBLOCK { | ||
if self.shouldWaitForNextSignal(with: errno) { | ||
// No more data for now wait for the next signal | ||
break | ||
} else { | ||
|
@@ -443,7 +437,7 @@ extension AsyncIO { | |
return writtenLength | ||
} | ||
} else { | ||
if errno == EAGAIN || errno == EWOULDBLOCK { | ||
if self.shouldWaitForNextSignal(with: errno) { | ||
// No more data for now wait for the next signal | ||
break | ||
} else { | ||
|
@@ -486,7 +480,7 @@ extension AsyncIO { | |
return writtenLength | ||
} | ||
} else { | ||
if errno == EAGAIN || errno == EWOULDBLOCK { | ||
if self.shouldWaitForNextSignal(with: errno) { | ||
// No more data for now wait for the next signal | ||
break | ||
} else { | ||
|
@@ -500,6 +494,11 @@ extension AsyncIO { | |
return 0 | ||
} | ||
#endif | ||
|
||
@inline(__always) | ||
private func shouldWaitForNextSignal(with error: CInt) -> Bool { | ||
return error == EAGAIN || error == EWOULDBLOCK || error == EINTR | ||
} | ||
} | ||
|
||
extension Array : AsyncIO._ContiguousBytes where Element == UInt8 {} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
threading bug here, you can't capture a mutable
buffer
. Why does the compiler not error here?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not? Are you suggesting we have to use Mutex here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First, note that this package is still building in Swift 5 language mode (we have not updated the build system's default, even when using a Swift 6 toolchain and tools version), so that can be a cause of missing diagnostics. In this case, it's because of the many missing annotations throughout Dispatch, in particular the read handler is not marked @sendable even though it should be based on its semantics.
I think technically this is safe because this closure is guaranteed to be called on the same serial queue and is not otherwise accessed while those blocks are executing, so there is no actual concurrent access. That said, we should express this in a way that the compiler can guarantee as safe, especially if these DispatchIO annotations get corrected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd suggest you stick @sendable on the handler here, similar to what I mentioned in #117 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hang on,
swift-tools-version:6.0
will make it-swift-version 6
(unlike just invokingswiftc
on 6.0+). Anyway, we should be in language mode 6.it's on
.global()
which is concurrent.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I switched to a dedicated serial queue.
I agree with @jakepetroules that this is safe because all calls are made on the same serial queue. I could mark the closure as
@Sendable
but that would require us to actually use an unnecessary lock. This is analogues to the old days ofactor
approach where you just basically use a serial queue to make sure there's no concurrent access.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, you're right, I might've been thinking of an old bug.
At the same time, it's a known issue that the closure is not marked @sendable. Fixing that bug WILL break the build of Swift Subprocess.
So I suggest writing the read method like this, which is compliant with full Sendable checking (without any Mutex) and insulates us against a potential fix there:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AsyncStream
uses a lock internally. This is a hot code path and it's continuously called when streaming. We really shouldn't put a lock here unless we are sure it is absolutely necessary. IMO, this is the same~Copyable
optional hack we use everywhere. The compiler isn't able to reason that even though we are sending this value to another execution context, it's done sequentially with no overlapping.If the concern is that when
Dispatch
adds the@Sendable
annotation, Subprocess will break, I'd rather figure out a (@unchecked Sendable
) solution then than prematurely optimize it now.Of course, I could be wrong about this being safe so I'm definitely all ears. I just don't want to change our approach simply because the compiler isn't able to reason our specific use case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can see your argument for performance and agree
@unchecked Sendable
might be a good compromise for now (unless @weissi or @FranzBusch knows something we don't w.r.t. memory safety) - would you mind throwing that annotation on the closure with a note about why it's there? That way we're insulated against future Dispatch changes and it's also explicit that we're knowingly bypassing compiler checking.