Swift 数组与 DispatchQueue 同步

Swift Array Synchronization with DispatchQueue

如果从同一个线程(线程 1 主线程)调用 uploadFailed(for id: String)uploadSuccess()updateOnStart(_ id: String),我知道我们不需要同步队列。如果每次上传都从不同的线程调用函数怎么办。我在哪里确保同步?是上传和状态还是只是状态?

enum FlowState {
 case started(uploads: [String])
 case submitted
 case failed
}

class Session {
 var state: FlowState
 let syncQueue: DispatchQueue = .init(label: "Image Upload Sync Queue",
                                      qos: .userInitiated,
                                      attributes: [],
                                      autoreleaseFrequency: .workItem)

 init(state: FlowState) {
   self.state = state
 }

 mutating func updateOnStart(_ id: String) {
  guard case .started(var uploads) = state else {
    return
  }

  uploads.append(id)

  state = .started(uploads)
}

  mutating func uploadFailed(for id: String) {
   guard case .started(var uploads) = state else {
    return
   }

   uploads.removeAll { [=11=] == id }

   if uploads.isEmpty {
      state = .failed
   } else {
      state = .started(uploads)
   }
 }

 mutating func uploadSuccess() {
  state = .submitted
 }

}

我们是否像下面那样同步 uploads 数组操作和状态?

syncQueue.sync {
  uploads.append(id)

  state = .started(uploads)
}


syncQueue.sync {
  uploads.removeAll { [=12=] == id }

   if uploads.isEmpty {
      state = .failed
   } else {
      state = .started(uploads)
   }
}

syncQueue.sync {
  state = .started(uploads)
}

syncQueue.sync {
  if uploads.isEmpty {
    state = .failed
  } else {
    state = .started(uploads)
  }
}

网络调用的完成处理程序可以更新 Sessionstate 属性。例如,用户选择 10 张图像并上传。完成后可能是失败也可能是成功。对于我们上传的每张图片,我们都会缓存资源 id 并在上传失败时将其删除。当所有图片上传失败时,我们更新状态.failed。我们只关心上传的一张图片。当上传单张图片时,我们将状态更新为 .submitted

使用同步来确保与 FlowState 的线程安全交互是完全有效的。

几点观察:

  1. 您提供了两个备选方案:

    syncQueue.sync {
        uploads.append(id)
    
        self.state = .started(uploads)
    }
    

    syncQueue.sync {
        state = .started(uploads)
    }
    

    这两个都不对。如果线程 1 和线程 2 同时调用这个例程怎么办?考虑:

    • 线程 1 检索 uploads
    • 线程 2 检索相同的 uploads;
    • 线程 1 然后进入其 sync 块并将记录附加到其本地副本,保存它,并离开其 sync 闭包;
    • 线程 2 然后进入它的 sync 块并将不同的记录附加到它自己的本地副本(没有线程 1 添加的记录),保存它,并离开它的 sync 闭包。

    在这种情况下,您将丢失线程 1 附加的内容。

    因此,您需要采用第三种更广泛的同步机制,将检索、附加和存储的整个过程包装在同步机制中 uploads

    syncQueue.sync {
        guard case .started(var uploads) = self.state else {
            return
        }
    
        uploads.append(id)
    
        state = .started(uploads)
    }
    
  2. 如果您还没有,我建议您在开发和测试期间打开 thread sanitizer (TSAN)。它可能会帮助您解决这些问题。

  3. 您没有显示 state 的任何额外读数,但如果有任何读数,请确保您也同步读数。与 state 的所有交互都必须同步。

  4. 一个微妙的问题:如果您要同步对 state 的访问,请确保将其设为 private,这样就没有外部代码可以访问它(否则你可能会阻碍你确保线程安全交互的尝试)。您需要将所有读取和写入包装在您的同步机制中。

    您可能也应该将同步队列设为私有,因为也不需要公开它。