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?