0

I am trying to implement upload mechanism for my application. However, I have a concurrency issue I couldn't resolve. I sent my requests using async/await with following code. In my application UploadService is creating every time an event is fired from some part of my code. As an example I creation of my UploadService in a for loop. The problem is if I do not use NSLock backend service is called multiple times (5 in this case because of loop). But if I use NSLock it never reaches the .success or .failure part because of deadlock I think. Could someone help me how to achieve without firing upload service multiple times and reaching success part of my request.

final class UploadService {
    /// If I use NSLock in the commented lines it never reaches to switch result so can't do anything in success or error part.
    static let locker = NSLock()

    init() {
        Task {
            await uploadData()
        }
    }

    func uploadData() async {
    //    Self.locker.lock()

        let context = PersistentContainer.shared.newBackgroundContext()
        // It fetches data from core data to send it in my request
        guard let uploadedThing = Upload.coreDataFetch(in: context) else {
            return
        }

        let request = UploadService(configuration: networkConfiguration)
        let result = await request.uploadList(uploadedThing)

        switch result {
        case .success:
            print("success")
        case .failure(let error as NSError):
            print("error happened")
        }

    //    Self.locker.unlock()
    }
}

class UploadExtension {
    func createUploadService() {
        for i in 0...4 {
            let uploadService = UploadService()
        }
    }
}
atalayasa
  • 3,310
  • 25
  • 42
  • Welcome to SO - Please take the [tour](https://stackoverflow.com/tour) and read [How to Ask](https://stackoverflow.com/help/how-to-ask) to improve, edit and format your questions. Without a [Minimal Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example) it is impossible to help you troubleshoot. You have to find what is recreating the service if you only want it to run once. – lorem ipsum Nov 08 '22 at 14:02
  • Does it matter? In my case it can be called multiple times. I think the question and what I am trying to achieve are clear. I just tried to show similar case. – atalayasa Nov 08 '22 at 16:01
  • I suspect it is unrelated to your question at hand, but `let request = UploadService(configuration: …)` seems like it must be a typo. Why would an upload service instance method create another upload service instance (and this time, using a `configuration` parameter that doesn’t exist in the implementation you shared with us). – Rob Nov 08 '22 at 16:40
  • 1
    Also unrelated, but it would appear that `uploadList` is returning a `Result` type. That is a bit of an anti-pattern in Swift concurrency. Just define it as an `async` method that `throws`. – Rob Nov 08 '22 at 16:54
  • Yep you are right it was my mistake thanks! Also thank you for your great recommendation. Throwing an error makes more sense. – atalayasa Nov 09 '22 at 09:00

2 Answers2

0

A couple of observations:

  1. Never use locks (or wait for semaphores or dispatch groups, etc.) to attempt to manage dependencies between Swift concurrency tasks. This is a concurrency system predicated upon the contract that threads can make forward progress. It cannot reason about the concurrency if you block threads with mechanisms outside of its purview.

  2. Usually you would not create a new service for every upload. You would create one and reuse it.

    E.g., either:

    func createUploadService() async {
        let uploadService = UploadService()
    
        for i in 0...4 {
            await uploadService.uploadData(…)
        }
    }
    

    Or, more likely, if you might use this same UploadService later, do not make it a local variable at all. Give it some broader scope.

    let uploadService = UploadService()
    
    func createUploadService() async {    
        for i in 0...4 {
            await uploadService.uploadData(…)
        }
    }
    
  3. The above only works in simple for loop, because we could simply await the result of the prior iteration.

    But what if you wanted the UploadService keep track of the prior upload request and you couldn’t just await it like above? You could keep track of the Task and have each task await the result of the previous one, e.g.,

    actor UploadService {
        var task: Task<Void, Never>?                // change to `Task<Void, Error>` if you change it to a throwing method
    
        func upload() {
            …
    
            task = Task { [previousTask = task] in  // capture copy of previous task (if any)
                _ = await previousTask?.result      // wait for it to finish before starting this one
    
                await uploadData()
            }
        }
    }
    

    FWIW, I made this service with some internal state an actor (to avoid races).

Rob
  • 415,655
  • 72
  • 787
  • 1,044
-1

Since creating Task {} is part of structured concurrency it inherits environment (e.g MainThread) from the scope where it was created,try using unstructured concurrency's Task.detached to prevent it from runnning on same scope ( maybe it was called on main thread ) - with creating Task following way:

Task.detached(priority: .default) {
 await uploadData()
}
Mr.SwiftOak
  • 1,469
  • 3
  • 8
  • 19