一、Goroutine
1.Goroutine概念
-
Go中使用Goroutine来实现并发
-
Goroutine是与其他函数或方法同时运行的函数或方法。
-
Goroutine在线程上的优势
-
与线程相比,Goroutine非常便宜。它们只是堆栈大小的几个kb,堆栈可以根据应用程序的需要增长和收缩,而在线程的情况下,堆栈大小必须指定并且是固定的。由于创建Goroutine的成本很小。因此,Go应⽤用程序可以并发运行数千个Goroutine。
-
当使用Goroutine访问共享内存时,通过设计的通道可以防止竞态条件发生。通道可以被认为Goroutine通信的管道。
Coroutine与Goroutine
-
Coroutine协程
-
协程,英文Coroutine,有称为微线程。是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。协程最初在1963年被提出。
-
协程不是进程或线程,其执行过程更类似于子程序,或者说不带返回值的函数调用。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源Coroutine是编译器级的,Process和Thread是操作系统级的。
-
协程的优势
-
与传统的系统级线程和进程相比,协程的最大优势在于其轻量级,可以轻松创建上百万个,而不不会导致系统资源衰竭;线程和进程通常最多也不能超过一万个。这也是协程叫做轻量级线程的原因。
-
协程的执行效率极⾼高。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销。和多线程比,线程数量越多,协程的性能优势就越明显。
-
-
-
Coroutine与Goroutine比较:
(1)、goroutine可能发生并行执行;但是coroutine始终顺序执行;
-
goroutine可能发生在多线程环境下,coroutine始终发生在单线程,coroutine程序需要主动交出控制权,宿主才能获得控制权并将控制权交给其他coroutine。
-
coroutine的运行机制属于协作式任务处理,应用程序在不需要使用CPU时,需要主动交出CPU使用权。如果开发者无意间让应用程序长时间占用CPU,操作系统也无能为力,计算机很容易失去响应或者死机。
-
goroutine属于抢占式任务处理,已经和现有的多线程和多进程任务处理非常类似。应用程序对CPU的控制最终需要有操作系统来管理,操作系统如何发现一个应用程序常时间占用CPU,那么用户有权终止这个任务。
(2)、goroutine间使用channel通信;coroutine使用yield和resume操作。
-
-
2.Goroutine创建和使用
在函数或方法调用前面加上关键字go,将会同时运行一个新的Goroutine。
-
使用go关键字创建Goroutine时,被调用的函数往往没有返回值,如果有返回值也会被忽略。
-
如果需要在Goroutine中返回数据,必须使用通道channel,通过通道把数据从Goroutine中作为返回值传出。
示例:
//创建goroutine //此程序循环打印0-99之间的数字,打印出来是乱序的,为什么?因为这是多个goroutine,哪个先跑起来是不一定的 func hello(i int){ fmt.Println("hello",i) } //程序启动之后会创建一个主goroutine去执行 func main(){ for i:=0;i<100;i++{ go hello(i) //go func(i int){ //用的是函数参数的那个i,不是外边的i //fmt.Println(i) //}(i) } fmt.Println("main") time.Sleep(time.Second) //main函数结束了,有main函数启动的goroutine也都结束了,所以需要加一个延迟 }
sync包中的waitGroup:如此创建goroutine的话,程序不知道goroutine什么时候执行完毕,延迟时间就会一直等待,导致输出出现卡顿并且浪费系统资源,可以通过WaitGroup来实现多个goroutine的同步,等待每个任务执行完成之后再退出:
//WaitGroup var wg sync.WaitGroup//实现多个goroutine的同步,等待每个任务执行完成之后再退出 func f(){ rand.Seed(time.Now().UnixNano())//加随机数种子,每次执行时重新生成随机数 for i:=0;i<5;i++{ r1 := rand.Int() r2 := rand.Intn(10)//0<= x <10 fmt.Println(r1,r2) } } func f1(i int){ defer wg.Done()//每执行完一个goroutine,减1 time.Sleep(time.Millisecond * time.Duration(rand.Intn(300))) fmt.Println(i) //wg.Done() } func main(){ //f() //wg.Add(10) for i:=0;i<10;i++{ wg.Add(1)//每启动一个goroutine,加1 go f1(i) } //如何知道这十个goroutine都结束了? //此时时间是随机的,不能简单使用time.Sleep() wg.Wait()//等待wg的计数器减为0 }*/
程序默认会跑满整个cpu,但是有时候不需要这么多资源,那么可以通过GOMAXPROCS来设置想使用的cpu数量。
//GOMAXPROCESS var wg sync.WaitGroup func a(){ defer wg.Done() for i:=0;i<10;i++{ fmt.Printf("A:%d\n",i) } } func b(){ defer wg.Done() for i:=0;i<10;i++{ fmt.Printf("B:%d\n",i) } } func main(){ //<1:不修改任何数值。 //=1:单核心执行。 //>1:多核并发执行。 runtime.GOMAXPROCS(1)//指定想使用的cpu逻辑核心数,默认跑满整个cpu 8 fmt.Println(runtime.NumCPU())//查询cpu核心数,此电脑为8核 wg.Add(2) go a() go b() wg.Wait() }
go关键字后也可以是匿名函数或闭包。
func main() { go func() { var times int for { times++ fmt.Println("tick" , times) time.Sleep(time.Second) } }() var input string fmt.Scanln(&input) }
二、channel
1.通道的概念
(一)、通道的概述
-
使用通道的意义
-
单纯地将函数并发执行没有意义。函数与函数间需要交换数据才能体现出并发执行的意义。
-
虽然可以使用共享内存进行数据交换,但是共享内存在不同goroutine中容易发生竞态问题,必须使用互斥对内存进行加锁,所以造成性能问题。
-
Go语言中提倡使用通道channel的⽅式代替共享内存。也就是说,Go语言中主张,应该通过数据传递来实现共享内存,⽽不是通过共享内存来实现消息传递。
-
排队的目的是避免拥堵、插队造成的资源使用和交换过程低效问题。多个goroutine为了争抢数据,势必造成低效,使用队列的方式是最高效的,channel就是一种队列结构。
-