skip to Main Content

Marking a class or function as Sendable ensures it is safe to pass across concurrent boundaries, value types are safe as they implement copy on write, etc. we all find in Swift language documentation. But my point is being Sendable does not promise that a variable is free of data races. We can have data races even in basic value types such as Int as demonstrated in code below. So Sendable does not equate to being data safe from concurrent access/modification, it only means the value is safe to copy across different threads. But my question is what problem does it solve or what is the importance of having Sendable as a construct then? Can someone please explain it?

var num = 0

DispatchQueue.global(qos: .background).async {
    for _ in 0..<100 {
        num += 1 
    }
}

DispatchQueue.global(qos: .background).async {
    for _ in 0..<100 {
        num -= 1 
    }
}

2

Answers


  1. As you said, Sendable allows the compiler to reason about whether a value is safe to send across concurrency domains. This is very useful, because you often write code that sends things across concurrency domains. The compiler can check whether your code is safe or not. Without Sendable, the compiler will either need to disallow any sending (overly restrictive), or allow all sendings (not safe).

    From SE-0302:

    Each actor instance and structured concurrency task in a program represents an “island of single threaded-ness”, which makes them a natural synchronization point that holds a bag of mutable state. These perform computation in parallel with other tasks, but we want the vast majority of code in such a system to be synchronization free — building on the logical independence of the actor, and using its mailbox as a synchronization point for its data.

    As such, a key question is: “when and how do we allow data to be transferred between concurrency domains?” Such transfers occur in arguments and results of actor method calls and tasks created by structured concurrency, for example.

    A very simple example to demonstrate how useful Sendable is, is:

    func f(_ x: SomeType) {
        Task {
            print(x)
        }
    }
    

    Is this safe? That depends on whether values of SomeType are safe to send to the top level Task. If it is not safe to send, you might end up with the value of SomeType being shared between the Task and wherever it originally came from. For example, if SomeType is a class with mutable properties, this can cause a race:

    func g() {
        let x = SomeType()
        f(x) // the task that f creates might run concurrently with the next line!
        x.someProperty = "some new value"
    }
    

    If SomeType is Sendable, then the compiler can allow the first code snippet to compile.


    Here is another example from SE-0302:

    actor SomeActor {
      // async functions are usable *within* the actor, so this
      // is ok to declare.
      func doThing(string: NSMutableString) async {}
    }
    
    // ... but they cannot be called by other code not protected
    // by the actor's mailbox:
    func f(a: SomeActor, myString: NSMutableString) async {
      // error: 'NSMutableString' may not be passed across actors;
      //        it does not conform to 'Sendable'
      await a.doThing(string: myString)
    }
    

    doThing is isolated to the actor, and the actor could store the string passed to doThing in one of its properties. And then you end up with a NSMutableString being shared between the actor and whoever originally owns it.

    If we use String instead of NSMutableString, this code is safe because String is Sendable. String is copy-on-write, so the actor modifying it will not affect the whoever owns it originally.

    Login or Signup to reply.
  2. Purpose of Sendable

    • Concurrency Safety: Ensures the type’s values are safe to share or pass between tasks or threads.
    • Compile-Time Checks: The compiler enforces that all stored properties of a Sendable type are themselves Sendable.
    • Prevents Data Races: Prevents unsafe access to shared mutable state across concurrent tasks.

    When to Use Sendable

    • Use Sendable for custom types that you intend to pass to other
      tasks or share across threads.
    • The standard library types like Int, String, and Array (if their elements are Sendable) already conform to Sendable.

    Example: Custom Type Conforming to Sendable

    import Foundation
    
    struct SafeData: Sendable {
        let value: Int // Immutable properties are inherently safe
    }
    
    // Example usage
    func performTask(with data: SafeData) async {
        await Task {
            print("Processing data: (data.value)")
        }.value
    }
    

    Example: Using Sendable with a Class

    For a class to conform to Sendable, all its stored properties must be immutable or themselves conform to Sendable. Also, the class must be marked as final.

    final class SafeClass: Sendable {
        let value: Int // Immutable property ensures thread safety
    
        init(value: Int) {
            self.value = value
        }
    }
    

    If a property is mutable, the compiler will emit an error unless you take explicit measures, like synchronizing access or ensuring exclusivity.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search