swiftconcurrency

How to express ownership release in Swift 6 for pipeline of process


I have a heavily multi-threaded app that runs perfectly in Swift 5. I want to switch to Swift 6, and I got very few errors, but there is still this one I get and can't get rid of.

The code is transmitting a very complex data structure through a pipeline of processes, like this:

//  Can't be Sendable because of the need for mutable properties that will be modified
//  by Process1, Process2 etc...
final class ComplexData  {
    var name: String
    init(name: String) {
        self.name = name
    }
}

func Process1(lotsOfData: [ComplexData]) {
    for data in lotsOfData {
        //  Do complex things with complex data, and then give data to another process
        Task.detached {
            Process2(data: data)
        }
        //  I will never ever do anything more with data
    }
}

func Process2(data: ComplexData) {
    //  Do complex things with complex data, and then give data to another process
    Task.detached {
        Process3(data: data)
    }
    //  I will never ever do anything more with data
}

func Process3(data: ComplexData) {
    //  Do complex things with complex data, and I am done
}

I get error : Task-isolated value of type '() async -> ()' passed as a strongly transferred parameter; later accesses could race at each Task.detached line.

There is no data race because each ComplexData is accessed by one and only one Process at the same time.

In Rust this "access-time-exclusion" would be solved with "transfer of ownership". How to solve that in proper Swift 6 ?


Solution

  • I think the real answer to my question is in fact "don't try to map Swift Concurrency to the old thread management way".

    Because of history, the current code is balancing the work over a number of threads and cores, when Swift Concurrency approach is to abstract the coder from that.

    I encourage anyone to read and understand the Swift concepts of Isolation and Tasks, which, IMHO, are not that well explained in the official documentation (and in number of tutorials I found). This one helped me: https://www.massicotte.org/intro-to-isolation).

    Because in my current code, I know for sure that there is no data-race problem, as thread are working on data, giving the result to another thread and then forgetting about it, I finally ended up doing the following:

    I didn't wanted to mark complexData as @unchecked Sendable directly, so I used the following trick:

    public struct PromiseToForget<P>: @unchecked Sendable {
        public let payload: P
        
        public init(_ p: P) {
            self.payload = p
        }
    }
    

    and I send this PromiseToForget from thread to thread.

    I fully acknowledge this is no better, and doesn't help the compiler to help me, but at least, it reminds me that I must forget the payload once transferred. I will have to make a deep refactor to make better use of Swift Concurrency.

    Finally, I would like to thank @Rob for spending some time to explain me a few things. Well appreciated.