Detached task - sendable closure allowing mutation

Hi,

I was watching Protect Mutable State with Actors talk and at 22:42 it is mentioned about the Task.detached accepting a sendable closure which prevents data races

However when I tried to execute the same code I didn't get any compilation error.

Actual Behaviour

  • No compilation errors

Expected Behaviour

  • Compilation Error Mutation of captured var 'counter' in concurrently-executing code

Question

  1. Am I missing something? Or has some API / concept changed?

Environment

  • Xcode 14.0 beta 2 (14A5229c)
  • macOS 13.0 Beta (22A5286j)
  • Xcode Command line project

Code

import Foundation

struct Counter {
    var value = 0
    
    mutating func increment () -> Int {
        value = value + 1
        return value
    }
}

var counter = Counter ()

Task.detached {
    print(counter.increment()) //No compilation error
}

Task.detached {
    print(counter.increment()) //No compilation error
}

RunLoop.main.run()

Observation

  • When counter is defined in a Task { ... } then the warnings are thrown as shown in the comments.
  • Should it be wrapped in a Task { ... } for it to be caught? Is this a bug?
Task {
    var counter = Counter ()

    Task.detached {
        print(counter.increment()) //Mutation of captured var 'counter' in concurrently-executing code; this is an error in Swift 6
    }
    
    Task.detached {
        print(counter.increment()) //Mutation of captured var 'counter' in concurrently-executing code; this is an error in Swift 6
    }
    
    Task {
        print(counter.increment()) //Mutation of captured var 'counter' in concurrently-executing code; this is an error in Swift 6
    }
}

It would be great if someone could help with this, thanks.

I have copied the example from the slides.

Is this something being built and would throw compilation errors in the future versions of swift?

No definitive answer here, but I think that's basically a problem with the counter in your example being a global variable. I further assume you're using a playground, correct?

I think for this race detection to work the compiler needs to know certain things related to the ownership of the counter and global and static properties simply do not get "caught" in this.
I'm not sure if this could/should be fixed in a general way, but at least the WWDC session should have somehow addressed this, I guess. The slides simply look like it would work copied as-is to a playground (where you're basically nudged into using lots of global variables). Maybe it is a bug in the way the compiler checks for this, but perhaps it's also not easy or even possible to do for global and static variables...

I briefly tested this in a SwiftUI project and that basically shows the same behavior. If the counter is constructed outside of the @main struct (i.e. as top-level code) or as a static property of some type the compiler does not catch the race condition. The latter (static property) can even be shown by adapting your example like so:

struct Counter {
    var value = 0
    mutating func increment () -> Int {
        value = value + 1
        return value
    }
}

struct Foo {
    static var counter = Counter()
}

Task {
    var counter = Counter()
    Task.detached {
        print(Foo.counter.increment()) // no error
        print(counter.increment()) // error
    }
    Task.detached {
        print(Foo.counter.increment()) // no error
        print(counter.increment()) // error
    }
}
2 Likes

Thanks a lot @Gero

I was using an Xcode Command Line project.

You were right looks like counter being a global variable had something that caused the compiler not to detect it. I wouldn't have guessed without your suggestion, thanks!

The following code throws the warnings:
Warning: Mutation of captured var 'counter' in concurrently-executing code; this is an error in Swift 6

import Foundation

struct Counter {
    var value = 0
    
    mutating func increment () -> Int {
        value = value + 1
        return value
    }
}

do {
    var counter = Counter ()
    
    Task.detached {
        //This is will be a data race in runtime
        print(counter.increment()) //Warning: Mutation of captured var 'counter' in concurrently-executing code; this is an error in Swift 6
    }
    
    Task.detached {
        //This is will be a data race in runtime
        print(counter.increment()) //Warning: Mutation of captured var 'counter' in concurrently-executing code; this is an error in Swift 6
    }
}

RunLoop.main.run()
1 Like

That's interesting, this was not an idea that came to me when testing, but it makes kind of sense.
I assume ultimately the entire question of whether something leads to a data race like this is tied to the scope of code as well, so above makes sense. Your last snippet basically creates a new scope just like any type instance does, in this case it's just an anonymous one.

Perhaps somebody more familiar with how concurrency is implemented could elaborate further on this here? Is it simply not possible for the compiler to analyze the examples we found that don't give warnings or is this an oversight? And, more importantly, will Swift 6 address this? Playgrounds are one thing, but command line apps are probably more common and important and it would be a shame if race conditions could still slip into people's projects like this...

2 Likes