初步了解go语言中的并行

一、概念

在了解,go的并行之前,我们首先需要搞清楚一下概念。

概念描述
进程可以理解为一个程序,其对应一个独立程序空间
线程一个执行空间,一个进程可以有多个线程
用户态的线程可以理解为就是把线程切换需要的上下文保存在线程本身。在go里面,可以将goroutine理解为用户态的线程,即协程。
逻辑处理器每个逻辑处理器都会绑定一个线程,并负责goroutine的执行
全局运行队列所有创建的goroutine都会放到这里
本地运行队列分发到每个逻辑处理器的goroutine的队列
什么是并发并发是以时间段为维度,即在单位时间内,同时完成多件事。
什么是并行并行是以时间点为维度,即在某个时间点,同时完成多件事。
同步与异步同步和异步针对应用程序而言,关注的是程序中间的协作关系,一般用于应用程序与内核的交互,同步即等待或是轮询;异步则是直接返回,完成后通知应用程序。
阻塞与非阻塞阻塞与非阻塞关注的是单个进程的执行状态,一般用于网络io中,阻塞即需要等待,不会立即返回;非阻塞则会立刻返回。
上下文上下文是一种非常泛化的概念,可以理解为程序执行的环境变量。

借用Erlang 之父 Joe Armstrong的一张图,来展示什么是并发与并行,如下:
这里写图片描述

二、go并行的原理

当我们创建了一个goroutine的后,会先存放在全局运行队列中,等Go运行时调度器便会进行调度,把他们分配给其中的一个逻辑处理器,并放到这个逻辑处理器对应的本地运行队列中,最终等着被逻辑处理器执行。

那么逻辑处理器,又是如何执行goroutine呢?

操作系统会在物理处理器上调度操作系统线程来运行,逻辑处理器绑定到系统线程上面了,于是系统线程执行哪些goroutine,就会受到逻辑处理器的控制,真正执行goroutine还是系统的线程。

参考The Go scheduler

M:操作系统线程,

P:逻辑处理器

G:goroutine,拥有自己的栈,指令指针等信息。
这里写图片描述
每个P(逻辑处理器)会维护着一个goroutine本地队列,处于就绪状态的goroutine(灰色G),就会被P控制着在M中执行。

在Go程序中,每当执行go func,就会创建一个goroutine,并放到goroutine的全局队列中,最后被分配到一个goroutine本地队列。

我们都知道,当M(系统线程)阻塞的时候,比如下载比较大的文件,cpu就会被空闲了,使用异步事件或回调的思维方式能够更加有效地利用CPU,但是这样的代价也是比较大的,因为需要保存上下文,且能够在该恢复的时刻进行恢复上下文。Go采取的做法是,当发现没有空闲的M(比如线程m被阻塞了)且M的数量少于GOMAXPROCS,则会创建新的M(线程),进而可以邦定P(逻辑处理器),从而执行G(goroutine)

三、go中goroutine

每当执行go func,就会创建一个goroutine,在Go语言中,goroutine就是协程。每个goroutine的结构体中有一个sched域就是用于保存自己上下文的。这样,goroutine就可以被换出去,再换进来。这种上下文保存在用户态完成,不必陷入到内核,非常的轻量,切换速度很快。有的协程运行到一定时候就主动调用yield放弃自己的执行,把自己再次放回到任务队列中等待下一次调用时机等。
1、并发案例

func trace(start, end int8) {
   for i:=start; i<=end;  i++{
      fmt.Printf("%c ", i)
      time.Sleep(time.Second)
   }
}

func main() {
   runtime.GOMAXPROCS(1) //限制只有一个逻辑处理器
   var wg sync.WaitGroup   //用于等待所有协程都完成
   wg.Add(2)

   go func(){
      defer wg.Done()//程序退出的时候执行
      trace('a', 'f')
   }()

   go func(){
      defer wg.Done()//程序退出的时候执行
      trace('A', 'F')
   }()

   wg.Wait() //等待所有协程的完成
}

上面的程序使用runtime.GOMAXPROCS(1)来分配一个逻辑处理器供调度器使用,两个goroutine将被该逻辑处理器调度并发执行。输出如下:

A a b B C c d D E e f F G g h H I i j J

在go语言中,“有函数调用,就有机会被调度器调度”,在上面案例中trace方法里面调用了time.sleep()函数的目的,就是让当前运行goroutine有机会被调度器调度,进剥夺该goroutine的执行权,让其他的goroutine执行。所以上面代码打印的结果是大小写字母,交替的输出。如果注销掉”time.sleep()”,输出结果为:

A B C D E F G H I J a b c d e f g h i j

当然,我们可以通过runtime.Gosched()来使当前在逻辑处理器上运行的goruntine让出运行权限,这样另一个goruntine就会得到执行。

四、并发间的相互通信

在Go中,所有I/O都被阻塞,通过goroutines和通道channel处理并发,而不是回调和异步。通道channel主要负责在并发过程中,实现通信。通道channel是类型相关的,一个通道channel只能传递一种数据类型的值,申明如下:

var chanName chan ElementType

比如声明一个传递int类型的通道如下:

var ch chan int

再如申明一个map类型如下:

var ch map[string] chan string

申明完一个通道以后,我们还需要定义该通道,也可以说是初始化该通道,如指定该通道的大小等。可以使用make函数,如下:

ch := make(chan int,bufferSize) //bufferSize为缓冲区的大小,可以不传递该值代表不带缓冲区的channel

通道channel的使用,主要包含数据的写入和读出,如下:

ch     <-  value   //往通道中写入数据
value  <-   ch    //从通道中读出数据

备注:如果channel没有写入数据,从channel中读取数据也会导致程序的阻塞,一直到channel中被写入数据为止。如下:

func main() {
   runtime.GOMAXPROCS(1)
   var ch chan int = make(chan int)
   go func(){
      fmt.Printf("开始阻塞1。。。")
      time.Sleep(time.Second)
      fmt.Printf("结束阻塞1。。。")
      ch <- 4
   }()

   fmt.Printf("ch ==>%d", <- ch)
}

在Go里面,不仅仅支持双向通道,还支持双向通道,定义双向通道如下:

var send chan <- int //定义只能发送的通道
var receive <- chan int //定义只能接收接受的通道

更多内容:http://www.findme.wang/blog/detail/id/427.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值