How to: Pass a `sending` sequence parameter whose elements are `sending`

The issue is related to a sequence type (for example Array, or a variadic parameter T...) whose elements will be passed in as sending, but the sequence itself is not recognised as sending.

The code below demonstrates the issue in a more clear way:

struct Foo<Event> {
    
    func send(_ event: sending Event) {
        // this is fine
    }
    
    func send(events: sending [Event]) {
        for event in events {
            send(event) // ERROR: Sending 'event' risks causing data races
        }
    }
}

The detailed error provided by the complier:

  • 'event' is used after being passed as a 'sending' parameter; Later uses could race.
  • Access can happen concurrently

Technically, iterating over a sequence involves copying the elements.
If I understand sending correctly, this violates the sending requirement and the compiler (must) fail to compile this.

Intuitively, I would have written:

...    
    func send(events: [sending Event]) {
        for event in events {
            send(event)
        }
    }

where sending for the parameter events will be inferred.

However, [sending T] does not compile.

It seems, this is a rather common use case. Is there a solution (other than constraining Event to Sendable) ?

A complete scenario:

Summary
import Testing

struct Foo<Event> {
    
    typealias Stream = AsyncStream<Event>
    
    let stream: Stream
    let continuation: Stream.Continuation
    
    init() {
        (stream, continuation) = Stream.makeStream()
    }
    
    func send(_ event: sending Event) {
        continuation.yield(event)
    }
    
    func send(events: sending [Event]) {
        for event in events {
            send(event) // Error: Sending 'event' risks causing data races
        }
    }
}
struct Test {
    
    class NonSendable {}
    
    @Test
    func testA() async throws {
        let nonSendable = NonSendable()
        let foo = Foo<NonSendable>()
        foo.send(nonSendable)
    }
    
    @Test
    func testB() async throws {
        let nonSendables = [NonSendable(), NonSendable()]
        let foo = Foo<NonSendable>()
        foo.send(events: nonSendables)
    }
}

I believe that this could be because Event isn’t Sendable (as you want), so even if you send a copy, the copy stored in the array could still contain a reference to some shared mutable state. Swift could maybe infer that the copies kept on the original thread are unique (cause the array is sending) and that they are dropped before further use, but I believe it hasn’t been taught to reason about that sort of stuff yet. And I believe if it were taught to do such a thing, it’d probably involve introducing a concept of ‘sending for loops’ that both send the iterator and any elements it produces. That would possibly require a new specialised iterator protocol that produces sending elements or something. The more I think about this, the less trivial it seems :sweat_smile:

I’m not sure of a nice way to convince today’s Swift that the code is safe, but you could at least do the following before sending each event to satisfy Swift in the interim;

nonisolated(unsafe) let event = event

Perhaps a ‘sending map’ helper method on Array could be a useful addition to the standard library? Perhaps there already is one and I missed its introduction?

4 Likes

Thank you for your reply. :)

IMHO it seems, that Swift currently doesn't have a notion, or "complete" notion, of a "sendable Sequence".

If the compiler would have a notion of a "sending sequence", it would treat a type sending T and a sequence sending S, where S.Element is sending, similar.

I could imagine, this can be statically checked. I agree, that this might not be trivial to be implemented in the compiler, though :smile:

So, having a function like this:

    func send(sending events: [sending Event]) {
        for event in events {
            send(event)
        }
    }

would just compile fine.

When intuitively approaching the problem in the above code, I don't see that there can be a potential concurrency problem. When the compiler can see this call site:

   func testB() async throws {
       let nonSendables = [NonSendable(), NonSendable()]
       let foo = Foo<NonSendable>()
       foo.send(events: nonSendables)
   }

It should us happily provide a concurrency-safe binary. :slight_smile:

So, when applying your workaround:

    func send(events: sending [Event]) {
        for event in events {
            nonisolated(unsafe) let event = event
            send(event)
        }
    }

the Swift 6.2 compiler is happy now.

I would guess, there's no potential concurrency flaw, unless someone can point me to one :slight_smile:

I added another test which shows that the compiler will emit an error, if we write unsafe code:

    @Test
    func testB2() async throws {
        let e1 = NonSendable()
        let e2 = NonSendable()
        let nonSendables = [e1, e2]
        let foo = Foo<NonSendable>()
        foo.send(events: nonSendables) // < Error: Sending 'nonSendables' risks causing data races
        e1.value = 1
    }

Yeah I also don’t see any concurrency issues in your particular implementation. That said, the logic I’ve used intuitively to verify that in my head is pretty array-specific. For example, consider an iterator that just returns the same element 5 times, and things start to break. My SendingIterator suggestion from earlier would probably be needed to reason about this pattern for general iterators.

I believe that [sending Int] probably won’t ever be supported, because it’s semantically a bit strange. If you could substitute the sending requirement into any generic type parameter, the underlying code would probably become incorrect in most cases, and if the substitution does succeed, then the methods could’ve all been sending anyway with no harm. E.g. a function designed to return a shared instance of a class of type T would break if someone substituted in sending SomeClass for T.

I’m glad to hear that the workaround worked for now! Perhaps someone will come along with a cleaner solution eventually :)

1 Like

Yeah, a "sending Sequence" is more complex, it's not just a vector of elements.

As a side note, I experience that Swift concurrency can be a challenge. Especially in library code where users can specify types, which are type parameters, which have constraints in the library code. For good ergonomics, APIs should have the least amount of constraints: just let the user throw in its mutable non-sendable object (worst case ever), and the API should still work correctly - provided the complier says it's good, too.

So, I'm happy now to have one more API that works without requiring the type to be sendable :)

1 Like

I was curious about what exactly was going on in this one. First thing I tried was desugaring anything related to the for-in loop (which I've found a helpful trick in debugging these issues). In this case it keeps the error almost the same:

struct Foo<Event> {
    // ...
    func send(iterator: consuming IteratorProtocol<Event>) {
        while let event =  iterator.next() {
            send(event) // ❌ Sending 'event' risks causing data races
        }
    }
}

func testB() async throws {
    let nonSendables = [NonSendable(), NonSendable()]
    var iterator = nonSendables.makeIterator()
    let foo = Foo<NonSendable>()
    foo.send(sendingIterator: iterator)
}

But the error offers a different note this time, which I think is more helpful:

Task-isolated 'event' is passed as a 'sending' parameter; Uses in callee may race with later task-isolated uses

What I believe is happening here is:

  • iterator.next() returns a non-sendable Event, which may still be referenced within the iterator's internal state!
  • You can't send that Event, because uses in the callee (here: send(_ event: Event)) may race uses inside the iterator (which I think is what the new error note is saying).

For example, nothing in the IteratorProtocol requirements prevents you from creating an iterator that always returns the same element, like this:

struct RepeatingIterator<Element>: IteratorProtocol {
    let element: Element
    
    mutating func next() -> Element? {
        return element
    }
}

But if you could send the Element returned by next() away with sending, different isolation contexts / threads could all be operating on the exact same Element. So the compiler is right to stop you here.

You can write today an alternative iterator-like protocol that enforces the key requirement the compiler can't see: that you want the element returned by next() to be sent out of the iterator so it can't be used in there anymore. You do this by using sending:

protocol SendingIteratorProtocol<Element> {
    associatedtype Element
    
    mutating func next() -> sending Self.Element?
}

And if you replace IteratorProtocol<Element> at the beginning with this new protocol, the compiler will notice the possibility of concurrent accesses has been removed, so it's now happy:

func send(sendingIterator: sending SendingIteratorProtocol<Event>) {
    while let event =  sendingIterator.next() {
        send(event) // ✅
    }
}

Though this is not very useful unless you can produce such an iterator-with-sending-next() type to pass to this function, which isn't trivial :sweat_smile: The Array's .makeIterator() method does not work (obviously), as it produces the regular IteratorProtocol type, with non-sending next(), so you'd need to roll your own.

A quick, dirty, and probably wrong implementation of such a type
func testC() async throws {
    let nonSendableArray = [NonSendable(), NonSendable()]
    let nsIterator = SendingIndexingIterator(nonSendableArray) // <-- The iterator with sending 'next()'
    let foo = Foo<NonSendable>()

    foo.send(sendingIterator: nsIterator)
}

// ⚠ Just for demonstration purposes, probably not be a sound implementation
struct SendingIndexingIterator<Element>: SendingIteratorProtocol {
    nonisolated(unsafe) var array: Array<Element> // <-- Should be safe despite 'nonisolated(unsafe)'  because the logic in this type only returns each element once and there are no later accesses.
    var currentIndex: Array.Index?
    
    init(_ array: sending Array<Element>) {
        self.array = array
        self.currentIndex = array.indices.first
    }
    
    mutating func next() -> sending Element? {
        guard let currentIndex else {
            return nil
        }
        guard array.indices.contains(currentIndex) else {
            self.currentIndex = nil
            return nil
        }
        let nextElement = array[currentIndex]
        self.currentIndex = array.index(after: currentIndex)
        return nextElement
    }
}
2 Likes

Yeah that’s essentially the same conclusion I came to when I suggested requiring a sending iterator protocol above.

I reckon in today’s Swift you’d just need to make a sending version of Array’s makeIterator function that returns a SendingIterator (as you’ve outlined). I believe that the implementation of the SendingIterator would have to use nonisolated(unsafe) let to implement next in today’s Swift (as you’ve done), but at least that’s simple to verify as a human, cause the iterator only has next (not previous) and the original array was sent, meaning that no one outside of the iterator’s implementation can access any previous elements.

It’d definitely be worth considering as an addition to the stdlib, or at least a sending map (which would be simpler to implement).

Doesn’t this permit data races though? Even though the array is sending, items in the array can reference each other. If you wanted an array of items which are each disconnected (sending), that would need to be a new data type, not Array.

2 Likes

Yes, you probably need the sequence to not be an array because there's no guarantee —once you have added the elements to an array— that each element is in its own separate region. The Array initializer doesn't take the elements by sending (hence allowing elements to reference each other), effectively merging every element into the same region. So extracting those elements back into separate regions always risks introducing data races.

I think a safe alternative would require building a Sequence-like type where its initializer takes each element sending (to ensure each element comes from its own separate region), and whose iterator-like type also returns the next element by sending it (which in practice would force the sequence to be a one-time-use).

1 Like

Since a sending T is a sub type of T, couldn't a syntax like this:

class NonSendable {}

let sendingArray: [sending NonSendable] = [
    NonSendable(), NonSendable(), 
] 

return such a sendable sequence whose elements are sending, and if this is true, it's iterator would return a sending T?

Ah yep, good catch! Probably requires a new collection type then as Andropov suggested.

Then Array.subscript’s return type would become sending T which wouldn’t be possible to satisfy because subscript keeps around the original element in the array.

Note that your original code is only safe "holistically";

func send(events: sending [Event]) {
    for event in events {
        send(event)
        // After the first iteration, the first event is still in `events`
        // and can still be accessed by this task. We can see that this is
        // safe with nonlocal reasoning (looking at the whole function body).
        //
        // We also know that `Event` doesn't have references to the *other*
        // elements of `events`, but that requires whole-program analysis.
    }
}

You can use Mutex (or roll your own lockless Disconnected type) to prove that a value remains in its own region; unfortunately, Array doesn't yet support noncopyable elements. So to make that work in practice, you'd have to wrap each element in a class. So you have the tools to write struct ArrayWithDisconnectedElements<Element: ~Copyable> (inefficiently, and it can't conform to Collection).

3 Likes

I see, there's a lot of complexity involved for this kind of sending sequence.

In this special use case, I don't need the whole feature set of a sequence. I just need a plain vector of (sending) elements.

So, I though to tackle the problem from a different angle:

struct Stream<Element> {
    func enqueue(_ element: sending Element) {}
}

struct Foo<Event> {
    let stream: Stream<Event> = .init()
    
    func send(_ event: sending Event) {
        stream.enqueue(event)
    }
    
    func send(events: (() -> sending Event)...) {
        for event in events {
            self.send(event())
        }
    }
}

@Test
func testD() async throws {
    class NonSendable {}
    
    let foo = Foo<NonSendable>()
    foo.send(events: { NonSendable() }, { NonSendable() })
}

The code snippet above compiles successfully.

The idea is to use functions.

When Event is a type provided from extern, would this solve all potential issues, such when the elements reference itself?

Caveat
Unfortunately, it's not allowed to use @autoclosure as a variadic parameter
('@autoclosure' must not be used on variadic parameters):

func send(events: (@autoclosure () -> sending Event)...) { 
    for event in events {
        self.send(event())
    }
}

sending, despite being a type attribute, does not work exactly on the type level.

It's not the value that is being sent, it's the entire region containing the value, and possible other values.

The problem is merging of regions. When element is added to the array, region containing the element and region containing the array itself are merged into one. And after that, to my best knowledge, there is no way to disconnect them. Original paper mentioned runtime primitive that checks in the runtime if any two values are connect, and if the check passes, allows to split one region into two. But it is not implemented in Swift, and I'm not sure it can be feasibly implemented. But it might be possible to implement a primitive that allows unsafe disconnection, something like this:

for event in events {
    // hypothetical
    send(unsafeActuallyDisconnected(event))
}

But even now it is possible to box non-sendable value into sendable boxes.

This is quite easy for a non-sendable values in regions connected to an actor:

struct IsolatedBox<T>: @unchecked Sendable {
    private var value: T
    private var isolation: any Actor

    init(_ value: T, isolation: isolated (any Actor) = #isolation) {
        self.value = value
        self.isolation = isolation
    }

    func open(isolation: isolated (any Actor) = #isolation) -> T? {
        if self.isolation === isolation {
            return self.value
        }
        return nil
    }
}

To box a non-sendable value in a disconnected region, we need somehow to record the region. Regions don't exist in the runtime, but if they would, they would be non-copyable values. sending parameters are really just parameters that consume the region value.

Despite regions non-existing in the runtime, we still can wrap non-sendable value in a sendable non-copyable box that would behave like a region:

struct DisconnectedBox<T>: ~Copyable, @unchecked Sendable {
    private var value: T

    init(_ value: sending T) {
        self.value = value
    }

    // NOTE: No public getter for the value
    // This would undermine isolation safety

    mutating func set(_ value: sending T) {
        self.value = value
    }

    consuming func take() -> sending T {
        return self.value
    }
}

class NS {
    var link: NS? = nil
}

nonisolated func test() async {
    var box = DisconnectedBox(NS())
    await useBox(box, another: NS())
}

@MainActor
func useBox(_ box: consuming DisconnectedBox<NS>, another: NS) {
    let x = box.take()
    // Regions: { (x), [@MainActor another] }
    x.link = another
    // Regions: { [@MainActor x, another] }
    let y = box.take() // error: `box` comsumed more than once
    let z = DisconnectedBox(x) // error: Sending `x` risks causing data races
}

Using DisconnectedBox you can reduce your original problem to the problem of using non-copyable value. Likely you will face lack of non-copyable APIs, but that's a different problem.

4 Likes