简体   繁体   English

Swift concurrency: 为什么这个方法是异步的?

[英]Swift concurrency: Why is this method async?

I'm trying to wrap my head around how to integrate Swift concurrency with old code that is using block-based things like Timer.我正在努力思考如何将 Swift 并发与使用基于块的东西(如 Timer)的旧代码集成。 So when I build the code below, the compiler tells me on the line self.handleTimer() that the Expression is 'async' but is not marked with 'await'因此,当我构建下面的代码时,编译器在self.handleTimer()行告诉我Expression is 'async' but is not marked with 'await'

Why is it async?为什么它是异步的? It is not marked async and is not doing anything.它没有标记为async ,也没有做任何事情。 When I call it without the timer I don't need await .当我在没有计时器的情况下调用它时,我不需要await Does actor-isolation mean that every call to a member is "async" from outside that context?参与者隔离是否意味着对成员的每次调用都是来自该上下文之外的“异步”?

@MainActor
class MyClass {
    
    func startTimer() {
        _ = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
            Task {
                self.handleTimer() // "Expression is 'async' but is not marked with 'await'"
            }
        }
    }
    
    func handleTimer() {
    }
}

Does actor-isolation mean that every call to a member is "async" from outside that context?参与者隔离是否意味着对成员的每次调用都是来自该上下文之外的“异步”?

Yes.是的。

You can remove the warning by telling the task to do its work on the main actor, like so:您可以通过告诉任务在主要参与者上完成它的工作来删除警告,如下所示:

Task { @MainActor in
    self.handleTimer()
}

Or by marking handleTimer() as nonisolated , if it has no side effects that would break actor isolation.或者通过将handleTimer()标记为nonisolated ,如果它没有会破坏 actor 隔离的副作用。

It is because of the @MainActor (only use to update UI) you get the same effect if you use actor instead of class .这是因为@MainActor (仅用于更新 UI)如果您使用actor而不是class ,您会得到相同的效果。

actor MyClass {

Actors are isolated, concurrent pieces Actor 是孤立的、并发的部分

The actor allows only one task at a time to access its mutable state Actor 一次只允许一个任务访问其可变状态

Meaning that anything that is done within handleTimer will be "paused" if something else is mutating the actor .这意味着如果其他东西正在改变actor ,那么在handleTimer中完成的任何事情都将被“暂停”。

If handleTimer will not mutate anything inside the actor you can add the nonisolated keyword.如果handleTimer不会改变 actor 内部的任何内容,您可以添加nonisolated关键字。

@MainActor
class MyClass {
    
    func startTimer() {
        _ = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
            Task {
                self.handleTimer() 
            }
        }
    }
    
    nonisolated func handleTimer() {
    }
}

But that Task inside of a closure is an issue by itself.但是闭包中的Task本身就是一个问题。 Concurrency is all about organizing and knowing what is going on at all times.并发性就是组织和了解任何时候发生的事情。 That Task break that, it gets called an then it is just left there.那个Task打破了它,它被调用然后它就留在那里。 There is no way of stopping it or keeping track of when it is done or when it has failed.无法停止它或跟踪它何时完成或何时失败。

I suggest rethinking your setup so it uses a pure concurrent setup, closures and concurrency are full of issues and leaks.我建议重新考虑您的设置,以便它使用纯并发设置,闭包和并发充满问题和漏洞。

@MainActor
class MyClass {
    func startTimer(withTimeInterval interval: TimeInterval, upperLimit: Int = .max) async throws {
        for _ in 0...upperLimit{
            try await Task.sleep(for: .seconds(interval))
            handleTimer()
        }
    }
    
    func handleTimer() {
        print(#function)
    }

}

Then use it something like below.然后像下面这样使用它。

let task = Task{
    do{
        let c = await MyClass()
        try await c.startTimer(withTimeInterval: 1, upperLimit: 10)
    }catch{
        print(error)
    }
}

With this setup you can cancel the timer/task with使用此设置,您可以取消计时器/任务

task.cancel()

I think the way to understand this situation is to build up to it in stages.我认为理解这种情况的方法是分阶段进行。 Let's start with no async/await markings of any kind:让我们从没有任何类型的async/await标记开始:

class MyClass {
    func startTimer() {
        _ = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
            self.handleTimer()
        }
    }
    func handleTimer() {}
}

Fine, so we all know we're allowed to talk like that;好吧,我们都知道我们可以这样说话; we've been doing it for years.我们多年来一直这样做。 Now let's mark MyClass as @MainActor :现在让我们将 MyClass 标记为@MainActor

@MainActor
class MyClass {
    func startTimer() {
        _ = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
            self.handleTimer() // warning -> error
        }
    }
    func handleTimer() {}
}

Now self.handleTimer() elicits a warning that, we are told, will evolve into an error when Swift concurrency comes to full fruition in Swift 6: "Call to main actor-isolated instance method 'handleTimer()' in a synchronous nonisolated context."现在self.handleTimer()引发了一个警告,我们被告知,当 Swift 并发在 Swift 6 中完全实现时,它将演变成一个错误:“在同步非隔离上下文中调用主参与者隔离实例方法‘handleTimer()’ ”

Clearly there is something about this line that the compiler regards with suspicion.显然,编译器怀疑这一行中的某些内容。 But what?但是什么? As you have said yourself, handleTimer seems innocuous enough.正如您自己所说, handleTimer似乎无伤大雅。 But let's take a step back.但是,让我们退后一步。 All these years, we've become accustomed to the notion that things in general tend to happen "on the main thread".这些年来,我们已经习惯了这样一种观念,即一般情况下事情往往会发生在“主线程”上。 As long as that's universally true, there aren't any threading issues.只要这是普遍正确的,就不存在任何线程问题。 If we call handleTimer from a view controller, everything is fine:如果我们从视图控制器调用handleTimer ,一切都很好:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        MyClass().handleTimer()
    }
}

No problem.没问题。 But not so fast.但没那么快。 There is no problem, only because ViewController, as a UIViewController, is also marked @MainActor (implicitly).没有问题,只是因为 ViewController 作为 UIViewController也被标记为@MainActor (隐式)。 As soon as we introduce a class with no such marking, things rapidly fall apart:一旦我们引入一个没有这种标记的类,事情就会迅速崩溃:

class OtherClass {
    func test() {
        MyClass().handleTimer() // BLAAAAAAAAAAP!
    }
}

Whoa.哇。 Everything about that line turns out to be illegal.关于那条线的一切都被证明是非法的。 Not only can we not call handleTimer , we can't even call the MyClass initializer!!!我们不仅不能调用handleTimer ,我们甚至不能调用 MyClass 初始化器!!!

Why is there an issue here?为什么这里有问题? Let's look at the matter historically.让我们从历史的角度来看这个问题。 While you were sleeping, Apple quietly attached a @MainActor designation to all your favorite UIKit classes, and turned on the beginnings of Swift concurrency.在你睡觉的时候,Apple 悄悄地为所有你最喜欢的 UIKit 类附加了一个@MainActor名称,并开启了 Swift 并发的开端。 But because Apple attached this designation to all your favorite UIKit classes, you didn't even notice;但是因为 Apple 将这个名称附加到所有你最喜欢的 UIKit 类上,你甚至没有注意到; it's just a bunch of methods calling one another on the same actor (namely the main actor), so the compiler is perfectly happy.它只是一堆在同一个演员(即主要演员)上互相调用的方法,所以编译器非常高兴。

In such a world, however, our OtherClass is a stranger.然而,在这样的世界中,我们的 OtherClass 是一个陌生人。 It is not declared @MainActor .没有声明为@MainActor So when it talks to MyClass, it crosses actor contexts — and the compiler brings down upon us the full force of its wrath.因此,当它与 MyClass 对话时,它会跨越 actor 上下文——编译器会向我们倾诉它的全部愤怒。

Obviously we can solve the problem by bringing OtherClass into our happy @MainActor world:显然,我们可以通过将 OtherClass 带入我们快乐的@MainActor世界来解决问题:

@MainActor
class OtherClass {
    func test() {
        MyClass().handleTimer() // no problem :)
    }
}

We could alternatively solve it introducing full-on async/await , which is what you do when you want to talk across actor contexts:我们也可以通过引入完整的async/await来解决它,这就是当你想跨 actor 上下文交谈时所做的事情:

class OtherClass {
    func test() {
        Task {
            await MyClass().handleTimer()
        }
    }
}

In that code, we are making no guarantees about what actor (if any) OtherClass is tied to, or what actor (if any) the Task is tied to.在该代码中,我们保证 OtherClass 绑定到哪个 actor(如果有),或者 Task 绑定到哪个 actor(如果有)。 But we can behave coherently just by crossing the actor contexts correctly, namely by saying await inside an async -context world, namely the Task.但是我们可以通过正确地跨越 actor 上下文来表现一致,即通过在async上下文世界中说await ,即任务。

Okay.好的。 So now we're ready to grapple with your original problem, Everything was fine, as we've shown, until you said @MainActor on MyClass.所以现在我们准备好解决您原来的问题,一切都很好,正如我们所展示的,直到您在 MyClass 上说@MainActor At that point, a question arises: what's the actor status of the line self.handleTimer() ?那时,出现了一个问题: self.handleTimer()行的参与者状态是什么?

@MainActor
class MyClass {
    func startTimer() {
        _ = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
            self.handleTimer() // warning -> error
        }
    }
    func handleTimer() {}
}

The compiler is saying: "This line belongs to the Timer (and the runtime), not to you. This self.handleTimer() call is coming from outside MyClass. But MyClass is bound to the main actor, so you can't do that, I (the compiler) am going to forgive you for now, because if I didn't. all your Timer code in every UIKit class in all your existing projects would break, But I'm warning you, I'm not going to be so easy-going in the future!"编译器说:“这一行属于定时器(和运行时),而不属于你。这个self.handleTimer()调用来自 MyClass外部。但是 MyClass 绑定到主要参与者,所以你不能这样做那,我(编译器)现在会原谅你,因为如果我不这样做。你所有现有项目中每个 UIKit 类中的所有计时器代码都会中断,但我警告你,我不会去以后要这么随和!”

To solve the problem, one approach is to guarantee that self.handleTimer() will be itself on the main actor.要解决这个问题,一种方法是保证self.handleTimer()将在主要参与者 One way to do that is to wrap the call in a MainActor block.一种方法是将调用包装在 MainActor 块中。 But we can't do that in a non-async context;但是我们不能在非异步上下文中这样做; this, for example, doesn't compile at all:例如,这根本无法编译:

@MainActor
class MyClass {
    func startTimer() {
        _ = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
            MainActor.run { // No way!!!!
                self.handleTimer()
            }
        }
    }
    func handleTimer() {}
}

Instead, we have to use the lessons we just learned.相反,我们必须利用刚刚学到的教训。 We need to get into an async context — which, since the Timer block doesn't even belong to us, we can do only by introducing a Task.我们需要进入异步上下文——因为 Timer 块甚至不属于我们,我们只能通过引入任务来实现。 We can then mark the inside of that task as tied to the main actor, and the whole issue goes away:然后我们可以将该任务的内部标记为与主要参与者相关联,整个问题就消失了:

@MainActor
class MyClass {
    func startTimer() {
        _ = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
            Task { @MainActor in
                self.handleTimer()
            }
        }
    }
    func handleTimer() {}
}

The alternative, obviously, would be, instead of saying @MainActor inside the task, just to use full-on async/await talk so we can cross the actor contexts:显然,另一种方法是,不在任务中使用@MainActor ,而是使用完整的async/await对话,这样我们就可以跨越 actor 上下文:

@MainActor
class MyClass {
    func startTimer() {
        _ = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
            Task {
                await self.handleTimer()
            }
        }
    }
    func handleTimer() {}
}

Last but not least, we can do what jrturton has said: mark handleTimer itself as innocuous.最后但同样重要的是,我们可以按照 jrturton 所说的去做:将handleTimer本身标记为无害的。 This is tricky, because you're telling the compiler that you know more than it does, which is always risky (like when you cast down with as! ).这很棘手,因为您是在告诉编译器您知道的比它知道的多,这总是有风险的(就像您使用as!时一样)。 But since handleTimer is currently empty, , we can certainly get away with it:但是由于handleTimer目前是空的,我们当然可以摆脱它:

@MainActor
class MyClass {
    func startTimer() {
        _ = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
                self.handleTimer()
            }
    }
    nonisolated func handleTimer() {}
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 Swift 并发:模拟器上的`async let` 奇怪行为 - Swift Concurrency: `async let` strange behaviour on Simulators 使用 Swift 中的新并发将同步函数转换为异步函数 - Convert a sync function to async using new concurrency in Swift Swift 5.5 并发:如何序列化异步任务以用 maxConcurrentOperationCount = 1 替换 OperationQueue? - Swift 5.5 Concurrency: how to serialize async Tasks to replace an OperationQueue with maxConcurrentOperationCount = 1? Swift:异步方法进入while循环 - Swift: Async method into while loop 不支持并发的 function 中的 'async' 调用 - 'async' call in a function that does not support concurrency Swift:如何在同步方法中包装异步方法? - Swift: How to wrap an async method in a synchronous method? 为什么在解码来自 API 请求的 JSON 数据时异步方法会引发错误? Swift - Why is async method throwing an error when decoding JSON data from an API request? Swift 异步操作并发理解 - async operation concurrency understanding 当场景进入后台使用新的 Swift 异步/等待并发 model 时,如何执行代码? - How do you execute code when scene goes to background using new Swift async/await concurrency model? Swift 编写一个带有返回值的 async/await 方法 - Swift write an async/await method with return value
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM