初步了解go语言中的并行

BirdUpton 发布于11天前 阅读102次
0 条评论

一、概念

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

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

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

初步了解go语言中的并行

二、go并行的原理

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

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

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

参考The Go scheduler

M:操作系统线程,

P:逻辑处理器

G:goroutine,拥有自己的栈,指令指针等信息。

初步了解go语言中的并行

每个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

查看原文: 初步了解go语言中的并行

  • silversnake
  • yellowduck
  • silverkoala
  • ticklishelephant
  • ticklishpanda
  • ticklishgorilla
  • orangecat
需要 登录 后回复方可回复, 如果你还没有账号你可以 注册 一个帐号。