第12篇多线程编程——并发与协程**作者**中文编程倡导者—— 李金雨联系方式wbtm2718qq.com目标掌握Go的并发编程学会使用协程和通道预计时间4课时180分钟难度⭐⭐⭐⭐⭐挑战一、开篇引入1.1 本课目标学完本篇你将能够✅ 理解什么是并发和多线程✅ 掌握协程Goroutine的使用✅ 掌握通道Channel的使用✅ 理解同步和异步的概念✅ 学会使用WaitGroup等待协程完成✅ 掌握select多路复用1.2 生活场景引入多线程就像餐厅的后厨想象一下餐厅的后厨‍主厨- 负责统筹全局主功能‍炒菜师傅- 专门炒菜协程1‍切菜师傅- 专门切菜协程2‍洗碗师傅- 专门洗碗协程3单线程 vs 多线程单线程一个人做所有事做完一道菜再做下一道多线程多个人同时工作有人炒菜、有人切菜、有人洗碗Go的协程Goroutine就是轻量级的线程可以高效地并发执行任务1.3 预期成果展示运行今天的程序后你会看到 多线程下载器 开始下载3个文件... [文件1] 开始下载... [文件2] 开始下载... [文件3] 开始下载... [文件2] 下载完成耗时1.2秒 [文件1] 下载完成耗时2.5秒 [文件3] 下载完成耗时3.1秒 全部下载完成总耗时3.1秒 串行下载需要6.8秒 二、概念讲解2.1 什么是并发并发 同时处理多个任务生活中的并发一边听音乐一边写作业一边走路一边说话厨师同时炒多道菜程序中的并发同时下载多个文件同时处理多个用户请求后台执行任务并发 vs 并行概念解释比喻并发交替执行多个任务一个人轮流做多个菜并行同时执行多个任务多个人同时做多个菜Go的协程Goroutine轻量级线程由Go运行时管理占用内存少几KB切换开销小可以轻松创建成千上万个协程2.2 创建协程使用go关键字创建协程packagemainimport(fmttime)// 普通功能func任务(名称string){fori:1;i3;i{fmt.Printf(%s 执行第 %d 次\n,名称,i)time.Sleep(100*time.Millisecond)}}funcmain(){// 串行执行fmt.Println( 串行执行 )任务(任务A)任务(任务B)fmt.Println()// 并发执行使用协程fmt.Println( 并发执行 )go任务(协程A)// 创建协程go任务(协程B)// 创建协程// 等待协程完成主功能不能太快结束time.Sleep(1*time.Second)fmt.Println(主功能结束)}输出结果 串行执行 任务A 执行第 1 次 任务A 执行第 2 次 任务A 执行第 3 次 任务B 执行第 1 次 任务B 执行第 2 次 任务B 执行第 3 次 并发执行 协程A 执行第 1 次 协程B 执行第 1 次 协程A 执行第 2 次 协程B 执行第 2 次 协程A 执行第 3 次 协程B 执行第 3 次 主功能结束2.3 匿名协程使用匿名功能创建协程packagemainimport(fmttime)funcmain(){fmt.Println(开始执行任务...)// 创建匿名协程gofunc(){fori:1;i5;i{fmt.Printf(后台任务第 %d 次执行\n,i)time.Sleep(200*time.Millisecond)}}()// 主功能继续执行fori:1;i3;i{fmt.Printf(主功能第 %d 次执行\n,i)time.Sleep(300*time.Millisecond)}// 等待后台任务完成time.Sleep(2*time.Second)fmt.Println(所有任务完成)}2.4 通道Channel通道 协程之间通信的管道为什么需要通道协程之间需要交换数据协调协程的执行顺序避免数据竞争创建和使用通道packagemainimport(fmttime)funcmain(){// 创建通道传递字符串消息通道:make(chanstring)// 创建协程向通道发送消息gofunc(){fori:1;i3;i{消息:fmt.Sprintf(消息 %d,i)fmt.Printf(发送%s\n,消息)消息通道-消息// 发送消息到通道time.Sleep(200*time.Millisecond)}close(消息通道)// 关闭通道}()// 主功能从通道接收消息for消息:range消息通道{fmt.Printf(接收%s\n,消息)}fmt.Println(通信结束)}输出结果发送消息 1 接收消息 1 发送消息 2 接收消息 2 发送消息 3 接收消息 3 通信结束2.5 带缓冲的通道缓冲通道 可以存储多个值的通道packagemainimportfmtfuncmain(){// 创建带缓冲的通道容量为3缓冲通道:make(chanint,3)// 发送数据不会阻塞因为还有空间缓冲通道-1缓冲通道-2缓冲通道-3fmt.Println(发送了3个数据)// 接收数据fmt.Printf(接收%d\n,-缓冲通道)fmt.Printf(接收%d\n,-缓冲通道)fmt.Printf(接收%d\n,-缓冲通道)}2.6 WaitGroup等待协程WaitGroup 等待多个协程完成packagemainimport(fmtsynctime)func下载文件(文件名string,等待组*sync.WaitGroup){defer等待组.Done()// 完成后减少计数fmt.Printf(开始下载 %s...\n,文件名)time.Sleep(time.Duration(12)*time.Second)// 模拟下载时间fmt.Printf(%s 下载完成\n,文件名)}funcmain(){var等待组 sync.WaitGroup 文件列表:[]string{文件1.zip,文件2.zip,文件3.zip}fmt.Println(开始并发下载...)// 启动多个协程for_,文件名:range文件列表{等待组.Add(1)// 增加计数go下载文件(文件名,等待组)}// 等待所有协程完成等待组.Wait()fmt.Println(所有文件下载完成)}2.7 Select多路复用Select 同时监听多个通道packagemainimport(fmttime)funcmain(){通道1:make(chanstring)通道2:make(chanstring)// 协程1gofunc(){time.Sleep(1*time.Second)通道1-来自通道1的消息}()// 协程2gofunc(){time.Sleep(2*time.Second)通道2-来自通道2的消息}()// 使用select监听多个通道fori:0;i2;i{select{case消息1:-通道1:fmt.Printf(收到%s\n,消息1)case消息2:-通道2:fmt.Printf(收到%s\n,消息2)}}}三、动手实践3.1 基础练习必做练习1并发计数器packagemainimport(fmtsynctime)func计数(名称string,等待组*sync.WaitGroup){defer等待组.Done()fori:1;i5;i{fmt.Printf(%s: %d\n,名称,i)time.Sleep(100*time.Millisecond)}}funcmain(){var等待组 sync.WaitGroup fmt.Println( 并发计数 )// 启动3个计数协程fori:1;i3;i{等待组.Add(1)go计数(fmt.Sprintf(计数器%d,i),等待组)}等待组.Wait()fmt.Println(全部完成)}练习2生产者消费者packagemainimport(fmttime)// 生产者func生产者(通道chan-int,数量int){fori:1;i数量;i{fmt.Printf(生产者生产%d\n,i)通道-i time.Sleep(100*time.Millisecond)}close(通道)}// 消费者func消费者(通道-chanint,名称string){for产品:range通道{fmt.Printf(%s 消费%d\n,名称,产品)time.Sleep(200*time.Millisecond)}}funcmain(){通道:make(chanint,5)// 启动生产者和消费者go生产者(通道,10)go消费者(通道,消费者A)go消费者(通道,消费者B)// 等待完成time.Sleep(3*time.Second)fmt.Println(生产消费完成)}练习3并发下载模拟packagemainimport(fmtmath/randsynctime)func下载(网址string,等待组*sync.WaitGroup,结果通道chan-string){defer等待组.Done()// 模拟随机下载时间耗时:time.Duration(rand.Intn(3)1)*time.Second fmt.Printf(开始下载 %s...\n,网址)time.Sleep(耗时)结果:fmt.Sprintf(%s 下载完成耗时%v,网址,耗时)结果通道-结果}funcmain(){rand.Seed(time.Now().Unix())var等待组 sync.WaitGroup 结果通道:make(chanstring,5)网址列表:[]string{https://example.com/file1.zip,https://example.com/file2.zip,https://example.com/file3.zip,}fmt.Println( 并发下载 )开始时间:time.Now()// 启动下载协程for_,网址:range网址列表{等待组.Add(1)go下载(网址,等待组,结果通道)}// 等待所有下载完成gofunc(){等待组.Wait()close(结果通道)}()// 收集结果for结果:range结果通道{fmt.Println(结果)}总耗时:time.Since(开始时间)fmt.Printf(\n全部完成总耗时%v\n,总耗时)}3.2 进阶练习选做练习4并发爬虫模拟packagemainimport(fmtmath/randsynctime)// 网页结构体type网页struct{URLstring标题string内容string}// 爬取网页func爬取(URLstring,结果通道chan-网页,等待组*sync.WaitGroup){defer等待组.Done()// 模拟爬取时间耗时:time.Duration(rand.Intn(2)1)*time.Second time.Sleep(耗时)网页:网页{URL:URL,标题:fmt.Sprintf(标题 of %s,URL),内容:fmt.Sprintf(内容 of %s,URL),}结果通道-网页}funcmain(){rand.Seed(time.Now().Unix())var等待组 sync.WaitGroup 结果通道:make(chan网页,10)URL列表:[]string{https://site1.com,https://site2.com,https://site3.com,https://site4.com,https://site5.com,}fmt.Println( 并发爬虫 )// 启动爬取协程for_,URL:rangeURL列表{等待组.Add(1)go爬取(URL,结果通道,等待组)}// 等待并关闭通道gofunc(){等待组.Wait()close(结果通道)}()// 收集结果var网页列表[]网页for网页:range结果通道{网页列表append(网页列表,网页)fmt.Printf(爬取完成%s - %s\n,网页.URL,网页.标题)}fmt.Printf(\n共爬取 %d 个网页\n,len(网页列表))}3.3 挑战练习拓展练习5工作池模式packagemainimport(fmtsynctime)// 任务结构体type任务struct{IDint数据int}// 结果结构体type结果struct{任务IDint结果int}// 工作者func工作者(编号int,任务通道-chan任务,结果通道chan-结果,等待组*sync.WaitGroup){defer等待组.Done()for任务:range任务通道{// 模拟处理fmt.Printf(工作者%d 处理任务%d\n,编号,任务.ID)time.Sleep(500*time.Millisecond)结果通道-结果{任务ID:任务.ID,结果:任务.数据*2,}}}funcmain(){const工作者数量3const任务数量10任务通道:make(chan任务,任务数量)结果通道:make(chan结果,任务数量)var等待组 sync.WaitGroup// 启动工作者fori:1;i工作者数量;i{等待组.Add(1)go工作者(i,任务通道,结果通道,等待组)}// 发送任务gofunc(){fori:1;i任务数量;i{任务通道-任务{ID:i,数据:i*10}}close(任务通道)}()// 等待工作者完成gofunc(){等待组.Wait()close(结果通道)}()// 收集结果fmt.Println( 工作池模式 )for结果:range结果通道{fmt.Printf(任务%d 完成结果%d\n,结果.任务ID,结果.结果)}fmt.Println(所有任务完成)}四、知识总结4.1 核心概念回顾概念解释例子协程轻量级线程go 功能()通道协程通信管道make(chan int)缓冲通道带存储的通道make(chan int, 10)WaitGroup等待协程完成sync.WaitGroupSelect多路复用select { case ... }4.2 关键代码速查// 创建协程go功能名()gofunc(){...}()// 创建通道通道:make(chan类型)缓冲通道:make(chan类型,容量)// 发送和接收通道-值// 发送值:-通道// 接收// WaitGroupvarwg sync.WaitGroup wg.Add(数量)wg.Done()wg.Wait()// Selectselect{case值1:-通道1:// 处理case值2:-通道2:// 处理case通道3-值3:// 处理}4.3 常见错误提醒错误原因解决方法死锁通道发送和接收不匹配确保有发送就有接收协程泄漏协程永远阻塞使用超时或关闭通道数据竞争多个协程同时读写数据使用通道或锁主功能提前结束协程还没执行完使用WaitGroup等待五、课后作业5.1 巩固练习题并发文件处理创建多个协程同时处理多个文件统计每个文件的行数。并发排序将一个大数组分成多份用多个协程分别排序最后合并。定时任务使用协程和通道实现一个定时提醒功能。5.2 创意编程题制作并发下载器要求包含支持同时下载多个文件显示每个文件的下载进度统计总下载速度和耗时支持取消下载示例输出 并发下载器 开始下载5个文件... [文件1] [██████░░░░░░░░░░] 37% 1.2MB/s [文件2] [████████████░░░░] 75% 2.5MB/s [文件3] [░░░░░░░░░░░░░░░░] 5% 0.8MB/s ... 全部下载完成总耗时15.3秒 平均速度2.1MB/s 5.3 下篇预习提示下一篇我们将学习“综合项目——并发聊天室”你将学会使用协程处理多个客户端使用通道广播消息实现简单的网络通信预习问题如何实现多人同时聊天如何将消息发送给所有在线用户附录代码模板// 第12篇练习模板// 作者____________// 日期____________packagemainimport(fmtsynctime)func我的协程(编号int,等待组*sync.WaitGroup){defer等待组.Done()fmt.Printf(协程%d 开始执行\n,编号)time.Sleep(1*time.Second)fmt.Printf(协程%d 执行完成\n,编号)}funcmain(){var等待组 sync.WaitGroup// 启动多个协程fori:1;i3;i{等待组.Add(1)go我的协程(i,等待组)}// 等待所有协程完成等待组.Wait()fmt.Println(全部完成)}学习感悟并发编程是Go语言的强项通过协程和通道我们可以轻松编写高效的并发程序。记住不要通过共享内存来通信而是通过通信来共享内存下篇预告《第13篇综合项目——并发聊天室》本教程由AI辅助生成专为初中生设计的Go语言中文编程入门教程作者李金雨联系方式wbtm2718qq.com