Параллелизм Swift кажется случайным при использовании MainActor?

Я пытался найти способ вызвать requestLocation corelocation в основном потоке (что, по-видимому, необходимо).

Рассмотрим это 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 работает так, как ожидалось, поскольку Task из представления наследуется как главный актер.

Test2 работает по той же причине, по которой я предполагаю

Test3 не работает, не знаю, почему, когда test2 работал? Думаю, если он перейдет в другой класс, ты потеряешь актера?

Test4 работает так, как ожидалось, потому что вы заставляете его быть главным актером.

Test5 загадочным образом не работает, даже если вы снова заставляете его быть главным актером Итак, каковы правила для основного потока в быстром параллелизме?

Я пытаюсь заставить Test5 работать, но другие описанные тестовые примеры помогут понять, как заставить Test5 работать.

Поскольку это SwiftUI, вам не следует инициализировать объекты как свойства в вашей структуре View, это утечка памяти. Вам необходимо запустить диспетчер местоположения в вашем .task и передать его асинхронным функциям, определенным в структуре, которые сохраняют его во время работы. Также авторизация должна выполняться в другой структуре View.

malhal 13.08.2024 09:09

Измените APIService на структуру. В SwiftUI обычно мы создаем эти ключи среды, чтобы их можно было переключать с помощью макетов для предварительного просмотра.

malhal 13.08.2024 09:12
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
2
50
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я пытался найти способ вызвать 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()

Что касается того, какой поток вызывает requestLocation (не имеет отношения к проблеме — просто для вашей информации), основной поток всегда будет вызывать вызов currentLocation2, поскольку он изолирован от основного актера, а неосновной поток всегда будет вызывать вызов. в currentLocation, потому что он не изолирован. Вы можете проверить это, нажав MainActor.shared.assertIsolated(). Если он вылетает, вы не на главном актере.


Обратите внимание, что ваш код содержит множество предупреждений, связанных с параллелизмом. Попробуйте включить полную проверку параллелизма и убедитесь в этом сами.

Я бы сделал LocationManageractor так, чтобы он был 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
    }
}

// also consider making this a struct
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)
    }
}

Теперь при запуске приложения вам просто нужно получить доступ к APIService.shared в основном потоке, это приведет к созданию CLLocationManager в основном потоке, и любые последующие доступы можно будет осуществлять из любого потока.


В код SwiftUI также необходимо внести несколько улучшений, например создание неструктурированного Task верхнего уровня, когда вместо этого можно использовать модификатор .task(id:), но это выходит за рамки данного вопроса.

спасибо за ответ. Возможно, у него есть работающее решение, но оно не помогает нам понять, как использовать быстрый параллелизм. Почему иногда использование mainactor исправляет проблему (case4), но использование mainactor в другом месте иногда не помогает (case5). И еще один вопрос: в вашем решении даже не используется главный актер, а только сам актер. Кажется, волшебство связано с актерами (любыми актерами), и это, возможно, создает цикл выполнения? Наконец, последний вопрос: сам метод withCheckedThrowingContinuation является асинхронным, так что не сводит ли это на нет всю работу актера, которую выполняют вызывающие объекты?

erotsppa 13.08.2024 16:05

@erotsppa Важнейшим моментом является «в каком потоке CLLocationManager() выполняется». CLLocationManager должен быть создан в потоке, имеющем цикл выполнения. В случае 4 CLLocationManager() запускается для главного актера, потому что @MainActor отмечает test2. В случае 5 CLLocationManager() не запускается для главного актера, поскольку test3 не отмечен @MainActor. Совершенно неважно, чем изолирован актер currentLocation2. На каком актере выполняются другие части кода, также не имеет значения, вызываются ли методы делегата.

Sweeper 13.08.2024 16:11

@erotsppa В своем коде я предполагал, что первый доступ к APIService.shared происходит у главного актера, и это все, что имеет значение для этого вопроса. Первый доступ к APIService.shared приведет к запуску let locationManager = LocationManager() и, следовательно, к инициализации CLLocationManager. Обратите внимание, что в этом конкретном случае инициализатор LocationManager не изолирован от LocationManager.

Sweeper 13.08.2024 16:15

@erotsppa Что касается циклов выполнения, цикл выполнения создается для потока при первом доступе RunLoop.current к этому потоку. Актеры Swift не связаны между собой. Поскольку вы не можете работать с другими потоками напрямую с помощью Swift Concurrency (в том смысле, что вы не можете контролировать, в каком именно фоновом потоке выполняется ваш код), создайте цикл выполнения в любом другом потоке, а затем попробуйте создать CLLocationManager в эта нить не будет надежной. Вам следует просто использовать цикл выполнения основного потока. Кажется, у вас есть другие путаницы относительно Swift Concurrency. Я бы предложил опубликовать новые вопросы по этому поводу.

Sweeper 13.08.2024 16:22

@erotsppa Честно говоря, то, как вы написали свой вопрос, не дает понять, что вы хотите «понять, как использовать быстрый параллелизм» (в любом случае это был бы слишком широкий вопрос для Stack Overflow). Вопрос в том, что CLLocationManager не работает в определенных случаях, поэтому я объяснил почему (а именно, вы должны создать CLLocationManager в основном потоке), и что «нужно вызвать requestLocation в основном потоке» — это ложная предпосылка. Если у вас есть другие вопросы, пожалуйста, задайте их отдельным постом.

Sweeper 13.08.2024 16:34

ваши комментарии помогли прояснить ситуацию. Теперь я это понимаю, наверное, я был слишком сосредоточен на том, где вызывается запрос, и не внимательно прочитал ваш ответ, хотя на самом деле все дело в том, где создается CLLocationManager. Я не знал ничего подобного в SwiftUI, думаю, возможно, это особенность UIKit.

erotsppa 14.08.2024 07:53

Другие вопросы по теме