☰
【SwiftUI提高练中学】第1课 并发与数据流:Task、Actor、@MainActor、Sendable
2026/9/30 2:35:39 网站建设 项目流程

本课目标:
入门第8课你用过 async/await 请求网络。进阶第1课要解决三个关键问题:代码在哪个线程/actor 上执行、共享状态如何安全修改、任务如何取消和并行。掌握 Task、@MainActor、actor、Sendable、TaskGroup,写出不卡界面、不数据竞争、可取消的 SwiftUI 代码。

一、核心概念

· async/await:异步函数和等待结果,入门已接触。
· Task:创建一个异步任务,可取消、可继承 actor 上下文。
· @MainActor:把类、方法或属性隔离到主 actor,保证 UI 更新在主线程。
· actor:保护内部可变状态,外部访问必须 await。
· Sendable:标记可安全跨并发域传递的类型,编译器会检查。
· Task.checkCancellation() / Task.isCancelled:协作式取消检查。
· withTaskGroup / withThrowingTaskGroup:结构化并行任务。
· Task.detached:脱离当前 actor 和优先级的非结构化任务,慎用。
· nonisolated:让方法不隔离到 actor,可在任意并发域调用。
· Swift 6 严格并发:编译期检查数据竞争,越早理解越省事。

二、示例代码:可取消搜索 + actor 缓存 + 并行加载

importSwiftUIimportObservation// @MainActor:整个 ViewModel 隔离到主 actor,UI 更新安全// @Observable:属性变化自动驱动界面刷新@MainActor@ObservablefinalclassSearchViewModel{varquery=""// 搜索词private(set)varresults:[String]=[]// 搜索结果private(set)varisLoading=false// 是否加载中// 保存当前搜索任务,便于取消旧任务privatevarsearchTask:Task<Void,Never>?funcsearch(){// 取消上一次搜索,避免旧结果覆盖新结果searchTask?.cancel()searchTask=Task{isLoading=truedefer{isLoading=false}// 无论成功失败都结束加载do{// 防抖:等待 300 毫秒,期间被取消会抛 CancellationErrortryawaitTask.sleep(for:.milliseconds(300))tryTask.checkCancellation()// 主动检查取消// 调用异步服务,内部会 await actor 缓存letitems=tryawaitDataService.shared.fetch(query:query)tryTask.checkCancellation()results=items// 回到主 actor 更新 UI}catchisCancellationError{// 被取消:不更新结果}catch{results=[]}}}deinit{searchTask?.cancel()// 视图销毁时取消任务}}// actor:保护内部可变状态,外部访问必须 awaitactorCache{privatevarstorage:[String:[String]]=[:]funcvalue(forkey:String)->[String]?{storage[key]}funcset(_value:[String],forkey:String){storage[key]=value}}// Sendable:值类型且属性都 Sendable,可安全跨并发域传递structDataService:Sendable{staticletshared=DataService()// actor 是 Sendable,可以安全持有privateletcache=Cache()// 模拟网络请求,带缓存funcfetch(query:String)asyncthrows->[String]{ifletcached=awaitcache.value(for:query){returncached// 命中缓存,直接返回}tryawaitTask.sleep(for:.milliseconds(500))// 模拟耗时letresult=(1...5).map{"\(query)结果\($0)"}awaitcache.set(result,for:query)// 写入 actorreturnresult}// 单个项目加载,供 TaskGroup 使用funcfetchItem(id:Int)asyncthrows->String{tryawaitTask.sleep(for:.milliseconds(300))return"项目\(id)"}// 结构化并行:同时加载多个项目funcloadAll(ids:[Int])asyncthrows->[String]{tryawaitwithThrowingTaskGroup(of:String.self){groupinforidinids{group.addTask{tryawaitself.fetchItem(id:id)}}varresults:[String]=[]fortryawaititemingroup{results.append(item)}returnresults}}}structSearchView:View{@StateprivatevarviewModel=SearchViewModel()varbody:someView{@BindablevarviewModel=viewModel// 获取 @Observable 的绑定NavigationStack{List(viewModel.results,id:\.self){iteminText(item)}.overlay{ifviewModel.isLoading{ProgressView()}}.searchable(text:$viewModel.query).onChange(of:viewModel.query){_,_inviewModel.search()// 搜索词变化就重新搜索}.navigationTitle("并发搜索")}}}#Preview{SearchView()}

解读:SearchViewModel 是 @MainActor,所有 UI 状态安全;Cache 是 actor,内部字典受保护;DataService 是 Sendable 值类型,可安全共享;TaskGroup 并行加载多个项目;旧搜索任务会被取消,避免结果错乱。

三、习题、答案与解读

选择题

题1

题目:保证 UI 更新在主 actor 上,使用哪个标注?
A. @MainActor B. @State C. @Binding D. @Query
要求:选出正确选项。
答案:A
解读:@MainActor 把类、方法或属性隔离到主 actor,UI 更新安全。

题2

题目:保护内部可变共享状态,使用哪个声明?
A. actor B. class C. struct D. enum
要求:选出正确选项。
答案:A
解读:actor 内部状态只能通过 await 访问,避免数据竞争。

题3

题目:主动检查任务是否被取消,使用?
A. Task.checkCancellation() B. Task.sleep() C. Task.detached D. Task.yield()
要求:选出正确选项。
答案:A
解读:try Task.checkCancellation() 在取消时抛出 CancellationError。

题4

题目:并行执行多个子任务,使用?
A. withThrowingTaskGroup B. Task { } C. async let 之外还可以?
要求:选出正确选项。
答案:A
解读:withThrowingTaskGroup 是结构化并行,适合动态数量任务。

题5

题目:标记可安全跨并发域传递的类型,使用哪个协议?
A. Sendable B. Codable C. Identifiable D. Hashable
要求:选出正确选项。
答案:A
解读:Sendable 让编译器检查跨 actor 传递是否安全。

判断题

题1

题目:Task 取消后会自动立即停止任务。
要求:判断对错。
答案:错
解读:取消是协作式的,任务需要自己检查 Task.isCancelled 或调用 checkCancellation()。

题2

题目:actor 的属性可以直接从外部读写。
要求:判断对错。
答案:错
解读:外部访问 actor 属性和方法必须 await,不能直接读写。

题3

题目:@MainActor 类的方法默认在主 actor 上执行。
要求:判断对错。
答案:对
解读:隔离到主 actor 后,方法调用会在主线程执行。

题4

题目:Sendable 会在编译期检查数据竞争风险。
要求:判断对错。
答案:对
解读:Swift 并发用 Sendable 在编译期减少数据竞争。

题5

题目:Task.detached 会继承当前 actor 上下文。
要求:判断对错。
答案:错
解读:Task.detached 不继承 actor、优先级和任务本地值,慎用。

填空题

题1

题目:UI 隔离使用 @______。
要求:填入空白处。
答案:MainActor
解读:@MainActor 保证主线程更新。

题2

题目:保护共享状态使用 ______。
要求:填入空白处。
答案:actor
解读:actor Cache { … }。

题3

题目:检查取消使用 Task.______()。
要求:填入空白处。
答案:checkCancellation
解读:try Task.checkCancellation()。

题4

题目:并行任务组使用 withThrowing______。
要求:填入空白处。
答案:TaskGroup
解读:withThrowingTaskGroup(of:) { group in … }。

题5

题目:跨并发传递安全协议是 ______。
要求:填入空白处。
答案:Sendable
解读:struct DataService: Sendable。

改错题

题1

题目:

actorCache{varstorage:[String:String]=[:]}letcache=Cache()cache.storage["key"]="value"

要求:找出错误并改正。
答案:

actorCache{privatevarstorage:[String:String]=[:]funcset(_value:String,forkey:String){storage[key]=value}}letcache=Cache()awaitcache.set("value",for:"key")

解读:actor 属性不能外部直接访问,必须通过方法并 await。

题2

题目:

@MainActorclassViewModel{funcload(){Task.detached{self.results=["A"]// 更新 UI 状态}}}

要求:找出问题并改正。
答案:

@MainActorclassViewModel{funcload(){Task{self.results=["A"]// Task 继承主 actor,安全更新}}}

解读:Task.detached 不继承主 actor,直接更新 UI 状态不安全。

题3

题目:

funcsearch(){Task{tryawaitTask.sleep(for:.seconds(1))letdata=tryawaitfetch()results=data}}

要求:增加取消检查,如何修改?
答案:

funcsearch(){Task{tryawaitTask.sleep(for:.seconds(1))tryTask.checkCancellation()letdata=tryawaitfetch()tryTask.checkCancellation()results=data}}

解读:耗时操作前后都应检查取消,避免无效更新。

代码预测题

题目:

@MainActor@ObservablefinalclassVM{varquery=""varresults:[String]=[]privatevartask:Task<Void,Never>?funcsearch(){task?.cancel()task=Task{try?awaitTask.sleep(for:.milliseconds(300))ifTask.isCancelled{return}results=["\(query)的结果"]}}}

要求:快速输入“A”再输入“B”,最终 results 显示什么?
答案:显示“B 的结果”。
解读:输入“A”的任务被取消,Task.isCancelled 为 true 直接返回,不会覆盖“B”的结果。

编程题

题1

题目:写一个 @MainActor @Observable 的搜索 ViewModel。
要求:搜索词变化时取消旧任务,延迟 300 毫秒后加载结果。
参考代码:

@MainActor@ObservablefinalclassSearchViewModel{varquery=""private(set)varresults:[String]=[]privatevartask:Task<Void,Never>?funcsearch(){task?.cancel()task=Task{do{tryawaitTask.sleep(for:.milliseconds(300))tryTask.checkCancellation()results=tryawaitDataService.shared.fetch(query:query)}catchisCancellationError{// 忽略取消}catch{results=[]}}}}

解读:取消旧任务 + 防抖 + 取消检查,是搜索场景的标准写法。

题2

题目:用 actor 写一个线程安全计数器。
要求:支持加一、读取当前值。
参考代码:

actorCounter{privatevarvalue=0funcincrement(){value+=1}funccurrent()->Int{value}}letcounter=Counter()awaitcounter.increment()letvalue=awaitcounter.current()

解读:actor 内部状态安全,外部通过 await 访问。

题3

题目:挑战——用 TaskGroup 并行加载 5 个项目。
要求:返回所有结果数组。
参考代码:

funcloadAll(ids:[Int])asyncthrows->[String]{tryawaitwithThrowingTaskGroup(of:String.self){groupinforidinids{group.addTask{tryawaitDataService.shared.fetchItem(id:id)}}varresults:[String]=[]fortryawaititemingroup{results.append(item)}returnresults}}

解读:TaskGroup 让多个请求并行执行,最后汇总结果。

开放思考题

题1

题目:为什么 UI 更新必须在主 actor?
要求:简要回答。
参考:UIKit/SwiftUI 的界面操作不是线程安全的,必须在主线程串行执行。
解读:@MainActor 让编译器帮你保证这一点。

题2

题目:actor 和锁有什么区别?
要求:简要回答。
参考:actor 用异步隔离保护状态,编译器检查;锁是手动同步,容易死锁。
解读:Swift 并发优先用 actor,少用锁。

题3

题目:为什么任务取消是协作式的?
要求:简要回答。
参考:系统不能强行终止任意代码,任务需要主动检查取消。
解读:Task.checkCancellation() 和 Task.isCancelled 是标准做法。

四、全课知识点总结

· @MainActor:把类、方法、属性隔离到主 actor,保证 UI 更新安全。
· @Observable:属性变化自动驱动 SwiftUI 刷新。
· Task:创建异步任务,可取消,可继承 actor 上下文。
· Task.detached:脱离当前上下文,慎用。
· actor:保护内部可变状态,外部访问必须 await。
· Sendable:标记可安全跨并发域传递的类型,编译期检查。
· Task.checkCancellation() / Task.isCancelled:协作式取消检查。
· Task.sleep(for:):异步等待,可被取消。
· withThrowingTaskGroup:结构化并行任务,适合动态数量。
· async let:适合固定数量并行。
· 搜索防抖标准写法:取消旧任务 → 延迟 → 检查取消 → 更新结果。
· Swift 6 严格并发:越早用 Sendable 和 actor 隔离,越容易迁移。
· 并发安全三原则:UI 用 @MainActor,共享状态用 actor,跨域传递用 Sendable。

本课达标标准:
能写 @MainActor @Observable ViewModel,能用 Task 取消和防抖,能用 actor 保护共享状态,能用 TaskGroup 并行加载,能理解 Sendable 和协作式取消,并能把并发安全地接入 SwiftUI 界面。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询