`Task` 在内部调用异步函数时阻塞主线程
`Task` blocks main thread when calling async function inside
我有一个 ObservableObject
class 和一个 SwiftUI 视图。点击按钮时,我会创建一个 Task
并从其中调用 populate
(异步函数)。我以为这会在后台线程上执行 populate
但整个 UI 都冻结了。这是我的代码:
class ViewModel: ObservableObject {
@Published var items = [String]()
func populate() async {
var items = [String]()
for i in 0 ..< 4_000_000 { /// this usually takes a couple seconds
items.append("\(i)")
}
self.items = items
}
}
struct ContentView: View {
@StateObject var model = ViewModel()
@State var rotation = CGFloat(0)
var body: some View {
Button {
Task {
await model.populate()
}
} label: {
Color.blue
.frame(width: 300, height: 80)
.overlay(
Text("\(model.items.count)")
.foregroundColor(.white)
)
.rotationEffect(.degrees(rotation))
}
.onAppear { /// should be a continuous rotation effect
withAnimation(.easeInOut(duration: 2).repeatForever()) {
rotation = 90
}
}
}
}
结果:
按钮停止移动,然后在 populate
完成后突然弹回。
奇怪的是,如果我将 Task
移动到 populate
本身并摆脱 async
,旋转动画不会断断续续,所以我认为循环实际上是在的背景。但是我现在收到 Publishing changes from background threads is not allowed
警告。
func populate() {
Task {
var items = [String]()
for i in 0 ..< 4_000_000 {
items.append("\(i)")
}
self.items = items /// Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
}
}
/// ...
Button {
model.populate()
}
结果:
如何确保我的代码在后台线程上执行?我认为这可能与 MainActor
有关,但我不确定。
您可以通过删除 class 来修复它。你没有使用 Combine,所以你不需要它 ObservableObject
并且如果你坚持使用值类型,SwiftUI 是最有效的。此设计不挂按钮:
extension String {
static func makeItems() async -> [String]{
var items = [String]()
for i in 0 ..< 4_000_000 { /// this usually takes a couple seconds
items.append("\(i)")
}
return items
}
}
struct AnimateContentView: View {
@State var rotation = CGFloat(0)
@State var items = [String]()
var body: some View {
Button {
Task {
items = await String.makeItems()
}
} label: {
Color.blue
.frame(width: 300, height: 80)
.overlay(
Text("\(items.count)")
.foregroundColor(.white)
)
.rotationEffect(.degrees(rotation))
}
.onAppear { /// should be a continuous rotation effect
withAnimation(.easeInOut(duration: 2).repeatForever()) {
rotation = 90
}
}
}
}
首先,你不能两全其美;您要么在主线程上执行 CPU 密集型工作(并对 UI 产生负面影响),要么在另一个线程上执行工作,但您需要显式分派 UI更新到主线程。
然而,你真正想问的是
(By using Task
) I thought this would execute populate on a background thread but instead the entire UI freezes.
当您使用 Task
时,您正在使用 unstructured concurrency, and when you initialise your Task
via init(priority:operation) 任务...继承调用者的优先级和参与者上下文。
虽然 Task
是异步执行的,但它使用调用者的参与者上下文执行此操作,在 View
body
的上下文中是主要参与者。这意味着虽然您的任务是异步执行的,但它仍然在主线程上运行,并且该线程在处理时不可用于 UI 更新。所以你是对的,这与 MainActor
.
有关
当您将 Task
移动到 populate
时,它不再在 MainActor
上下文中创建,因此不会在主线程上执行。
如您所见,您需要使用第二种方法来避开主线程。您需要对代码做的就是使用 MainActor
:
将最终更新移回主队列
func populate() {
Task {
var items = [String]()
for i in 0 ..< 4_000_000 {
items.append("\(i)")
}
await MainActor.run {
self.items = items
}
}
}
您还可以在正文上下文中使用 Task.detached()
来创建不附加到 MainActor
上下文的 Task
。
考虑:
func populate() async {
var items = [String]()
for i in 0 ..< 4_000_000 {
items.append("\(i)")
}
self.items = items
}
你用async
标记了populate
,但是这里没有异步,会阻塞调用线程。
然后考虑:
func populate() {
Task {
var items = [String]()
for i in 0 ..< 4_000_000 {
items.append("\(i)")
}
self.items = items
}
}
看起来它一定是异步的,因为它正在启动 Task
。但是这个Task
运行s在同一个actor上,而且由于task是慢同步的,所以会阻塞当前的executor。
如果你不想运行在主要演员身上,你可以:
可以把view model留在main actor上,但是手动把慢同步进程移到detached task:
@MainActor
class ViewModel: ObservableObject {
@Published var items = [String]()
func populate() {
Task.detached {
var items = [String]()
for i in 0 ..< .random(in: 4_000_000...5_000_000) { // made it random so I could see values change
items.append("\(i)")
}
await MainActor.run { [items] in
self.items = items
}
}
}
}
为了完整起见,你也可以让视图模型成为它自己的actor,并且只指定相关的观察属性在主actor上:
actor ViewModel: ObservableObject {
@MainActor
@Published var items = [String]()
func populate() {
Task {
var items = [String]()
for i in 0 ..< .random(in: 4_000_000...5_000_000) { // made it random so I could see values change
items.append("\(i)")
}
await MainActor.run { [items] in
self.items = items
}
}
}
}
我通常倾向于前者,但这两种方法都有效。
无论哪种方式,它都会让慢进程不阻塞主线程。在这里,我点击了两次按钮:
查看 WWDC 2021 视频 Swift concurrency: Behind the scenes, Protect mutable state with Swift actors, and Swift concurrency: Update a sample app,所有这些在尝试理解从 GCD 到 Swift 并发的过渡时都很有用。
正如其他人所提到的,这种行为的原因是 Task.init
自动继承了 actor 上下文。您正在从按钮回调中调用您的函数:
Button {
Task {
await model.populate()
}
} label: {
}
按钮回调在主要角色上,因此传递给 Task
初始化程序的闭包也在主要角色上。
一种解决方案是使用分离任务:
func populate() async {
Task.detached {
// Calculation here
}
}
虽然分离任务是非结构化的,但我想建议结构化任务,例如 async let
任务:
@MainActor
class ViewModel: ObservableObject {
@Published var items = [String]()
func populate() async {
async let newItems = { () -> [String] in
var items = [String]()
for i in 0 ..< 4_000_000 {
items.append("\(i)")
}
return items
}()
items = await newItems
}
}
当您希望 populate
函数异步地 return 一些值时,这很有用。这种结构化任务方法还意味着可以自动传播取消。例如,如果你想在短时间内多次点击按钮时取消计算,你可以这样做:
@MainActor
class ViewModel: ObservableObject {
@Published var items = [String]()
func populate() async {
async let newItems = { () -> [String] in
var items = [String]()
for i in 0 ..< 4_000_000 {
// Stop in the middle if cancelled
if i % 1000 == 0 && Task.isCancelled {
break
}
items.append("\(i)")
}
return items
}()
items = await newItems
}
}
struct ContentView: View {
@StateObject var model: ViewModel
@State var task: Task<Void, Never>?
init() {
_model = StateObject(wrappedValue: ViewModel())
}
var body: some View {
Button {
task?.cancel() // Cancel previous task if any
task = Task {
await model.populate()
}
} label: {
// ...
}
}
}
此外,withTaskGroup
还会创建结构化任务,您也可以避免继承 actor 上下文。当您的计算有多个可以同时进行的子任务时,它会很有用。
我有一个 ObservableObject
class 和一个 SwiftUI 视图。点击按钮时,我会创建一个 Task
并从其中调用 populate
(异步函数)。我以为这会在后台线程上执行 populate
但整个 UI 都冻结了。这是我的代码:
class ViewModel: ObservableObject {
@Published var items = [String]()
func populate() async {
var items = [String]()
for i in 0 ..< 4_000_000 { /// this usually takes a couple seconds
items.append("\(i)")
}
self.items = items
}
}
struct ContentView: View {
@StateObject var model = ViewModel()
@State var rotation = CGFloat(0)
var body: some View {
Button {
Task {
await model.populate()
}
} label: {
Color.blue
.frame(width: 300, height: 80)
.overlay(
Text("\(model.items.count)")
.foregroundColor(.white)
)
.rotationEffect(.degrees(rotation))
}
.onAppear { /// should be a continuous rotation effect
withAnimation(.easeInOut(duration: 2).repeatForever()) {
rotation = 90
}
}
}
}
结果:
按钮停止移动,然后在 populate
完成后突然弹回。
奇怪的是,如果我将 Task
移动到 populate
本身并摆脱 async
,旋转动画不会断断续续,所以我认为循环实际上是在的背景。但是我现在收到 Publishing changes from background threads is not allowed
警告。
func populate() {
Task {
var items = [String]()
for i in 0 ..< 4_000_000 {
items.append("\(i)")
}
self.items = items /// Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
}
}
/// ...
Button {
model.populate()
}
结果:
如何确保我的代码在后台线程上执行?我认为这可能与 MainActor
有关,但我不确定。
您可以通过删除 class 来修复它。你没有使用 Combine,所以你不需要它 ObservableObject
并且如果你坚持使用值类型,SwiftUI 是最有效的。此设计不挂按钮:
extension String {
static func makeItems() async -> [String]{
var items = [String]()
for i in 0 ..< 4_000_000 { /// this usually takes a couple seconds
items.append("\(i)")
}
return items
}
}
struct AnimateContentView: View {
@State var rotation = CGFloat(0)
@State var items = [String]()
var body: some View {
Button {
Task {
items = await String.makeItems()
}
} label: {
Color.blue
.frame(width: 300, height: 80)
.overlay(
Text("\(items.count)")
.foregroundColor(.white)
)
.rotationEffect(.degrees(rotation))
}
.onAppear { /// should be a continuous rotation effect
withAnimation(.easeInOut(duration: 2).repeatForever()) {
rotation = 90
}
}
}
}
首先,你不能两全其美;您要么在主线程上执行 CPU 密集型工作(并对 UI 产生负面影响),要么在另一个线程上执行工作,但您需要显式分派 UI更新到主线程。
然而,你真正想问的是
(By using
Task
) I thought this would execute populate on a background thread but instead the entire UI freezes.
当您使用 Task
时,您正在使用 unstructured concurrency, and when you initialise your Task
via init(priority:operation) 任务...继承调用者的优先级和参与者上下文。
虽然 Task
是异步执行的,但它使用调用者的参与者上下文执行此操作,在 View
body
的上下文中是主要参与者。这意味着虽然您的任务是异步执行的,但它仍然在主线程上运行,并且该线程在处理时不可用于 UI 更新。所以你是对的,这与 MainActor
.
当您将 Task
移动到 populate
时,它不再在 MainActor
上下文中创建,因此不会在主线程上执行。
如您所见,您需要使用第二种方法来避开主线程。您需要对代码做的就是使用 MainActor
:
func populate() {
Task {
var items = [String]()
for i in 0 ..< 4_000_000 {
items.append("\(i)")
}
await MainActor.run {
self.items = items
}
}
}
您还可以在正文上下文中使用 Task.detached()
来创建不附加到 MainActor
上下文的 Task
。
考虑:
func populate() async {
var items = [String]()
for i in 0 ..< 4_000_000 {
items.append("\(i)")
}
self.items = items
}
你用async
标记了populate
,但是这里没有异步,会阻塞调用线程。
然后考虑:
func populate() {
Task {
var items = [String]()
for i in 0 ..< 4_000_000 {
items.append("\(i)")
}
self.items = items
}
}
看起来它一定是异步的,因为它正在启动 Task
。但是这个Task
运行s在同一个actor上,而且由于task是慢同步的,所以会阻塞当前的executor。
如果你不想运行在主要演员身上,你可以:
可以把view model留在main actor上,但是手动把慢同步进程移到detached task:
@MainActor class ViewModel: ObservableObject { @Published var items = [String]() func populate() { Task.detached { var items = [String]() for i in 0 ..< .random(in: 4_000_000...5_000_000) { // made it random so I could see values change items.append("\(i)") } await MainActor.run { [items] in self.items = items } } } }
为了完整起见,你也可以让视图模型成为它自己的actor,并且只指定相关的观察属性在主actor上:
actor ViewModel: ObservableObject { @MainActor @Published var items = [String]() func populate() { Task { var items = [String]() for i in 0 ..< .random(in: 4_000_000...5_000_000) { // made it random so I could see values change items.append("\(i)") } await MainActor.run { [items] in self.items = items } } } }
我通常倾向于前者,但这两种方法都有效。
无论哪种方式,它都会让慢进程不阻塞主线程。在这里,我点击了两次按钮:
查看 WWDC 2021 视频 Swift concurrency: Behind the scenes, Protect mutable state with Swift actors, and Swift concurrency: Update a sample app,所有这些在尝试理解从 GCD 到 Swift 并发的过渡时都很有用。
正如其他人所提到的,这种行为的原因是 Task.init
自动继承了 actor 上下文。您正在从按钮回调中调用您的函数:
Button {
Task {
await model.populate()
}
} label: {
}
按钮回调在主要角色上,因此传递给 Task
初始化程序的闭包也在主要角色上。
一种解决方案是使用分离任务:
func populate() async {
Task.detached {
// Calculation here
}
}
虽然分离任务是非结构化的,但我想建议结构化任务,例如 async let
任务:
@MainActor
class ViewModel: ObservableObject {
@Published var items = [String]()
func populate() async {
async let newItems = { () -> [String] in
var items = [String]()
for i in 0 ..< 4_000_000 {
items.append("\(i)")
}
return items
}()
items = await newItems
}
}
当您希望 populate
函数异步地 return 一些值时,这很有用。这种结构化任务方法还意味着可以自动传播取消。例如,如果你想在短时间内多次点击按钮时取消计算,你可以这样做:
@MainActor
class ViewModel: ObservableObject {
@Published var items = [String]()
func populate() async {
async let newItems = { () -> [String] in
var items = [String]()
for i in 0 ..< 4_000_000 {
// Stop in the middle if cancelled
if i % 1000 == 0 && Task.isCancelled {
break
}
items.append("\(i)")
}
return items
}()
items = await newItems
}
}
struct ContentView: View {
@StateObject var model: ViewModel
@State var task: Task<Void, Never>?
init() {
_model = StateObject(wrappedValue: ViewModel())
}
var body: some View {
Button {
task?.cancel() // Cancel previous task if any
task = Task {
await model.populate()
}
} label: {
// ...
}
}
}
此外,withTaskGroup
还会创建结构化任务,您也可以避免继承 actor 上下文。当您的计算有多个可以同时进行的子任务时,它会很有用。