Home > Enterprise >  Is it safe to make a struct containing a closure Sendable?
Is it safe to make a struct containing a closure Sendable?

Time:02-03

I want to have a Sendable struct, which contains a closure. This closure takes in a reference type, but returns Void, therefore the Greeter doesn't directly stores the Person reference. However, closures themselves are still references anyway.

Current code:

class Person {
    let name: String

    init(name: String) {
        self.name = name
    }
}

struct Greeter: Sendable { // <- Trying to make this Sendable
    let greet: (Person) -> Void

    init(greeting: String) {
        greet = { person in
            print("\(greeting), \(person.name)!")
        }
    }
}
let person = Person(name: "George")
let greeter = Greeter(greeting: "Hello")
greeter.greet(person)

// Hello, George!

In my actual problem (this is simplified) I don't actually know Person's implementation and so can't mark it Sendable. It's actually a MTLRenderCommandEncoder, but for simplicity we just have Person.

On the greet definition, I get the following warning:

Stored property 'greet' of 'Sendable'-conforming struct 'Greeter' has non-sendable type '(Person) -> Void'

I can make the warnings go away, but I don't think it's the safe & correct solution:

struct Greeter: Sendable {
    let greet: @Sendable (Person) -> Void

    init(greeting: String) {
        greet = { @Sendable person in
            print("\(greeting), \(person.name)!")
        }
    }
}

How can I be sure this code is safe across threads?

CodePudding user response:

I reason that you can’t. Someone else may have a reference to Person, modify it concurrently and break your assumptions.

But you could create a PersonWrapper: @unchecked Sendable that duplicates Person if there is more than one reference or stores it as a serialized Sendable type. This may be expensive but it will be safe. You may also have to lock if you make changes, and return duplicates instead the real thing.

A trivial example:

public struct SendableURL: Sendable {
    private let absoluteString: String
    public init(_ url: URL) {
        self.absoluteString = url.absoluteString
    }
    public var url: URL {
        URL(string: absoluteString)! 
    }
}

The version that deals with non serializable objects would be:

public final class SendablePerson: @unchecked Sendable {
    private let _person: Person
    private init(_ person: Person) {
        self._person = person
    }
    public static func create(_ person: inout Person) -> SendablePerson? {
        let person = isKnownUniquelyReferenced(&person) ? person : person.copy()
        return SendablePerson(person)
    }
    public func personCopy() -> Person {
        _person.copy()
    }
}

What do you think? I reason that as long as you avoid shared mutable state you should be fine. If you are unable to copy the object you depend on it not being modified.

In practice, we do unsafe things every day (e.g. passing a Data/UIImage, etc.) through threads. The only difference is that SC is more restrictive to avoid data races in all cases, and let the compiler reason about concurrency.

I’m trying to figure out this stuff in the face of ever increasing warnings levels in Xcode, and lack of guidance.

  •  Tags:  
  • Related