在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
GO语言的进阶之路-协程和Channel 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任。
看过我之前几篇博客小伙伴可能对Golang语言的语法上了解的差不多了,但是,如果想要你的代码和性能更高,那还得学点晋升的本来,这个时候我们就需要引入Golang的协成这个概念了,其实,你可能早就听说了Golang的优势就是处理大并发,我们可以用它来做日志收集系统,也可以用它做业务上的“秒杀系统”,当然我们还可以用它来做“监控系统”。好了,下面跟我一起来体会一下Golang的五味杂陈吧。
一.什么是协程; 再说协成之前,我们需要了解两个概念,即用户态和内核态。 1.什么是用户态; 官方解释:用户态(user mode)在计算机结构指两项类似的概念。在CPU的设计中,用户态指非特权状态。在此状态下,执行的代码被硬件限定,不能进行某些操作,比如写入其他进程的存储空间,以防止给操作系统带来安全隐患。在操作系统的设计中,用户态也类似,指非特权的执行状态。内核禁止此状态下的代码进行潜在危险的操作,比如写入系统配置文件、杀掉其他用户的进程、重启系统等。 应用程序在用户态下运行,仅仅只能执行cpu整个指令集的一个子集,该子集中不包含操作硬件功能的部分,因此,一般情况下,在用户态中有关I/O和内存保护(操作系统占用的内存是受保护的,不能被别的程序占用)。 如果感兴趣的朋友可以参考:https://baike.baidu.com/item/%E7%94%A8%E6%88%B7%E6%80%81/9548791?fr=aladdin
2.什么是内核态; 内核态也叫和核心态。 官方解释:在处理器的存储保护中,主要有两种权限状态,一种是核心态(管态),也被称为特权态;一种是用户态(目态)。核心态是操作系统内核所运行的模式,运行在该模式的代码,可以无限制地对系统存储、外部设备进行访问。 操作系统在内核态运行情况下可以访问硬件上所有的内容。 如果感兴趣的朋友可以参考:https://baike.baidu.com/item/%E6%A0%B8%E5%BF%83%E6%80%81/6845908?fr=aladdin
3.什么是协成; 官方解释:一个程序可以包含多个协程,可以对比与一个进程包含多个线程,因而下面我们来比较协程和线程。我们知道多个线程相对独立,有自己的上下文,切换受系统控制;而协程也相对独立,有自己的上下文,但是其切换由自己控制,由当前协程切换到其他协程由当前协程来控制。 协程(coroutine)是Go语言中的轻量级线程实现,由Go运行时(runtime)管理。在一个函数调用前加上go关键字,这次调用就会在一个新的goroutine中并发执行。当被调用的函数返回时,这个goroutine也自动结束。需要注意的是,如果这个函数有返回值,那么这个返回值会被丢弃。协成工作在用户态,它类似于现场的运行方式可以并行处理任务。
二.创建一个协程; 在一个函数调用前加上go关键字,这次调用就会在一个新的goroutine中并发执行,下面我们一起来看段代码的执行结果: 1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:[email protected] 6 */ 7 8 package main 9 10 import ( 11 "time" 12 "fmt" 13 ) 14 15 func sayhi(s string) { 16 for i := 0; i < 5; i++ { 17 time.Sleep(100*time.Millisecond) //表示每次循环后都要休息100毫秒。 18 fmt.Println(s) 19 } 20 } 21 22 func main() { 23 go sayhi("尹正杰") //在函数执行前加个go,表示单独起了一个协程,表示和当前主协程(main)并驾齐驱运行代码。 24 sayhi("Golang") 25 } 26 27 28 #以上代码执行结果如下: 29 Golang 30 尹正杰 31 尹正杰 32 Golang 33 Golang 34 尹正杰 35 尹正杰 36 Golang 37 Golang 38 尹正杰 相信大家已经看到了一些端倪,我们先定义了一个“sayhi”函数,这个函数的功能就是将传入的字符串打印5遍,然后我们在主程序上调用的时候,发现在函数面前加"go"关键字时,就会打印输出,但是奇怪的是没有按照顺序打,它没有先打印五遍“尹正杰”然后在打印5遍“Golang”,而是函数执行结果是不规律的打印了两个字符串。这就是并发的效果,两个函数同事运行效果。这也是我们的并发之路的初体验。哈哈~
三.协程的局限性; 发现让两端代码同事运行貌似听起来很高大上的样子,但是有个局限性,就是当主进程结束后,协成也会跟着结束,其实这个很好理解,我们通过一段代码就知道了: 1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:[email protected] 6 */ 7 8 package main 9 10 import ( 11 "time" 12 "fmt" 13 ) 14 15 func main() { 16 s := []int{2,7,1,6,4,3,11,15,17,5,8,9,12} 17 for _,n := range s{ 18 go func(n int) { //定义一个匿名函数,并对该函数开启协程,每次循环都会开启一个协成,也就是说它开启了13个协程。 19 time.Sleep(time.Duration(n) * time.Second) /*表示每循环一次就需要睡1s,睡的总时间是由n来控制的, 20 总长度是由s切片数组中最大的一个数字决定,也就是说这个协成最少需要17秒才会结束哟*/ 21 fmt.Println(n) 22 }(n) //由于这个函数是匿名函数,所以调用方式就直接:(n)调用,不用输入函数名。 23 } 24 time.Sleep(12*time.Second) //主进程要执行的时间是12秒 25 } 26 27 28 29 #以上代码执行结果如下: 30 1 31 2 32 3 33 4 34 5 35 6 36 7 37 8 38 9 39 11 40 12 是不是很神奇,直接就进行排序了,但是15和17没有在终端输出,为什么呢?这就是我所说的主进程结束,那么整个程序也终将结束,因为主程序的时间只有12s,如果你想让所有的数据都排序出来,就得把数字换成一个大于或等于17的,这样才会对所有的数字进行排序。因为协成13个协程都要执行完毕的话需要17s才好使。
四.锁; 1.为什么要引入锁; 在线上生活中,我们为了避免多个用户操作同一个文件,就会定义一个锁,为什么我们需要一个锁呢?我们可以看以下的案例: 1 package main 2 3 import ( 4 "time" 5 "fmt" 6 ) 7 8 type Accout struct { 9 money int 10 } 11 12 func (a *Accout) Do_Prepare() { 13 time.Sleep(time.Second) 14 } 15 16 func (a *Accout) Get_Gongzi(n int) { 17 a.money += n 18 } 19 20 func (a *Accout) Give_Wife(n int) { 21 if a.money > n { 22 a.Do_Prepare() 23 a.money -= n 24 } 25 } 26 27 func (a *Accout) Buy(n int) { 28 if a.money >n { 29 a.Do_Prepare() 30 a.money -= n 31 } 32 } 33 34 func (a *Accout) Left()int { 35 return a.money 36 } 37 38 func main() { 39 var account Accout 40 account.Get_Gongzi(10000) 41 go account.Give_Wife(6000) 42 go account.Buy(5000) 43 time.Sleep(2 * time.Second) //不能让主程序结束掉,因为主进程一结束go的协程也就跟着结束啦。 44 fmt.Println(account.Left()) 45 } 46 47 48 49 #以上代码执行结果如下: 50 -1000
估计大家都看出来端倪了,在让两个协程并发跑起来,发现你得到的10000块钱都让给花出去了,是不是老尴尬了,写的判断语句也是没有生效的,最终10000块钱的工资,它竟然花了11000,如果银行这么干早就倒闭了,哈哈~那么问题来了,如果解决这一种现象呢?目前有四种常见的处理方案即:互斥锁,读写锁和channel锁以及waitgroup等等。
2.互斥锁; 互斥锁是传统的并发程序对共享资源进行访问控制的主要手段。它由标准库代码包sync中的Mutex结构体类型代表。sync.Mutex类型(确切地说,是*sync.Mutex类型)只有两个公开方法——Lock和Unlock。顾名思义,前者被用于锁定当前的互斥量,而后者则被用来对当前的互斥量进行解锁。 1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:[email protected] 6 */ 7 8 package main 9 10 import ( 11 "time" 12 "fmt" 13 "sync" 14 ) 15 16 17 type Accout struct { 18 flag sync.Mutex 19 money int 20 } 21 22 func (a *Accout) Do_Prepare() { 23 time.Sleep(time.Second) 24 } 25 26 func (a *Accout) Get_Salary(n int) { //定义发的工资 27 a.money += n 28 } 29 30 func (a *Accout) Give_Wife(n int) { //上交给妻子的工资 31 a.flag.Lock() 32 defer a.flag.Unlock() 33 if a.money > n { 34 a.Do_Prepare() 35 a.money -= n 36 }else { 37 fmt.Println("您的余额已不足!请及时充值~") 38 } 39 } 40 41 func (a *Accout) Buy(n int) { //自己买的工资 42 a.flag.Lock() 43 defer a.flag.Unlock() 44 if a.money >n { 45 a.Do_Prepare() 46 a.money -= n 47 }else { 48 fmt.Println("您的余额已不足!请及时充值~") 49 } 50 } 51 52 func (a *Accout) Left()int { 53 return a.money 54 } 55 56 func main() { 57 var account Accout 58 account.Get_Salary(10000) 59 go account.Give_Wife(6000) 60 go account.Buy(5000) 61 time.Sleep(2 * time.Second) //不能让主程序结束掉,因为主进程一结束go的协程也就跟着结束啦。 62 fmt.Printf("您的剩余工资是\033[31;1m%d\033[0m",account.Left()) 63 64 } 65 66 67 68 #以上代码解析如下: 69 您的余额已不足!请及时充值~ 70 您的剩余工资是4000
3.读写锁; 在Go语言中,读写锁由结构体类型sync.RWMutex代表。与互斥锁类似,sync.RWMutex类型的零值就已经是立即可用的读写锁了。 读写锁即是针对于读写操作的互斥锁。它与普通的互斥锁最大的不同就是,它可以分别针对读操作和写操作进行锁定和解锁操作。读写锁控制下的多个写操作之间都是互斥的,并且写操作与读操作之间也都是互斥的。但是,多个读操作之间却不存在互斥关系。
A.多个读操作之间却不存在互斥关系; 我们会发现读取操作没有存在互斥的关系。5个进程同事进行读取,最终都顺利并行跑完了。 1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:[email protected] 6 */ 7 8 package main 9 10 import ( 11 "sync" 12 "time" 13 ) 14 15 var m *sync.RWMutex //定义m的类型为读写锁。 16 17 func main() { 18 m = new(sync.RWMutex) 19 go read("第一次读取:") //开启5个协程进行读取操作。如果不能并发跑最少需要5秒钟时间。 20 go read("第二次读取:") 21 go read("第三次读取:") 22 go read("第四次读取:") 23 go read("第五次读取:") 24 time.Sleep(2* time.Second) //主进程只有2s,意味着这个程序最多能运行2s的时间。 25 } 26 27 func read(i string) { 28 println(i, "read start") 29 m.RLock() //读锁定 30 println(i, "资料正在读取中....") 31 time.Sleep(1 * time.Second) //改函数执行完毕最少需要睡1秒。 32 m.RUnlock() //读解锁 33 println(i, "read end") 34 } 35 36 37 38 #以上代码执行结果如下: 39 第二次读取: read start 40 第二次读取: 资料正在读取中.... 41 第一次读取: read start 42 第一次读取: 资料正在读取中.... 43 第三次读取: read start 44 第三次读取: 资料正在读取中.... 45 第四次读取: read start 46 第四次读取: 资料正在读取中.... 47 第五次读取: read start 48 第五次读取: 资料正在读取中.... 49 第一次读取: read end 50 第三次读取: read end 51 第二次读取: read end 52 第四次读取: read end 53 第五次读取: read end
B.读写锁控制下的多个写操作之间都是互斥的,并且写操作与读操作之间也都是互斥的; 我们发现,读的时候不能写,写的时候不能读,我们将主程序设置的时间是10秒钟,一次读两次写花费的总时间是12秒钟,因此肯定有一个是无法完成度或是写的部分。 1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:[email protected] 6 */ 7 8 package main 9 10 import ( 11 "sync" 12 "time" 13 ) 14 15 var m *sync.RWMutex 16 17 func read(i string) { 18 println(i, "read start") 19 m.RLock() 20 println(i, "资料正在读取中.....") 21 time.Sleep(4 * time.Second) 22 m.RUnlock() 23 println(i, "read end") 24 } 25 26 func write(i string) { 27 println(i, "write start") 28 m.Lock() //写操作锁定 29 println(i, "数据正在写入硬盘中.....") 30 time.Sleep(4 * time.Second) 31 m.Unlock() //写操作解锁 32 println(i, "write end") 33 } 34 35 func main(){ 36 m = new(sync.RWMutex) 37 go write("第一次写入") //写的时候啥都不能干,即其他协程无法写入,也无法读取。 38 go read("第一次读取") 39 go write("第二次写入") 40 time.Sleep(10 * time.Second) //主进程只给出10秒的时间,但是整个进程跑完最少需要12秒的时间。 41 } 42 43 44 45 46 #以上代码直接结果如下: 47 第一次写入 write start 48 第一次写入 数据正在写入硬盘中..... 49 第一次读取 read start 50 第二次写入 write start 51 第一次写入 write end 52 第一次读取 资料正在读取中..... 53 第一次读取 read end 54 第二次写入 数据正在写入硬盘中.....
4.channel锁; channel锁的工作原理就是让2个协成互相通信,一会我们会详细介绍。 1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:[email protected] 6 */ 7 8 package main 9 10 import ( 11 "time" 12 "fmt" 13 "sync" 14 ) 15 16 17 type Accout struct { 18 flag sync.Mutex 19 money int 20 } 21 22 func (a *Accout) Do_Prepare() { 23 time.Sleep(time.Second) 24 } 25 26 func (a *Accout) Get_Gongzi(n int) { 27 a.money += n 28 } 29 30 func (a *Accout) Give_Wife(n int) { 31 a.flag.Lock() 32 defer a.flag.Unlock() 33 if a.money > n { 34 a.Do_Prepare() 35 a.money -= n 36 }else { 37 fmt.Println("您的余额已不足,请及时充值!") 38 } 39 } 40 41 func (a *Accout) Buy(n int) { 42 a.flag.Lock() 43 defer a.flag.Unlock() 44 if a.money >n { 45 a.Do_Prepare() 46 a.money -= n 47 }else { 48 fmt.Println("您的余额已不足,请及时充值!") 49 } 50 } 51 52 func (a *Accout) Left()int { 53 return a.money 54 } 55 56 func main() { 57 var account Accout 58 account.Get_Gongzi(10000) 59 60 var work_info chan string 61 work_info = make(chan string ,2) //定义一个channel 62 63 go func() { 64 account.Give_Wife(6000) 65 work_info <- "I done" //如果该进程执行完毕就发送一条数据给channel,下面的那个也一样 66 }() 67 68 go func() { 69 account.Buy(50000) 70 work_info <- "I have done too!" 71 }() 72 73 cnt := 0 74 for i := range work_info { 75 fmt.Println(i) 76 cnt++ //每次循环都叠加1,当2个协程都工作完就让主程序结束。 77 if cnt >= 2 { 78 break 79 } 80 81 } 82 defer close(work_info) 83 fmt.Printf("您的剩余工资是\033[31;1m%d\033[0m",account.Left()) 84 } 85 86 87 88 89 #以上代码直接结果如下: 90 您的余额已不足,请及时充值! 91 I have done too! 92 I done 93 您的剩余工资是4000
其实channel还有一个好处就是解决超时问题,就是如果在规定的时间没有完成进程的内容,我们就返回给用户超时的状态。以下是示例代码: 1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:[email protected] 6 */ 7 8 package main 9 10 import ( 11 "time" 12 "fmt" 13 "sync" 14 ) 15 16 17 type Accout struct { 18 flag sync.Mutex 19 money int 20 } 21 22 func (a *Accout) Do_Prepare() { 23 time.Sleep(time.Second) 24 } 25 26 func (a *Accout) Get_Gongzi(n int) { 27 a.money += n 28 } 29 30 func (a *Accout) Give_Wife(n int) { 31 a.flag.Lock() 32 defer a.flag.Unlock() 33 if a.money > n { 34 a.Do_Prepare() 35 a.money -= n 36 }else { 37 fmt.Println("您的余额已不足,请及时充值!") 38 } 39 } 40 41 func (a *Accout) Buy(n int) { 42 a.flag.Lock() 全部评论
专题导读
热门推荐
热门话题
阅读排行榜
|
请发表评论