A type that conforms to Sendable type is a thread-safe type?

Hi,

A type that conforms to the Sendable protocol is a thread-safe type

When we talked about the above description in SE-0430, we thought that it's not correct since the type which conforms to Sendable is not always thread-safe.

For example, if a non-Sendable class which has a Sendable struct(e.g. Array<Int>) is accessed from multiple concurrent contexts at once and the class calls the struct's mutating func, data races could be caused.

We think that Swift just has a system to avoid data races rather than guaranteeing thread safety for types and it's better to change the description.

We are proposing an amendment. Please check it.

Thank you.

2 Likes

That’s by itself a sendability violation though — you’re racing on the class’s reference to the safe type. Such would have been flagged on the racy accesses to the not sendable class type.

1 Like

Thank you for the reply!

What we want to say is that Swift guarantees thread-safety by system, but types that conforms to Sendable is not always thread-safe.

Are we misunderstanding?

I think what confuses you here is that thread-safety of Sendable types and shared mutable state are not entirely same concept when we are talking about sendability. Sendable type always thread-safe, guaranteed by compiler, as it can safely cross concurrency domains boundaries without introducing data races. As an example, you can safely pass Array<Int> from one actor to another without any risk to introduce data races. And such type remains safe by itself.

What you are referring to initially is a shared mutable state, which in your case represented by some non-Sendable type. As you operate at the level of this type, you need to reason about its sendability, not underlying types that are part of this type. This non-Sendable type is a shared mutable state, which is unprotected and being accessed from different concurrency contexts, leading to potential data races. Swift 6 will be treating concurrent access to such type as an error, preventing you from using it in an unsafe way.

// our non-Sendable class which represents shared mutable state
final class SharedState {
    var indexes: [Int] = []
}

@MainActor
func passIndexesToAnotherContext(_ indexes: [Int]) {
}

@MainActor
func passNonSendableToAnotherContext(_ sharedState: SharedState) {
}

nonisolated func main() async {
    let sharedState = SharedState()
    await withDiscardingTaskGroup { group in
        for _ in 0..<10 {
            group.addTask {
                sharedState.indexes.append(0)  // this is an error in Swift 6 mode
            }
        }
    }
    await passIndexesToAnotherContext(sharedState.indexes)  // ok: indexes are Sendable
    await passNonSendableToAnotherContext(sharedState)  // error: SharedState is not Sendable
}

While it is true that types conforming to Sendable are thread-safe under Swift Concurrency, I believe the term “thread-safe type” used in the proposal might be a bit misleading. For beginners learning Swift Concurrency, seeing the phrase “thread-safe type” could lead them to believe that the type itself contains mechanisms to ensure thread safety, similar to Java’s Collections.synchronizedList.

In Swift, just like in the example of Java, a class can also use locks or other mechanisms to ensure thread safety and thus conform to Sendable. However, this is not the case for value types. The reason a structure held by a non-Sendable class is safe is because the compiler guarantees that instances of the class are not accessed concurrently. This means the structure itself does not possess inherent thread safety mechanisms.

Therefore, instead of using the term “thread-safe type,” it might be clearer to explain the concept without implying that the type itself is inherently thread-safe. This would help avoid any potential misunderstandings.

What do you think?

4 Likes

I agree with this.

Swift Concurrency guarantees thread-safety, but type itself doesn't guarantee it. So, I think we can not say “A type conforming to Sendable is a thread-safe type”.

When a beginner learns language, there is no such concept for them of "Java’s Collections.synchronizedList", if ones familiar with that, I'd say they are not beginners, as they have already some understanding of concurrency and thread safety. Yet in that case, direct mapping of concepts between languages have never been unambiguous. Especially, since Java has no notion of value types as Swift has, everything is class and has single semantic.

But Array is thread-safe, as eliminating shared state (by copying of value type in that case) is the way to make it thread-safe. Similar to that, class that is final and has no mutable state in it is thread safe:

final class JustPassing: Sendable {
    let indexes: [Int]
    
    init(indexes: [Int]) {
        self.indexes = indexes
    }
}

Internal protection by actor isolation or locks is just a way to make it thread-safe when type involves mutability. What's not thread safe is a shared mutable state you are introducing.

Consider another case (pretty close to Collections.synchronizedList as it contains mechanisms to ensure thread safety):

actor ProtectedSharedState {
    private(set) var indexes: [Int] = []

    func append(_ newValue: Int) {
        indexes.append(newValue)
    }
}

Now, actors are thead-safe by design, you can mutate Array as shared state in isolation to actor. Nice. Let's add another layer:

class UnsafeSharedState {
    var protected = ProtectedSharedState()  // stored property 'protected' of 'Sendable'-conforming class 'UnsafeSharedState' is mutable; this is an error in the Swift 6 language mode
}

It says it's unsafe to use our actor here! That's odd, but is it? We've introduced another layer of shared state here, so it needs its own protection mechanisms. So, does that mean our actor has become not thread-safe anymore?

Hello, I am a co-author of shiz’s proposal.

I believe it is important to distinguish between whether a type is thread-safe and whether the handling of a type guarantees thread safety.

The concept of whether a type is thread-safe has existed since before the introduction of Swift Concurrency. People have implemented thread-safe types by making member access of classes mutually exclusive using NSLock or DispatchQueue.

Thread safety here means that the type can be accessed simultaneously from multiple threads. This concept is general and not dependent on Swift, as described on Wikipedia:

On the other hand, guaranteeing that the handling of a type is thread-safe is only realized with Swift 6’s Strict Concurrency mode.

However, when a type is Sendable, it means it can be safely sent between concurrent contexts. This property remains the same in both Swift 5 and Swift 6.

For example, consider the following code:

import Foundation

func main() async throws {
    var x: [Int] = [1, 2, 3]

    DispatchQueue.global().async {
        x[0] = 42
        print(x)
    }

    DispatchQueue.global().async {
        x[0] = 99
        print(x)
    }

    try await Task.sleep(for: .seconds(1))
}

try await main()

This code reads and writes x concurrently. In Swift 5, the following warning appears but it can still compile:

Mutation of captured var ‘x’ in concurrently-executing code; this is an error in Swift 6

And, of course, there is a risk of data races, precisely because Array<Int> is not thread-safe. However, in Swift 5, Array<Int> is undoubtedly Sendable.

This example confirms that conforming to Sendable and being thread-safe are separate facts.

In the section we propose to change, it is written as follows:

A type that conforms to the Sendable protocol is a thread-safe type: values of that type can be shared with and used safely from multiple concurrent contexts at once without causing data races.

It says “can be shared with and used safely from multiple concurrent contexts,” which may not be accurate. As demonstrated in the above example, sharing would cause data races.

One way to achieve Sendable is by making the type thread-safe and shareable. However, another way is to make the type non-thread-safe but copyable so it can be sent to another context.

2 Likes

swift-evolution/proposals/0302-concurrent-value-and-concurrent-closures.md at main · swiftlang/swift-evolution · GitHub
The Sendable protocol models types that are allowed to be safely passed across concurrency domains by copying the value.

Why Dictionary crashed on concurrency write? - #2 by John_McCall
As a general rule in Swift, you must not access the same variable (or property, or so on) multiple times concurrently unless all the accesses are reads. Assignments to elements of a dictionary are considered to be modifications of the dictionary as a whole, so your code is in violation of that.

It is a goal of the concurrency feature that we've been working on to diagnose this problem at compile time. If you enable the strict concurrency checking in Swift 5.7, you should already get a diagnostic for this, at least if the dictionary is a local variable; global variables, including top-level variables in playgrounds, will not necessarily get the same diagnostic.

Swift does not currently provide a concurrent dictionary that allows different keys to be safely modified from multiple threads at once.

In this scenario, the race that you'd get would be on the variable of the non-Sendable class, not the array value itself. I don't think the description in this proposal is incorrect, and I do think that programmers should think about types conforming to Sendable is being inherently thread-safe.

9 Likes

I am merely pointing out that the term "thread-safe type" can be misleading, and I am not denying that Sendable types are thread-safe.

By "beginner," I mean those who are new to Swift Concurrency, not beginners in programming or Swift in general. Swift Concurrency is a relatively new concept, and there are many beginners in this area. With the upcoming release of Swift 6, many more people will be getting into Swift Concurrency. For such "beginners," the term "thread-safe type" could lead to the misunderstanding that the type itself contains mechanisms to ensure thread safety. This is because, before Swift Concurrency, that was the common way to make types thread-safe.

In fact, I have seen this kind of misunderstanding occur. I believe it would be better to avoid using potentially misleading terms.

For SE-0430, I think a minimal change like the following may suffice:


- A type that conforms to the Sendable protocol is a thread-safe type: ...

+ A type that conforms to the Sendable protocol is thread-safe: ...

2 Likes

Let’s flip this around. In what way does a Sendable type not contain mechanisms for safe use from multiple threads? When you mark a (copyable) type Sendable, you’re saying that even if a value of that type is used from multiple threads at once, you won’t have any problems—or at least not any problems you couldn’t have on one thread. Isn’t that thread-safe?

So what’s different from Java? Well, that safety for values doesn’t extend to safety for variables in Swift, and (perhaps unfortunately), mutating methods syntactically look like non-mutating methods, so it may not be obvious there’s a difference. Additionally, Java’s garbage collector and near-universal reference semantics allows assignment to be atomic in practice. Sendable doesn’t fix that, and perhaps that’s the thing that needs to be called out more.

4 Likes

Assigning into an ordinary reference variable in Java is not thread-safe. The only guarantees amount to that you will not corrupt the VM; the contents of the object’s fields are in general undefined.

2 Likes

I'm not sure that "thread safety" is a property of types, rather of operations. I believe that was the conclusion of the original ValueSemantics protocol discussion which was part of the Sendable design process.

In that sense, I think of a Sendable type as one whose primary API takes measures to avoid data races (e.g. via internal locking, or copy-on-write, or by being actor-isolated). The presence of a Sendable conformance does not mean the type is forbidden from offering unsafe APIs as well (as long as those are clearly marked).

Or to put it another way, a Sendable type is one that I, as the type author, want to allow developers to pass around wherever Sendable types are required, and that's about as deep as it goes. It's about curation.

3 Likes

That's not a misunderstanding. It's what Sendable means.

The fact that value semantics inherently qualifies as such a mechanism without explicit code doesn't make it any different than other mechanisms to ensure mutual exclusion.

It is never the case with a "thread safe type" T, even if it is a reference type made thread safe by explicitly locking around all access to its state, that this safety somehow makes variables of type T thread safe. It's never safe to race on a mutable variable. Making that thread safe is a matter of the type that contains that variable.

That's why in the example given, it's the lack of Sendable on the class containing the Array that makes it unsafe. If you conform that class to Sendable, the compiler correctly flags the mutable variable as unsafe (regardless of whether the type of the variable is Sendable). There's no such thing as a type that can make such a var safe by internally protecting state, even with locks.

The confusion is probably over Swift's "mutable but not really" structs. The equivalent in Java/Kotlin/C# would be to use a class like ImmutableList whose internal state is final and that only exposes methods to read state, or create new instances with modified state. In that case, any "mutation" of this list would be calling a method that returns a new list, and assigning this return value back to a mutable variable. There is nothing the class could do itself to ensure this is safe. It is never safe to race on variable assignment, so the containing class has to deal with that.

It's also true that such an ImmutableList class in Java has the exact same thread safety guarantees as a, say, AtomicList class with non-final state that is protected by locking. Both guarantee their state is safe to access, the latter by using locks, the former by forbidding mutation, and both can't guarantee it's safe to reassign a variable. Both are superior to List in this way (which isn't safe to pass across threads period).

However, the latter allows "internal" mutations that it can guarantee are thread safe, while the former doesn't. In both cases reassignment of the var to a new instance isn't safe, but this is more serious with the ImmutableList because such reassignment is the only way to change it.

This is a pervasive point of confusion and is quite language independent (novice Java devs will likely hear "immutable types are always thread safe" and assume that means such reassignment must be safe). If anything, the inevitable confusion many devs experience regarding exactly what "thread safe type" means, and could mean, triggers an important lesson in how to think about thread safety, and also how "value" and "reference" semantics applies to variables (i.e. "does a global mutable variable whose type is a pure value type have value or reference semantics?").

If better language is possible, it's not just a matter of what Sendable means in Swift.

12 Likes