Go语言goroutine、channel【重点】
Go语言goroutine、channel
进程、线程:
①进程就是程序程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位。
②线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位。
③一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行。
④一个程序至少有一个进程,一个进程至少有一个线程。
并发:多线程程序在单核上运行
因为是在一个cpu上,比如有10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度看,好像这10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行。
并行:多线程程序在多核上运行
因为是在多个cpu上(比如有10个cpu),比如有10个线程,每个线程执行10毫秒(各自在不同cpu上执行),从人的角度看,这10个线程都在运行,但是从微观上看,在某一个时间点看,也同时有10个线程在执行。

Go协程goroutine、Go主线程:
Go主线程(有程序员直接称为线程 / 也可以理解成进程):一个Go线程上,可以起多个协程goroutine,可以这样理解,协程是轻量级的线程【编译器做优化】。
Go协程goroutine的特点:
①有独立的栈空间
②共享程序堆空间
③调度由用户控制
④协程是轻量级的线程
Go协程goroutine的案例:
1.在主线程(可以理解成进程)中,开启一个goroutine,该协程每隔1秒输出"hello,world"。
2.在主线程中也每隔一秒输出"hello.golang",输出10次后,退出程序。
3.要求主线程和goroutine同时执行。


①如果主线程退出了,协程没执行完毕也会退出。
②主线程是一个物理线程,直接作用在cpu上的。是重量级的,非常耗费cpu资源。
③协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
④Golang的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显Golang 在并发上的优势了。
Go协程goroutine的调度模型:
MPG:
M:操作系统的主线程(是物理线程)
P:协程执行需要的上下文
G:协程
MPG模式运行的状态1:

当前程序有三个M,如果都在一个cpu运行,就是并发,如果在不同的cpu运行,就是并行。
MPG模式运行的状态2:

分成两个部分来看;
M0主线程正在执行G0协程,另外有三个协程在队列等待。
如果G0协程阻塞,这时就会创建M1主线程(也可能是从已有的线程池中取出M1),并且将等待的3个协程挂到M1下开始执行,M0的主线程下的G0仍然执行。
这样的MPG调度模式,可以既让G0执行,同时也不会让队列的其它协程一直阻塞,仍然可以并发/并行执行。
等到G0不阻塞了,M0会被放到空闲的主线程继续执行(从已有的线程池中取),同时G0又会被唤醒。
设置Golang运行的cpu数:
为了充分利用多cpu的优势,在Golang程序中,设置运行的cpu数目。


go1.8后,默认让程序运行在多个核上,可以不用设置了。
go1.8前,还是要设置一下,可以更高效的利益cpu。
channel(管道)-看个需求
需求:
现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到map中。
最后显示出来。要求使用goroutine完成
思路:
1. 编写一个函数,来计算各个数的阶乘,并放入到 map中。
2. 我们启动的协程多个,统计的将结果放入到 map中。
3. map 应该做出一个全局的。
4.因为没有对全局变量m加锁,因此会出现资源争夺问题,代码会出现错误,提示 concurrent map writes。解决方案:加入互斥锁。
不同goroutine之间如何通讯
①全局变量的互斥锁
②使用管道channel来解决



为什么需要channel
①前面使用全局变量加锁同步来解决goroutine的通讯,但不完美
②主线程在等待所有 goroutine全部完成的时间很难确定,这里设置10秒,仅仅是估算。
③如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有 goroutine 处于工作状态,这时也会随主线程的退出而销毁
④通过全局变量加锁同步来实现通讯,也并不利于多个协程对全局变量的读写操作。
⑤上面种种分析都在呼唤一个新的通讯机制-channe1
channel的基本介绍
Go语言中的通道(channel)是一种特殊的类型。
在任何时候,同时只能有一个goroutine访问通道进行发送和获取数据。
①channel本身是一个队列,先进先出
②线程安全,多goroutine访问时,不需要加锁
③本身是有类型的,string,int等,如果要存多种类型,则定义成interface类型
④channel是引用类型,必须make后才能使用,一旦make,容量就确定了,不会增加!!

特点:
①一旦初始化容量,就不会改变了。
②当写满时,不可以写,取空时,不可以取,否则报dead lock。
③发送将持续阻塞直到数据被接收。
Go程序运行时能智能地发现一些永远无法发送成功的语句并做出提示。
④接收将持续阻塞直到发送方发送数据。
如果接收方接收时,通道中没有发送方发送数据,接收方也会发生阻塞,直到发送方发送数据为止。
⑤通道一次只能接收一个数据元素。
定义/声明channel:var变量名chan数据类型
举例:
Var intChan chan int (intChan用于存放int数据)
Var mapChan chan map[int]string (mapChan用于存放map[int]string类型)
Var perChan chan Person
Var perChan2 chan *Person
说明:
channel是引用类型;
channel必须初始化才能写入数据,即make后才能使用管道是有类型的。
管道的初始化,写入数据到管道,从管道读取数据

fmt.Printf("intChan的值=%v intChan本身的地址=%p\n", intChan, &intChan):
channel和指针一样,存放在一个内存单元中,有它的地址,它的值是一个int类型的地址。
读写channel案例演示:
①创建一个intChan,最多可以存放3个int,存数据到intChan,然后再取出这三个int。

②创建一个mapChan,最多可以存放10个map[string]string的key-val,演示写入和读取。

③创建一个catChan,最多可以存放10个cat结构体变量,演示写入和读取。

④创建一个catChan2,最多可以存放10个*Cat变量,演示写入和读取

⑤创建一个allChan,最多可以存放10个任意数据类型变量,演示写入和读取

⑥注意空接口类型的 channel:

定义interface类型的空接口,可以接收任意类型的数据,但是在取出来的时候,必须断言!a := newCat.(Cat)
channel的关闭:close( )
关闭之后,不能再写入,只能读。只能由发送者执行这句代码。
channel 的遍历:for--range遍历,不用for循环
①在遍历时,如果channel没有关闭,则会出现deadlock的错误
②在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
应用
实例1:请完成goroutine和channel协同工作的案例,具体要求:
①开启一个writeData协程,向管道intChan中写入50个整数;
②开启一个readData协程,从管道intChan中读取writeData写入的数据;
③注意: writeData和readDate操作的是同一个管道;
④主线程需要等待writeData和readDate协程都完成工作才能退出管道。
方法:
①开两个管道;
②当writeData协程完成后,close数据管道,readData协程对数据管道intChan的数据读完之后,就向退出管道exitChan写入一个 true,close掉;
③主线程循环检测退出管道里是否有数据,如果有,说明readData协程完成,主程序就可以退出了。

实例2:要求统计1-200000的数字中,哪些是素数?
分析思路:使用并发/并行的方式,将统计素数的任务分配给多个(4个)goroutine去完成。
定义三个管道:
intChan :放80000个数
primeChan:放素数
exitChan :4个协程运行完毕的标志



channel使用细节和注意事项
①channel可以声明为只读 / 只写性质

②使用 select可以解决从管道取数据的阻塞问题(不知道何时关闭管道时)

③goroutine中使用recover,解决“协程中出现panic,导致程序崩溃”的问题。


通道的数据接收, 4种写法。
①阻塞接收数据
将接收变量作为<-操作符的左值,格式:data := <-ch
执行该语句时将会阻塞,直到接收到数据并赋值给data变量。
②非阻塞接收数据
语句不会发生阻塞,格式:data, ok := <-ch
data:表示接收到的数据。未接收到数据时,data为通道类型的零值。
ok:表示是否接收到数据。
非阻塞的通道接收方法可能造成高的CPU占用,因此使用非常少。如果需要实现接收超时检测,可以配合 select 和计时器 channel进行。
③接收任意数据,忽略接收的数据
阻塞接收数据后,忽略从通道返回的数据,格式:<-ch
执行该语句时将会发生阻塞,直到接收到数据,但接收到的数据会被忽略。
这个方式实际上只是通过通道在 goroutine 间阻塞收发实现并发同步。
使用通道做并发同步的写法,可以参考下面的例子:

④循环接收
通道的数据接收可以借用for range语句进行多个元素的接收操作,格式:
for data := range ch {
}
通道ch是可以进行遍历的,遍历的结果就是接收到的数据。数据类型就是通道的数据类型。通过for遍历获得的变量只有一个,即上面例子中的data。
