使用MainActor时Swift并发似乎是随机的?

问题描述 投票:0回答:1

我一直在尝试找出一种在主线程上调用 corelocation 的 requestLocation 的方法(这显然是必需的)。

考虑这个 MRE

import CoreLocation
import MapKit
import SwiftUI

struct ContentView: View {
    var locationManager = LocationManager()

    var body: some View {
        Button {
            Task {
                let location = try await locationManager.currentLocation // works
                print(location)
                let location2 = try await locationManager.work() // works, no mainactor needed
                print(location2)
                 let location3 = try await APIService.shared.test() // doesnt work
                print(location3)
                let location4 = try await APIService.shared.test2() // works, mainactor needed
                print(location4)
                let location5 = try await APIService.shared.test3() // doesnt work even with mainactor
                print(location5)
            }
        } label: {
            Text("Get Location")
        }.task {
            // 1. Check if the app is authorized to access the location services of the device
            locationManager.checkAuthorization()
        }
    }
}

class LocationManager: NSObject, CLLocationManagerDelegate {
    // MARK: Object to Access Location Services

    private let locationManager = CLLocationManager()

    // MARK: Set up the Location Manager Delegate

    override init() {
        super.init()
        locationManager.delegate = self
    }

    // MARK: Request Authorization to access the User Location

    func checkAuthorization() {
        switch locationManager.authorizationStatus {
        case .notDetermined:
            locationManager.requestWhenInUseAuthorization()
        default:
            return
        }
    }

    // MARK: Continuation Object for the User Location

    private var continuation: CheckedContinuation<CLLocation, Error>?

    // MARK: Async Request the Current Location

    var currentLocation: CLLocation {
        get async throws {
            return try await withCheckedThrowingContinuation { continuation in
                // 1. Set up the continuation object
                self.continuation = continuation
                // 2. Triggers the update of the current location
                locationManager.requestLocation()
            }
        }
    }

    @MainActor
    var currentLocation2: CLLocation {
        get async throws {
            return try await withCheckedThrowingContinuation { continuation in
                // 1. Set up the continuation object
                self.continuation = continuation
                // 2. Triggers the update of the current location
                locationManager.requestLocation()
            }
        }
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        // 4. If there is a location available
        if let lastLocation = locations.last {
            // 5. Resumes the continuation object with the user location as result
            continuation?.resume(returning: lastLocation)
            // Resets the continuation object
            continuation = nil
        }
    }

    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        // 6. If not possible to retrieve a location, resumes with an error
        continuation?.resume(throwing: error)
        // Resets the continuation object
        continuation = nil
    }

    func work() async throws -> CLLocation {
        return try await currentLocation
    }
}

class APIService {
    static let shared = APIService()

    // Private initializer to prevent the creation of additional instances
    private init() {
    }

    func test() async throws -> String {
        return try await String(describing: LocationManager().currentLocation)
    }

    @MainActor
    func test2() async throws -> String {
        return try await String(describing: LocationManager().currentLocation)
    }

    func test3() async throws -> String {
        return try await String(describing: LocationManager().currentLocation2)
    }
}

Test1 按预期工作,因为视图中的任务继承为 mainactor

Test2 的工作原理与我假设的相同

Test3 不工作,不知道为什么 test2 工作?我猜如果转到另一个班级,你就会失去演员?

Test4 按预期工作,因为你强制它成为 mainactor

即使你强迫 Test5 再次成为 mainactor,它也不会神秘地工作 那么swift并发中主线程的规则是什么呢?

我正在尝试让 Test5 工作,但解释的其他测试用例将有助于理解如何让 Test5 工作。

swift swiftui core-location
1个回答
0
投票

我一直在尝试找出一种在主线程上调用 corelocation 的

requestLocation
的方法(这显然是必需的)。

不需要在主线程上调用requestLocation

。您可以从任何线程调用它。

这里的问题是您在错误的线程上

创建CLLocationManager

。来自
文档

Core Location 使用您初始化

RunLoop

 对象的线程的 
CLLocationManager
 来调用委托对象的方法。该线程本身必须有一个活动的 
RunLoop
,就像在应用程序主线程中找到的那样。

在失败的情况下,您将在非隔离异步方法中创建

LocationManager

(进而创建 
CLLocationManager
)。这将在协作线程池中的某个线程上运行,该线程池肯定没有 
RunLoop
。因此不会调用委托方法。

// locationManager is initialised in the View initialiser, which is run on the main thread // so these work let location = try await locationManager.currentLocation let location2 = try await locationManager.work() // test2 is isolated to the main actor, so "LocationManager()" is run on the main thread too let location4 = try await APIService.shared.test2() // neither test nor test3 are isolated to the main actor, so LocationManager is not created on the main thread // the fact that 'currentLocation2' is isolated to the main thread doesn't matter let location3 = try await APIService.shared.test() let location5 = try await APIService.shared.test3()


请注意,您的代码有许多与并发相关的警告。尝试打开完整的并发检查并亲自查看。

我会将

LocationManager

 变成 
actor
,这样它就是 
Sendable
,因此可以安全地从任何地方调用它的属性/方法。我也会将
APIService
作为最后一堂课,这样它也可以成为
Sendable
。这使得 
shared
 实例安全。

请注意,目前如果您在正在进行的延续尚未恢复时获得

currentLocation

,您将覆盖现有的延续,导致永远不会在该覆盖的延续上调用 
resume

这里我修复了代码以删除与并发相关的警告:

actor LocationManager: NSObject, CLLocationManagerDelegate { private let locationManager = CLLocationManager() override init() { super.init() locationManager.delegate = self } func checkAuthorization() { switch locationManager.authorizationStatus { case .notDetermined: locationManager.requestWhenInUseAuthorization() default: return } } private var continuation: CheckedContinuation<CLLocation, Error>? enum LocationError: Error { case locationInProgress } var currentLocation: CLLocation { get async throws { return try await withCheckedThrowingContinuation { continuation in if self.continuation != nil { continuation.resume(throwing: LocationError.locationInProgress) } else { self.continuation = continuation locationManager.requestLocation() } } } } func updateLocation(_ location: CLLocation) { continuation?.resume(returning: location) continuation = nil } func locationError(_ error: Error) { continuation?.resume(throwing: error) continuation = nil } nonisolated func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { if let lastLocation = locations.last { Task { await updateLocation(lastLocation) } } } nonisolated func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { Task { await locationError(error) } } func work() async throws -> CLLocation { return try await currentLocation } } final class APIService: Sendable { static let shared = APIService() let locationManager = LocationManager() private init() { } func test() async throws -> String { return try await String(describing: locationManager.currentLocation) } }
    
© www.soinside.com 2019 - 2024. All rights reserved.