9、协程

栏目: Go · 发布时间: 7年前

内容简介:并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。Coroutine(协程)是一种用户态的轻量级线程,特点如下:

1、并发与并行

并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。

并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

2、Coroutine

Coroutine(协程)是一种用户态的轻量级线程,特点如下:

A、轻量级线程

B、非抢占式多任务处理,由协程主动交出控制权。

C、编译器/解释器/虚拟机层面的任务

D、多个协程可能在一个或多个线程上运行。

E、子程序是协程的一个特例。

不同语言对协程的支持:

A、C++通过Boost.Coroutine实现对协程的支持

B、 Java 不支持

C、 Python 通过yield关键字实现协程,Python3.5开始使用async def对原生协程的支持

3、goroutine

Go语言并发的基础是goroutine和channel,当然 Go 也提供了传统的对共享资源加锁的方式实现并发:原子函数(atomic函数-类似Java 中的AtomicInteger)和互斥锁(mutex-类似java中的Lock)。

goroutine奉行通过通信共享内存,而不是共享内存来通信

这里主要讲下goroutine和channel

goroutinue使用示例

示例1:使用关键字go来定义并启动一个goroutine:

func loop() {
    for i := 0; i < 10; i++ {
        fmt.Printf("%d ", i)
    }
}
func main() {
    go loop()
    loop()
    time.Sleep(time.Second) // 停顿一秒
}

示例二:信号量方式

package main
import (
   "fmt"
   "sync"
)
func main(){
   var wg sync.WaitGroup
   wg.Add(2)
   go func() {
      defer wg.Done()
      for i := 0; i < 10000; i++ {
         fmt.Printf("Hello,Go.This is %d\n", i)
      }
   }()
   go func() {
      defer wg.Done()
      for i := 0; i < 10000; i++ {
         fmt.Printf("Hello,World.This is %d\n", i)
      }
   }()
   wg.Wait()
}

sync.WaitGroup是一个计数的信号量,类似java中的CountDownLatch。使main函数所在主线程等待两个goroutine执行完成后再结束,否则两个goroutine还在运行时,主线程已经结束。

sync.WaitGroup使用非常简单,使用Add方法设设置计数器为2,每一个goroutine的函数执行完后,调用Done方法减1。Wait方法表示如果计数器大于0,就会阻塞,main函数会一直等待2个goroutine完成再结束。

示例三:使用通道channel在并发过程中实现通信

package main

import (
   "fmt"
)

func main() {
   ch := make(chan int)
   go func() {
      var sum int = 0
      for i := 0; i < 10; i++ {
         sum += i
      }
      //发送数据到通道
      ch <- sum
   }()
   //从通道接收数据
   fmt.Println(<-ch)
}

在计算sum和的goroutine没有执行完,将值赋发送到ch通道前,fmt.Println(<-ch)会一直阻塞等待,main函数所在的主goroutine就不会终止,只有当计算和的goroutine完成后,并且发送到ch通道的操作准备好后,main函数的<-ch会接收计算好的值,然后打印出来

概念

进程:一个程序对应一个独立程序空间

线程:一个执行空间,一个进程可以有多个线程

逻辑处理器:执行创建的goroutine,绑定一个线程

调度器:Go运行时中的,分配goroutine给不同的逻辑处理器

全局运行队列(global runqueue):所有刚创建的goroutine队列

本地运行队列:逻辑处理器的goroutine队列

9、协程

goroutine调度原理图.png

可以在程序开头使用runtime.GOMAXPROCS(n)设置逻辑处理器的数量。

如果需要设置逻辑处理器的数量,一般采用如下代码设置:

runtime.GOMAXPROCS(runtime.NumCPU())

调度器对可以创建的逻辑处理器的数量没有限制,但语言运行时默认限制每个程序最多创建10000个线程。

goroutine vs thread

1、内存占用

goroutine并不需要太多太多的内存占用,初始只需2kB的栈空间即可(自Go 1.4起),按照需要可以增长。一般来说一个Goroutine成本在 4 — 4.5 KB,在go程序中,一次创建十万左右的goroutine很容易(4KB*100,000=400MB)。

线程初始1MB,并且会分配一个防护页(guard page)。在64位 Linux 系统,max user process限制线程数量:(可通过ulimit –a查看,默认值1024,通过ulimit –u可以修改此值)。

在使用Java开发服务器的过程中经常会遇到request per thread的问题,如果为每个请求都分配一个线程的话,大并发的情况下服务器很快就死掉,因为内存不够了,所以很多Java框架比如Netty都会使用线程池来处理请求,而不会让线程任意增长。

而使用goroutine则没有这个问题,你页可以看到官方的net/http库就是使用request per goroutine这种模式进行处理的,内存占用不会是问题。

2、上下文切换

从调度上看,goroutine的调度开销远远小于线程调度开销。

OS的线程由OS内核调度,每隔几毫秒,一个硬件时钟中断发到CPU,CPU调用一个调度器内核函数。这个函数暂停当前正在运行的线程,把他的寄存器信息保存到内存中,查看线程列表并决定接下来运行哪一个线程,再从内存中恢复线程的注册表信息,最后继续执行选中的线程。这种线程切换需要一个完整的上下文切换:即保存一个线程的状态到内存,再恢复另外一个线程的状态,最后更新调度器的数据结构。某种意义上,这种操作还是很慢的。

Go运行的时候包涵一个自己的调度器,这个调度器使用一个称为一个M:N调度技术,m个goroutine到n个os线程(可以用GOMAXPROCS来控制n的数量),Go的调度器不是由硬件时钟来定期触发的,而是由特定的go语言结构来触发的,他不需要切换到内核语境,所以调度一个goroutine比调度一个线程的成本低很多。

当线程阻塞时,其它的线程进可能被执行,这叫做线程的切换。切换的时候,调度器需要保存当前阻塞的线程的状态,恢复要执行的线程状态,包括所有的寄存器,16个通用寄存器、程序计数器、栈指针、段寄存器、16个XMM寄存器、FP协处理器、16个 AVX寄存器、所有的MSR等等。

goroutine的保存和恢复只需要三个寄存器:程序计数器、栈指针和DX寄存器。因为goroutine之间共享堆空间,不共享栈空间,所以只需把goroutine的栈指针和程序执行到那里的信息保存和恢复即可,花费很低。

其实, goroutine 用到的就是线程池的技术,当 goroutine 需要执行时,会从 thread pool 中选出一个可用的 M 或者新建一个 M。而 thread pool 中如何选取线程,扩建线程,回收线程,Go Scheduler 进行了封装,对程序透明,只管调用就行,从而简化了 thread pool 的使用。

Go调度器

Go的调度器内部有三个重要的结构:M,P, G

M:

代表真正的内核OS线程,和POSIX里的thread差不多

P:

代表调度的上下文(逻辑处理器),可以把它看做一个局部的调度器,使go代码在一个线程上跑,它是实现从N:1到N:M映射的关键。

M必须拿到P才能对G进行调度,P限定了go调度goroutine的最大并发度。每一个运行的M都必须绑定一个P。

G:

代表一个goroutine,它有自己的栈,instruction pointer和其他信息(正在等待的channel等等),用于调度。

9、协程

Go调度器.png

调度方式:Goroutine 在 system call 和 channel call 时都可能发生阻塞,但这两种阻塞发生后,处理方式又不一样的。

系统调用时

当程序发生阻塞的 system call(如打开一个文件)时,P可以转而投奔另一个OS线程。

9、协程

系统调用时处理方式.png

图中看到,当一个OS线程M0陷入阻塞时,P转而在OS线程M1上运行。调度器保证有足够的线程来运行所以的context P。

图中的M1可能是被创建,或者从线程缓存中取出。当MO返回时,它必须尝试取得一个context P来运行goroutine,一般情况下,它会从其他的OS线程那里steal偷一个context过来,如果没有偷到的话,它就把goroutine放在一个global runqueue里,然后自己就去睡大觉了(放入线程缓存里)。Contexts们也会周期性的检查global runqueue,否则global runqueue上的goroutine永远无法执行。

网络IO调用时

当goroutine需要做一个网络IO调用时,G会和P分离,并移到集成了网络轮询器的运行时,一旦该轮询器指示某个网络读或者写操作已经就绪,对应的goroutine就会重新分配到P上完成操作。

channel call时

当程序发起一个 channel call时,G会和P分离,G 的状态会设置为 waiting,M 继续执行其他的 G。当 G 的调用完成,会有一个可用的 M 继续执行它。

任务窃取

另一种情况是P所分配的任务G很快就执行完了(分配不均),这就导致了一个上下文P闲着没事儿干而系统却任然忙碌。

9、协程

任务窃取.png

但是如果global runqueue没有任务G了,那么P就不得不从其他的上下文P那里拿一些G来执行。一般来说,如果上下文P从其他的上下文P那里要偷一个任务的话,一般就‘偷’run queue的一半,这就确保了每个OS线程都能充分的使用。

参考:


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Algorithms

Algorithms

Sanjoy Dasgupta、Christos H. Papadimitriou、Umesh Vazirani / McGraw-Hill Education / 2006-10-16 / GBP 30.99

This text, extensively class-tested over a decade at UC Berkeley and UC San Diego, explains the fundamentals of algorithms in a story line that makes the material enjoyable and easy to digest. Emphasi......一起来看看 《Algorithms》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具