Search code examples
iosswiftasync-awaitconcurrencynsurlsession

URLSession concurrency issue for async calls


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()
        }
    }
}

Solution

  • 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).