Sharing memory (unsafe pointers) between actors

In swift 5.6 the unsafe pointers will no longer be sendable to avoid data races.
In this post @Andrew_Trick points out that "sharing UnsafePointers across actors requires a strategy to avoid shared mutable state."

His gives an example to make it explicit:

// The underlying memory may only be accessed via a single
// UniquePointer instance during its lifetime.
struct UniquePointer<T> : @unchecked Sendable {
  var pointer: UnsafeMutablePointer<T>
}
 // The underlying memory may only be accessed via other instances of
 // SharedPointer during the lifetime of a SharedPointer instance.
 struct SharedPointer<T> : @unchecked Sendable {
   var pointer: UnsafePointer<T>
 }

I do not fully understand the SharedPointer unfortunately.

I have a class that manages its own memory (a bag of bytes).
I want to share this memory across actors.
Each actor may read and/or write to (a copy) of that memory.

To communicate between actors I (could) use this enum:

 enum Container : @unchecked Sendable
 {
   case empty
   case unique(UnsafeMutableRawBufferPointer) //one actor has exclusive access
   case shared(UnsafeRawBufferPointer)        //many actors can read but none can write
 }

Each actor (or whatever structs/classes within its concurrency domain) should go through the container to access the memory.
If it's unique, the actor can do whatever.
It it's shared, the actor first makes a copy (cow principle).

If one actor sends memory to another actor it should first decide what it wants to do.
It could set its own container to empty and just send the unique or shared value onwards.
It has in effect promised not to access the memory anymore. The receiver gets a unique pointer and thus knows it needs to cleanup.
If it's already shared, it just passes it along. The other actor is just another reader.
If it's unique and the actor wants to keep writing to it, a read-only copy should be made and forwarded to the other actor(s). That actor now has a read-only copy.

But this last case is the thing I don't understand.
Actor A makes a copy and sends it actors B and C.

func copy() -> Container
{
      let pointer : UnsafeMutableRawBufferPointer = ...//
      let shared = UnsafeMutableRawBufferPointer.allocate(byteCount: pointer.count, alignment: 1) //must be deallocated manually !!!
      shared.copyBytes(from: pointer)
      return Container.shared(.init(shared)) //now we have a dangling memory pointer
  }

If these actors cease to exist then one of them should deallocate the read-only copy.
Actors B and C can't because the are read-only and have no idea if other readers are out there.
Actor A can't either, I think, because it doesn't know either if there any readers still active.

Ok, so make actor A track all its read-only versions. But there is no guarantee that A will outlive B and C either.

Ok, only send copies around. Yeah, let's try avoid unnecessary copies.

Ok, so don't send just a pointer across. Make it a reference type and deallocate the memory when everyone is done with. But that takes us back to the beginning. A pointer is not sendable thus neither the class. Likewise isKnownUniquelyReferenced is not thread safe eiter afaik.

Did I overlook something in this reasoning?
And euh, what would the experts do with this?

I'm not sure I follow here.

If your data is being modeled by a class, then why does it matter if a pointer is not sendable? There's no (explicit) pointers to send, just the reference to your @sendable object.

In a kind of roundabout way, it is: How am I supposed to implement value-semantics for multi-threaded environment without synchronization? - #3 by michelf

2 Likes

struct SharedPointer : @unchecked Sendable {
var pointer: UnsafePointer
}

The only take away from the proposal quoted above is this: if you take responsibility for synchronizing access on the SharedPointer, then you're entitled to add an @unchecked Sendable annotation. I don't actually know the best way to do that and haven't tried.

It sounds like you want to handle shared memory allocation across actors, and you don't have any guarantee about the lifetime of the actors relative to each other. You can either implement your own atomic reference counts, or rely on the language provided mechanism, which is thread safe in the narrowest sense. The reference counts themselves can be read and written, but you need to check uniqueness before accessing the reference counted object. isKnownUniquelyReferenced should work for you.

1 Like

Oh right, so I just have to wrap the pointer in a reference type.


final class Box : @unchecked Sendable //wrap read-only pointer
{
    let pointer: UnsafeRawBufferPointer
    init(pointer:UnsafeRawBufferPointer){ self.pointer = pointer }
    deinit { pointer.deallocate() }
}

enum Container : @unchecked Sendable
{
    case empty
    case shared(Box)
    case unique(UnsafeMutableRawBufferPointer)
}

That should take care of cleanup, right.

And if a shared pointer needs to become unique I first call isKnownUniquelyReferenced(&aBox). It it returns true then a copy doesn’t even need to be make.

FYI the stdlib has a reference-counted box for raw memory already, in the form of ManagedBuffer. You might be able to save yourself some trouble by inheriting from that and making your subclass @unchecked Sendable.

2 Likes

Thanks for the tip.
I want a fixed-sized buffer though. ManagedBuffer doesn’t support that unfortunately. At least not that I can tell.

In case somebody takes a similar approach: beware that the isKnownUniquelyReferenced(&aBox) doesn't work (swift 5.5.). See this thread.