Skip to content

#120 Ensure concurrent access to AsyncBufferedByteIterator does not crash from dangling pointers #121

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

Merged
merged 1 commit into from
Apr 8, 2022

Conversation

phausler
Copy link
Member

This resolves issue #120 by ensuring the shared buffers are reloaded independently. This does not promise any data consistency across tasks, but does ensure that the iteration can be sent as a move from one task to another.

if !isKnownUniquelyReferenced(&storage) {
// The count is not mutated across invocations so the access is safe.
let capacity = storage.buffer.count
storage = Storage(capacity: capacity)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So - if it doesn't crash, but it is an inconsistent state, perhaps we should always crash instead, by asserting? What do we expect the behavior to actually be if two tasks are using the same iterator?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every single assertion or fatal error is a risk to potentially crash someone's app in the wild. If we always crash then the type is not sendable.

The expected output here is that it is not deterministic on which consumer may get which slice of the bytes but the total bytes of all of the consumers should be the total bytes from the read. For example if you read a file the iterators are going to read all of that file split across N iterations; this should ensure that there is no duplication of reads.

Also we should be REALLY clear about the scenario of how this occurs:

func example(_ bytes: SomeByteSequence) {
  let iterator: AsyncBufferedByteIterator = bytes.makeAsyncIterator()
  Task {
     var iter = iterator 
     while let byte = try await iter.next() { }
  }
  Task {
     var iter = iterator 
     while let byte = try await iter.next() { }
  }
}

That very edge case usage should not prevent the type from being Sendable for other uses like being used with zip or other restricted Sendable algorithms. This fix alleviates that restriction and makes it at least fit the behavior of other AsyncSequence types that are Sendable; consume it in more than one spot and it acts structurally.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In raw technical terms this type is a move-only Sendable type. We don't have move only semantics yet.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, thanks for the additional info.

@parkera parkera added this to the 1.0 milestone Mar 29, 2022
@phausler phausler merged commit 9d03d90 into apple:main Apr 8, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Sendable conformance on _AsyncBytesBuffer.Storage allows unsafe concurrent access
3 participants