EXC_BAD_ACCESS 在 Swift 中初始化 CurrentValueSubject 的字典时

EXC_BAD_ACCESS when initializing Dictionary of CurrentValueSubject in Swift

我正在尝试创建一个 class 执行一次数据加载并 returns 在数据加载时将数据发送给该方法的所有调用者,以不对同一项目执行数据加载(标识符)不止一次。我遇到的问题是它似乎在 CurrentValueSubject 的第一次初始化时崩溃了。只有 downloadStuff returns 和 Error 我不知道出了什么问题,才会发生这种情况。这是问题的再现。

Class 进行同步:

class FetchSynchronizer<T, ItemIdentifier: Hashable> {

typealias CustomParams = (isFirstLoad: Bool, result: Result<T, Error>)

    enum FetchCondition {
        // executes data fetching only once
        case executeFetchOnlyOnce
        // re-executes fetch if request failed
        case retryOnlyIfFailure
        // always executes fetch even if response is cached
        case noDataCache
        // custom condition
        case custom((CustomParams) -> Bool)
    }
    
    struct LoadingState<T> {
        let result: Result<T, Error>
        let isLoading: Bool
        
        init(result: Result<T, Error>? = nil, isLoading: Bool = false) {
            self.result = result ?? .failure(NoResultsError())
            self.isLoading = isLoading
        }
    }
    
    private var cancellables = Set<AnyCancellable>()
    private var isLoading: [ItemIdentifier: CurrentValueSubject<LoadingState<T>, Never>] = [:]
    
    func startLoading(identifier: ItemIdentifier,
                      fetchCondition: FetchCondition = .executeFetchOnlyOnce,
                      loaderMethod: @escaping () async -> Result<T, Error>) async -> Result<T, Error> {
        
        // initialize loading tracker for identifier on first execution
        var isFirstExecution = false
        if isLoading[identifier] == nil {
            print("----0")
            isLoading[identifier] = CurrentValueSubject<LoadingState<T>, Never>(LoadingState<T>())
            isFirstExecution = true
        }
        
        guard let currentIsLoading = isLoading[identifier] else {
            assertionFailure("Should never be nil because it's set above")
            return .failure(NoResultsError())
        }
        
        if currentIsLoading.value.isLoading {
            // loading in progress, wait for finish and call pending callbacks
            return await withCheckedContinuation { continuation in
                currentIsLoading.filter { ![=12=].isLoading }.sink { currentIsLoading in
                    continuation.resume(returning: currentIsLoading.result)
                }.store(in: &cancellables)
            }
        } else {
            // no fetching in progress, check if it can be executed
            let shouldFetchData: Bool
            switch fetchCondition {
            case .executeFetchOnlyOnce:
                // first execution -> fetch data
                shouldFetchData = isFirstExecution
            case .retryOnlyIfFailure:
                // no cached data -> fetch data
                switch currentIsLoading.value.result {
                case .success:
                    shouldFetchData = false
                case .failure:
                    shouldFetchData = true
                }
            case .noDataCache:
                // always fetch
                shouldFetchData = true
            case .custom(let completion):
                shouldFetchData = completion((isFirstLoad: isFirstExecution,
                                              result: currentIsLoading.value.result))
            }
            
            if shouldFetchData {
                
                currentIsLoading.send(LoadingState(isLoading: true))
                // fetch data
                return await withCheckedContinuation { continuation in
                    Task {
                        // execute loader method
                        let result = await loaderMethod()
                        let state = LoadingState(result: result,
                                                 isLoading: false)
                        currentIsLoading.send(state)
                        continuation.resume(returning: result)
                    }
                }
            } else {
                // use existing data
                return currentIsLoading.value.result
            }
        }
    }
}

用法示例:

class Executer {
    
    let fetchSynchronizer = FetchSynchronizer<Data?, String>()
    
    func downloadStuff() async -> Result<Data?, Error> {
        await fetchSynchronizer.startLoading(identifier: "1") {
            return await withCheckedContinuation { continuation in
                sleep(UInt32.random(in: 1...3))
                print("-------request")
                continuation.resume(returning: .failure(NSError() as Error))
            }
        }
    }
    
    init() {
        start()
    }
    
    func start() {
        Task {
            await downloadStuff()
            print("-----3")
        }
        DispatchQueue.global(qos: .utility).async {
            Task {
                await self.downloadStuff()
                print("-----2")
            }
        }
        
        DispatchQueue.global(qos: .background).async {
            Task {
                await self.downloadStuff()
                print("-----1")
            }
        }
    }
}

开始执行:

Executer()

崩溃于

isLoading[identifier] = CurrentValueSubject<LoadingState<T>, Never>(LoadingState<T>())

如有任何指导,我们将不胜感激。

Swift字典不是thread-safe。 您需要确保仅从一个线程(即队列)或使用锁访问它。

编辑 - @Bogdan 建议的另一个解决方案问题作者是使 class 成为 actor class 并发安全由编译器处理!

通过调度到一个全局队列,你增加了两个线程“同时”尝试写入字典的机会,这可能会导致崩溃

看看这些例子。

https://github.com/iThink32/Thread-Safe-Dictionary/blob/main/ThreadSafeDictionary.swift