我应该将我的服务设置为可发送吗? Swift 中的并发问题

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

我通过协议实现了多个服务,以便能够在 ViewModel 初始化时注入模拟服务,并且在启用严格的并发检查后,我收到许多警告

"Capture of 'self' with non-sendable type "ViewModelType" in 'async let' binding"

这是一个最小的可重现示例:

class ViewController: UIViewController {

    let viewModel: UserProfileViewModel

    init(service: UserProfileServiceProtocol) {
        self.viewModel = UserProfileViewModel(service: service)
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        getUserProfileData()
        // Do any additional setup after loading the view.
    }

    func getUserProfileData() {
        Task {
            do {
                try await viewModel.getProfileData()
                // update UI
            } catch {
                print(error)
            }
        }
    }
}

struct UserModel {
    var userID: String
    var username: String
    var profilePictureURL: URL?
    var profilePhoto: UIImage?
}

class UserProfileViewModel {

    private let service: any UserProfileServiceProtocol

    var user: UserModel?
    var postsCount: Int?
    var followersCount: Int?
    var followedUsersCount: Int?

    init(service: UserProfileServiceProtocol) {
        self.service = service
    }

    func getProfileData() async throws {
        async let user = service.getUserData()
        async let followersCount = service.getFollowersCount()
        async let followedUsersCount = service.getFollowingCount()
        async let postsCount = service.getPostsCount()

        self.user = try await user
        self.followersCount = try await followersCount
        self.followedUsersCount = try await followedUsersCount
        self.postsCount = try await postsCount
    }
}



protocol UserProfileServiceProtocol {
    var followService: FollowSystemProtocol { get }
    var userPostsService: UserPostsServiceProtocol { get }
    var userDataService: UserDataServiceProtocol { get }

    func getFollowersCount() async throws -> Int
    func getFollowingCount() async throws -> Int
    func getPostsCount() async throws -> Int
    func getUserData() async throws -> UserModel
}

protocol FollowSystemProtocol {
    func getFollowersNumber(for uid: String) async throws -> Int
    func getFollowingNumber(for uid: String) async throws -> Int
}

protocol UserPostsServiceProtocol {
    func getPostCount(for userID: String) async throws -> Int
}

protocol UserDataServiceProtocol {
    func getUser(for userID: String) async throws -> UserModel
}

class UserService: UserProfileServiceProtocol {

    let userID: String

    let followService: FollowSystemProtocol
    let userPostsService: UserPostsServiceProtocol
    let userDataService: UserDataServiceProtocol

    init(userID: String, followService: FollowSystemProtocol, userPostsService: UserPostsServiceProtocol, userDataService: UserDataServiceProtocol) {
        self.userID = userID
        self.followService = followService
        self.userPostsService = userPostsService
        self.userDataService = userDataService
    }

    func getFollowersCount() async throws -> Int {
        let followersCount = try await followService.getFollowersNumber(for: userID)
        return followersCount
    }
    func getFollowingCount() async throws -> Int {
        let followersCount = try await followService.getFollowingNumber(for: userID)
        return followersCount
    }

    func getPostsCount() async throws -> Int {
        let postsCount = try await userPostsService.getPostCount(for: userID)
        return postsCount
    }

    func getUserData() async throws -> UserModel {
        let user = try await userDataService.getUser(for: userID)
        return user
    }
}

class FollowSystemService: FollowSystemProtocol {
    func getFollowersNumber(for uid: String) async throws -> Int {
        try await Task.sleep(for: .seconds(1))
        return 5
    }
    
    func getFollowingNumber(for uid: String) async throws -> Int {
        try await Task.sleep(for: .seconds(1))
        return 19
    }
}

class UserPostsService: UserPostsServiceProtocol {
    func getPostCount(for userID: String) async throws -> Int {
        try await Task.sleep(for: .seconds(1))
        return 27
    }
}

class UserProfileService: UserDataServiceProtocol {
    func getUser(for userID: String) async throws -> UserModel {
        try await Task.sleep(for: .seconds(1))
        return UserModel(userID: "testUser_01", username: "testUser", profilePictureURL: nil)
    }
}

我没有足够的经验来判断解决这个问题的正确方法是什么,所以我只是紧张地试图挖掘有关此问题的任何信息,但没有运气。
我应该使服务协议符合可发送吗?这样做是一种常见的做法吗?或者我应该做一些完全不同的事情来解决这个问题?

swift async-await concurrency thread-safety sendable
1个回答
0
投票

目前,您的视图模型还不是

Sendable
。因为视图模型没有与任何特定参与者隔离,所以它的所有
async
方法(凭借 SE-0338)都在“通用执行器”(即不是主线程)上运行。因此,您有一个后台线程更新视图控制器从主线程访问的属性)。那不是线程安全的。如果将“Concurrency Strict Checking”构建设置设置为“Complete”,您将看到更多有关缺乏线程安全性的警告。

视图模型至少应该将视图访问的属性与主要参与者隔离。更简单的是,我们经常将整个视图模型与主要参与者隔离。视图模型的整个工作就是支持视图(位于主要参与者上),因此将整个视图模型与主要参与者隔离也是有意义的:

@MainActor
class UserProfileViewModel {…}

关于服务,是的,您也会想要制定这些协议

Sendable
。编译器(尤其是“严格并发检查”构建设置为“完整”时)会警告您,
Sendable
对象不能具有非
Sendable
的属性。即,如果对象的属性不是线程安全的,那么该对象就不是线程安全的。所以,制定协议
Sendable

protocol UserPostsServiceProtocol: Sendable {
    func getPostCount(for userID: String) async throws -> Int
}

然后,当然,也进行实施

Sendable
。例如,如果服务没有可变属性,您可以将其声明为
final
:

final class UserPostsService: UserPostsServiceProtocol {
    func getPostCount(for userID: String) async throws -> Int {
        try await Task.sleep(for: .seconds(1))
        return 27
    }
}

但是,如果您想让服务具有一些内部可变状态,请添加一些同步,或者更容易的参与者隔离它。您可以将其隔离到主要参与者(这对于服务而言不如视图模型那么有吸引力),或者只是将其设为自己的参与者:

actor UserPostsService: UserPostsServiceProtocol {
    private var value = 0

    func getPostCount(for userID: String) async throws -> Int {
        try await Task.sleep(for: .seconds(1))
        value = 27
        return value
    }
}

因此,将所有这些放在一起,您最终可能会得到:

class ViewController: UIViewController {
    let viewModel: UserProfileViewModel
    var task: Task<Void, Error>?

    init(service: UserProfileServiceProtocol) {
        self.viewModel = UserProfileViewModel(service: service)
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        getUserProfileData() // you might consider doing this in `viewDidAppear` … it depends upon whether this view presents other view controllers and whether you want it to re-fetch user profile data when it re-appears
    }

    // If you use unstructured concurrency, you are responsible for 
    // canceling the task whenever its results are no longer needed.

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        task?.cancel()
    }

    // So, make sure to save this unstructured concurrency `Task` in a property so you can cancel it when no longer needed

    func getUserProfileData() {
        task = Task {
            do {
                try await viewModel.getProfileData()
                // update UI
            } catch {
                print(error)
            }
        }
    }
}

struct UserModel {
    var userID: String
    var username: String
    var profilePictureURL: URL? = nil
    var profilePhoto: UIImage? = nil
}

@MainActor
class UserProfileViewModel {
    private let service: any UserProfileServiceProtocol

    var user: UserModel?
    var postsCount: Int?
    var followersCount: Int?
    var followedUsersCount: Int?

    init(service: UserProfileServiceProtocol) {
        self.service = service
    }

    func getProfileData() async throws {
        async let user = service.getUserData()
        async let followersCount = service.getFollowersCount()
        async let followedUsersCount = service.getFollowingCount()
        async let postsCount = service.getPostsCount()

        self.user = try await user
        self.followersCount = try await followersCount
        self.followedUsersCount = try await followedUsersCount
        self.postsCount = try await postsCount
    }
}

protocol UserProfileServiceProtocol: Sendable {
    var followService: FollowSystemProtocol { get }
    var userPostsService: UserPostsServiceProtocol { get }
    var userDataService: UserDataServiceProtocol { get }

    func getFollowersCount() async throws -> Int
    func getFollowingCount() async throws -> Int
    func getPostsCount() async throws -> Int
    func getUserData() async throws -> UserModel
}

protocol FollowSystemProtocol: Sendable {
    func getFollowersNumber(for uid: String) async throws -> Int
    func getFollowingNumber(for uid: String) async throws -> Int
}

protocol UserPostsServiceProtocol: Sendable {
    func getPostCount(for userID: String) async throws -> Int
}

protocol UserDataServiceProtocol: Sendable {
    func getUser(for userID: String) async throws -> UserModel
}

final class UserService: UserProfileServiceProtocol {
    let userID: String

    let followService: FollowSystemProtocol
    let userPostsService: UserPostsServiceProtocol
    let userDataService: UserDataServiceProtocol

    init(userID: String, followService: FollowSystemProtocol, userPostsService: UserPostsServiceProtocol, userDataService: UserDataServiceProtocol) {
        self.userID = userID
        self.followService = followService
        self.userPostsService = userPostsService
        self.userDataService = userDataService
    }

    func getFollowersCount() async throws -> Int {
        let followersCount = try await followService.getFollowersNumber(for: userID)
        return followersCount
    }
    func getFollowingCount() async throws -> Int {
        let followersCount = try await followService.getFollowingNumber(for: userID)
        return followersCount
    }

    func getPostsCount() async throws -> Int {
        let postsCount = try await userPostsService.getPostCount(for: userID)
        return postsCount
    }

    func getUserData() async throws -> UserModel {
        let user = try await userDataService.getUser(for: userID)
        return user
    }
}

final class FollowSystemService: FollowSystemProtocol {
    func getFollowersNumber(for uid: String) async throws -> Int {
        try await Task.sleep(for: .seconds(1))
        return 5
    }

    func getFollowingNumber(for uid: String) async throws -> Int {
        try await Task.sleep(for: .seconds(1))
        return 19
    }
}

actor UserPostsService: UserPostsServiceProtocol {
    var value = 0

    func getPostCount(for userID: String) async throws -> Int {
        try await Task.sleep(for: .seconds(1))
        value = 27
        return value
    }
}

final class UserProfileService: UserDataServiceProtocol {
    func getUser(for userID: String) async throws -> UserModel {
        try await Task.sleep(for: .seconds(1))
        return UserModel(userID: "testUser_01", username: "testUser")
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.