【无标题】

Go语言中的并发编程

并发

goroutine实现,类似线程,++属于用户态的线程++,可以并发工作
goroutine是由Go语言的运行时(runtime)调度完成

线程是由操作系统调度完成。

Go语言还提供channel在多个goroutine间进行通信

goroutine和channel是 Go 语言秉承的 CSP(Communicating Sequential Process)并发模式的重要实现基础。

goroutine

程序员只需要定义很多个任务,让系统去帮助我们把这些任务分配到CPU上实现并发执行呢

Go程序会智能地将goroutine中的任务合理地分配给每个CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制

当你需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数,开启一个goroutine去执行这个函数就可以了,就是这么简单粗暴。

使用goroutine

Go语言中使用goroutine非常简单,只需要在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine

P与M一般也是一一对应的。他们关系是:

P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M。

一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。

goroutine是并发执行的,而goroutine的调度是随机的


调度与栈

可增长的栈

OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine的栈不是固定的,他可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,虽然极少会用到这么大。所以在Go语言中一次创建十万左右的goroutine也是可以的

goroutine调度

GPM是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统

单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)
其一大特点是goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。

Go语言中的操作系统线程和goroutine的关系:

  • 一个操作系统线程对应用户态多个goroutine。
  • go程序可以同时使用多个操作系统线程。
  • goroutine和OS线程是多对多的关系,即m:n。

channel

实现函数与函数之间的交换数据

Go语言的并发模型是CSP(CommunicatingSequentialProcesses),提倡通过通信共享内存而不是通过共享内存而实现通信

如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。

channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

channel类型

channel是一种类型,一种引用类型。

==### ==无缓冲通道阻塞问题:

无缓冲通道上的发送会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能够发送成功。相反,如果有先有接受操作,那么接受端的goroutine也会阻塞,直到有发送端向通道传送数据才会唤醒接受端。

就如同没有设置代收点的快递,快递员送快递,而你不在,快递员就得等,
相反,他还没送到,你就得等。

使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道

两种方式判断接收值的时候通道是否关闭:

  • for range遍历通道,当通道被关闭的时候就会退出for range
  • for i:OK := <- ch // i, ok := <-ch1 // 通道关闭后再取值ok=false
i, ok := <-ch1 // 通道关闭后再取值ok=false
			if !ok {
				break
			}

单向通道

有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。

  • chan<- int是一个只写单向通道(只能对其写入int类型值),可以对其执行发送操作但是不能执行接收操作;
  • <-chan int是一个只读单向通道(只能从其读取int类型值),可以对其执行接收操作但是不能执行发送操作。
func counter(out chan<- int) { //参数为  写单向通道(只能对其写入int类型值),可以对其执行发送操作但是不能执行接收操作
	for i := 0; i < 100; i++ {
		out <- i
	}
	close(out)
}
func squarer(out chan<- int, in <-chan int) { //参数为  写单向通道(只能对其写入int类型值),可以对其执行发送操作但是不能执行接收操作
	for i := range in {
		out <- i * i
	}
	close(out)
}
func printer(in <-chan int) {
	for i := range in {
		fmt.Println(i)
	}

}
func singalFuncCh() {
	ch1 := make(chan int)
	ch2 := make(chan int)
	go counter(ch1)      //ch1 只作为输出通道
	go squarer(ch2, ch1) // ch2 只作为输出通道 ch1 只作为输入通道
	printer(ch2)         // ch2 只作为输入通道
}

在函数传参及任何赋值操作中可以将双向通道转换为单向通道,但反过来是不可以的

通道总结

【无标题】


使用select语句能提高代码的可读性。

  • 可处理一个或多个channel的发送/接收操作。
  • 如果多个case同时满足,select会随机选择一个。
  • 对于没有case的select{}会一直等待,可用于阻塞main函数。
上一篇:Channel的典型应用场景


下一篇:14-Netty 应用实例 - 多人群聊系统