GO Road -Goroutine of concurrent scheduling principles & Channel Comments

It has always been a central theme of one of the programming language of concurrent (parallel), but also the most attention by developers topic; Go language as a debut since it comes with "high concurrency" aura of rich second generation programming language concurrent (parallel) programming is definitely worth the developers to explore, and the go language concurrent (parallel) programming is achieved via goroutine, goroutine is one of the most important features golang, low cost, low consumption of resources, and high energy efficiency, the official claimed that tens of thousands of concurrent native goroutine not a problem, so it has become a characteristic Gopher often used.

A, goroutine Profile

Golang be extremely praised is its asynchronous mechanism, which is goroutine. goroutine use is very simple, just go use the keyword to start a coroutine, and it is in the run asynchronously, so after you do not need to re-run it after the completion of the implementation of the code.

go func () // run by starting a coroutine go keyword functions

Remove simple grammatical, goroutine is a coroutine, that is, save more resources than a thread, a thread can have multiple coroutines, and goroutine be assigned to multiple CPU running concurrently in the true sense.

go func () // run by starting a coroutine go keyword functions

Two, goroutine internal principle

Before introducing goroutine principle, to introduce some of the key concepts:

Key concepts

Complicated by

On a cpu perform multiple tasks simultaneously, in a very short period of time, toggling cpu task execution (executing the program in a certain very short time a, and then quickly switched to the obtained program to perform b), overlap in time (At the same time the macro is the micro is still the order of execution), so it looks like to perform multiple tasks simultaneously, which is concurrent.

parallel

When the system has multiple CPU, each CPU running the same task all the time, and do not seize the CPU where their resources, at the same time, called parallel.

Simple to understand

You eat half a meal, phone, and you only have to pick up after eating, which means you do not support concurrent not support parallel.
You eat half a meal, phone, and you pick up the phone stopped, then continued after a meal, it shows your support for concurrency.
You eat half a meal, phone, and your phone while eating side, it shows your support parallel.

The key is you have the ability to concurrently handle multiple tasks, not necessarily at the same time.
Parallel key is you have the ability to handle multiple tasks.

In the computer is:

So I think they are the most crucial point is: whether it is "at the same time."

  

process

cpu when switching programs, if the state on a program (that is, we often say context-- context) without saving, switch directly to the next procedure, you will lose the status of a series of procedures, so the introduction of this process the concept for the program is running good division of resources needed.

Therefore, the process is needed to run a program when the basic resource unit (an entity can be said to be running).

Thread

Cpu time switching a plurality of processes, it takes a lot of time, since the handover process needs to switch to kernel mode, the kernel mode needed per scheduling requires the user to read the data state, once the process multiply, it will consume a cpu scheduler piles of resources, so the introduction of the concept of threads, the thread itself hardly possess the resources, they share resources in the process, it will not be so much like the kernel scheduler switching process so resource-intensive.

A thread is a process of implementing entity, it is the basic unit of CPU scheduling and dispatching, which is smaller than the process of the basic unit can operate independently.

NOTE: Thread include three categories, but it is not really goroutine coroutine. (See: "thread that thing")

Sometimes in order to facilitate understanding you can simply put goroutine analogy to coroutines, but my heart there must be a clear understanding - goroutine not the same coroutine.

Coroutine

Coroutine has its own stack and register context. When coroutine scheduled handover, the context save registers and stack to another location, when cut back, context restore a previously saved registers and stack. Thus, coroutine to retain the state when the last call (i.e., a particular combination of all of the local state), during each reentrant, the equivalent of entering a call state, another way: once on entering the leaving at the logic flow position. Operation threads and processes are triggered by the program system interface, the final executor of the system; co-operation executor process is the user's own program, goroutine also coroutine.

 

 

GPM Allocation Modeling

groutine to have strong concurrent implementation is achieved by GPM scheduling model, following on to explain goroutine scheduling model.

 

Three important internal structure Go scheduler: M, P, G
M: M representative of kernel-level threads, a thread is a M, M goroutines is to run on the; M is a large structure, which maintain small objects memory cache (mcache), goroutine currently executing random number generator, and so very much information
G: represents a goroutine, it has its own stack, instruction pointer and other information (waiting channel, etc.), with in scheduling.
P: P is the full name Processor, a processor, its main purpose is to perform goroutine, so it maintains a queue goroutine, which stores all it needs to perform goroutine

NOTE: GPM model Hi, check out this blog post.

 

Scheduling achieve

从上图中看,有2个物理线程M,每一个M都拥有一个处理器P,每一个也都有一个正在运行的goroutine。
P的数量可以通过GOMAXPROCS()来设置,它其实也就代表了真正的并发度,即有多少个goroutine可以同时运行。
图中灰色的那些goroutine并没有运行,而是出于ready的就绪态,正在等待被调度。P维护着这个队列(称之为runqueue),
Go语言里,启动一个goroutine很容易:go function 就行,所以每有一个go语句被执行,runqueue队列就在其末尾加入一个
goroutine,在下一个调度点,就从runqueue中取出(如何决定取哪个goroutine?)一个goroutine执行。

 

当一个OS线程M0陷入阻塞时(如下图),P转而在运行M1,图中的M1可能是正被创建,或者从线程缓存中取出。

 


当MO返回时,它必须尝试取得一个P来运行goroutine,一般情况下,它会从其他的OS线程那里拿一个P过来,
如果没有拿到的话,它就把goroutine放在一个global runqueue里,然后自己睡眠(放入线程缓存里)。所有的P也会周期性的检查global runqueue并运行其中的goroutine,否则global runqueue上的goroutine永远无法执行。
 
另一种情况是P所分配的任务G很快就执行完了(分配不均),这就导致了这个处理器P很忙,但是其他的P还有任务,此时如果global runqueue没有任务G了,那么P不得不从其他的P里拿一些G来执行。一般来说,如果P从其他的P那里要拿任务的话,一般就拿run queue的一半,这就确保了每个OS线程都能充分的使用,如下图:
参考地址:http://morsmachine.dk/go-scheduler
 
 

三、使用goroutine

基本使用

设置goroutine运行的CPU数量,最新版本的go已经默认已经设置了。

num := runtime.NumCPU()    //获取主机的逻辑CPU个数
runtime.GOMAXPROCS(num)    //设置可同时执行的最大CPU数

使用示例

package main

import (
    "fmt"
    "time"
)

func cal(a int , b int )  {
    c := a+b
    fmt.Printf("%d + %d = %d\n",a,b,c)
}

func main() {
  
    for i :=0 ; i<10 ;i++{
        go cal(i,i+1)  //启动10个goroutine 来计算
    }
    time.Sleep(time.Second * 2) // sleep作用是为了等待所有任务完成
} 
//结果
//8 + 9 = 17
//9 + 10 = 19
//4 + 5 = 9
//5 + 6 = 11
//0 + 1 = 1
//1 + 2 = 3
//2 + 3 = 5
//3 + 4 = 7
//7 + 8 = 15
//6 + 7 = 13
View Code

 

goroutine异常捕捉

当启动多个goroutine时,如果其中一个goroutine异常了,并且我们并没有对进行异常处理,那么整个程序都会终止,所以我们在编写程序时候最好每个goroutine所运行的函数都做异常处理,异常处理采用recover

package main

import (
    "fmt"
    "time"
)

func addele(a []int ,i int)  {
    defer func() {    //匿名函数捕获错误
        err := recover()
        if err != nil {
            fmt.Println("add ele fail")
        }
    }()
   a[i]=i
   fmt.Println(a)
}

func main() {
    Arry := make([]int,4)
    for i :=0 ; i<10 ;i++{
        go addele(Arry,i)
    }
    time.Sleep(time.Second * 2)
}
//结果
add ele fail
[0 0 0 0]
[0 1 0 0]
[0 1 2 0]
[0 1 2 3]
add ele fail
add ele fail
add ele fail
add ele fail
add ele fail
View Code

 

同步的goroutine

由于goroutine是异步执行的,那很有可能出现主程序退出时还有goroutine没有执行完,此时goroutine也会跟着退出。此时如果想等到所有goroutine任务执行完毕才退出,go提供了sync包和channel来解决同步问题,当然如果你能预测每个goroutine执行的时间,你还可以通过time.Sleep方式等待所有的groutine执行完成以后在退出程序(如上面的列子)。

示例一:使用sync包同步goroutine
sync大致实现方式
WaitGroup 等待一组goroutinue执行完毕. 主程序调用 Add 添加等待的goroutinue数量. 每个goroutinue在执行结束时调用 Done ,此时等待队列数量减1.,主程序通过Wait阻塞,直到等待队列为0.
 
package main

import (
    "fmt"
    "sync"
)

func cal(a int , b int ,n *sync.WaitGroup)  {
    c := a+b
    fmt.Printf("%d + %d = %d\n",a,b,c)
    defer n.Done() //goroutinue完成后, WaitGroup的计数-1

}

func main() {
    var go_sync sync.WaitGroup //声明一个WaitGroup变量
    for i :=0 ; i<10 ;i++{
        go_sync.Add(1) // WaitGroup的计数加1
        go cal(i,i+1,&go_sync)  
    }
    go_sync.Wait()  //等待所有goroutine执行完毕
}
//结果
9 + 10 = 19
2 + 3 = 5
3 + 4 = 7
4 + 5 = 9
5 + 6 = 11
1 + 2 = 3
6 + 7 = 13
7 + 8 = 15
0 + 1 = 1
8 + 9 = 17
View Code

 

示例二:通过channel实现goroutine之间的同步。

实现方式:通过channel能在多个groutine之间通讯,当一个goroutine完成时候向channel发送退出信号,等所有goroutine退出时候,利用for循环channe去channel中的信号,若取不到数据会阻塞原理,等待所有goroutine执行完毕,使用该方法有个前提是你已经知道了你启动了多少个goroutine。

复制代码
package main

import (
    "fmt"
    "time"
)

func cal(a int , b int ,Exitchan chan bool)  {
    c := a+b
    fmt.Printf("%d + %d = %d\n",a,b,c)
    time.Sleep(time.Second*2)
    Exitchan <- true
}

func main() {

    Exitchan := make(chan bool,10)  //声明并分配管道内存
    for i :=0 ; i<10 ;i++{
        go cal(i,i+1,Exitchan)
    }
    for j :=0; j<10; j++{   
         <- Exitchan  //取信号数据,如果取不到则会阻塞
    }
    close(Exitchan) // 关闭管道
}
复制代码

goroutine之间的通讯

goroutine本质上是协程,可以理解为不受内核调度,而受go调度器管理的线程。goroutine之间可以通过channel进行通信或者说是数据共享,当然你也可以使用全局变量来进行数据共享。

示例:使用channel模拟消费者和生产者模式

复制代码
package main

import (
    "fmt"
    "sync"
)

func Productor(mychan chan int,data int,wait *sync.WaitGroup)  {
    mychan <- data
    fmt.Println("product data:",data)
    wait.Done()
}
func Consumer(mychan chan int,wait *sync.WaitGroup)  {
     a := <- mychan
    fmt.Println("consumer data:",a)
     wait.Done()
}
func main() {

    datachan := make(chan int, 100)   //通讯数据管道
    var wg sync.WaitGroup

    for i := 0; i < 10; i++ {
        go Productor(datachan, i,&wg) //生产数据
        wg.Add(1)
    }
    for j := 0; j < 10; j++ {
        go Consumer(datachan,&wg)  //消费数据
        wg.Add(1)
    }
    wg.Wait()
}
//结果
consumer data: 4
product data: 5
product data: 6
product data: 7
product data: 8
product data: 9
consumer data: 1
consumer data: 5
consumer data: 6
consumer data: 7
consumer data: 8
consumer data: 9
product data: 2
consumer data: 2
product data: 3
consumer data: 3
product data: 4
consumer data: 0
product data: 0
product data: 1
复制代码

 

四、channel

不同goroutine之间是如何进行通讯的呢?

  • 方法一:全局变量和锁同步
  • 方法二:Channel

这里我们主要注重讲解下go中特有的channel,其类似于UNIX中的管道(piple)。

channel概念

channel俗称管道,用于数据传递或数据共享,其本质是一个先进先出的队列,使用goroutine+channel进行数据通讯简单高效,同时也线程安全多个goroutine可同时修改一个channel,不需要加锁

channel操作

定义和声明:

1 var 变量名 chan 类型    //channel是有类型的,一个整数的channel只能存放整数
2 
3 var test chan int 
4 
5 var test chan map[string]string
6 
7 var test chan *stu

channel可分为三种:

只读channel:只能读channel里面数据,不可写入

只写channel:只能写数据,不可读

一般channel:可读可写

复制代码
var readOnlyChan <-chan int            // 只读chan
var writeOnlyChan chan<- int           // 只写chan
var mychan  chan int                   //读写channel
mychannel = make(chan int,10)

//或者
read_only := make (<-chan int,10)//定义只读的channel
write_only := make (chan<- int,10)//定义只写的channel
read_write := make (chan int,10)//可同时读写
复制代码
定义完成以后需要make来分配内存空间,不然会deadlock!
//定义一个结构体类型的channel

package main

type student struct{
    name string
}

func main() {
    var stuChan chan student
    stuChan = make(chan student, 10)

    stu := student{name:"syu01"}

    stuChan <- stu  
}
struct类型channel

 

读写数据

ch <- "wd"  //写数据
a := <- ch //读取数据
a, ok := <-ch  //推荐的读取数据方法

注意:

  • 管道如果未关闭,在读取超时会则会引发deadlock异常
  • 管道如果关闭进行写入数据会pannic
  • 当管道中没有数据时候再行读取或读取到默认值,如int类型默认值是0

遍历管道

  • 使用for range遍历管道,如果管道未关闭会引发deadlock错误。
  • 如果采用for死循环已经关闭的管道,当管道没有数据时候,读取的数据会是管道的默认值,并且循环不会退出。
复制代码
package main

import (
    "fmt"
    "time"
)


func main() {
    mychannel := make(chan int,10)
    for i := 0;i < 10;i++{
        mychannel <- i
    }
    close(mychannel)  //关闭管道
    fmt.Println("data lenght: ",len(mychannel))
    for  v := range mychannel {  //遍历管道
        fmt.Println(v)
    }
    fmt.Printf("data lenght:  %d",len(mychannel))
}
复制代码

带缓冲区channe和不带缓冲区channel

带缓冲区channel:定义声明时候制定了缓冲区大小(长度),可以保存多个数据。

不带缓冲区channel:只能存一个数据,并且只有当该数据被取出时候才能存下一个数据。

ch := make(chan int) //不带缓冲区
ch := make(chan int ,10) //带缓冲区

不带缓冲区示例:

复制代码
package main

import "fmt"

func test(c chan int) {
    for i := 0; i < 10; i++ {
        fmt.Println("send ", i)
        c <- i
    }
}
func main() {
    ch := make(chan int)
    go test(ch)
    for j := 0; j < 10; j++ {
        fmt.Println("get ", <-ch)
    }
}


//结果:
send  0
send  1
get  0
get  1
send  2
send  3
get  2
get  3
send  4
send  5
get  4
get  5
send  6
send  7
get  6
get  7
send  8
send  9
get  8
get  9
复制代码

channel实现作业池

我们创建三个channel,一个channel用于接受任务,一个channel用于保持结果,还有个channel用于决定程序退出的时候。

复制代码
package main

import (
    "fmt"
)

func Task(taskch, resch chan int, exitch chan bool) {
    defer func() {   //异常处理
        err := recover()
        if err != nil {
            fmt.Println("do task error:", err)
            return
        }
    }()

    for t := range taskch { //  处理任务
        fmt.Println("do task :", t)
        resch <- t //
    }
    exitch <- true //处理完发送退出信号
}

func main() {
    taskch := make(chan int, 20) //任务管道
    resch := make(chan int, 20)  //结果管道
    exitch := make(chan bool, 5) //退出管道
    go func() {
        for i := 0; i < 10; i++ {
            taskch <- i
        }
        close(taskch)
    }()


    for i := 0; i < 5; i++ {  //启动5个goroutine做任务
        go Task(taskch, resch, exitch)
    }

    go func() { //等5个goroutine结束
        for i := 0; i < 5; i++ {
            <-exitch
        }
        close(resch)  //任务处理完成关闭结果管道,不然range报错
        close(exitch)  //关闭退出管道
    }()

    for res := range resch{  //打印结果
        fmt.Println("task res:",res)
    }
}
复制代码

只读channel和只写channel

一般定义只读和只写的管道意义不大,更多时候我们可以在参数传递时候指明管道可读还是可写,即使当前管道是可读写的。

package main

import (
    "fmt"
    "time"
)

//只能向chan里写数据
func send(c chan<- int) {
    for i := 0; i < 10; i++ {
        c <- i
    }
}
//只能取channel中的数据
func get(c <-chan int) {
    for i := range c {
        fmt.Println(i)
    }
}
func main() {
    c := make(chan int)
    go send(c)
    go get(c)
    time.Sleep(time.Second*1)
}
//结果
0
1
2
3
4
5
6
7
8
9
View Code  

select-case实现非阻塞channel

原理通过select+case加入一组管道,当满足(这里说的满足意思是有数据可读或者可写)select中的某个case时候,那么该case返回,若都不满足case,则走default分支。

复制代码
package main

import (
    "fmt"
)

func send(c chan int)  {
    for i :=1 ; i<10 ;i++  {
     c <-i
     fmt.Println("send data : ",i)
    }
}

func main() {
    resch := make(chan int,20)
    strch := make(chan string,10)
    go send(resch)
    strch <- "wd"
    select {
    case a := <-resch:
        fmt.Println("get data : ", a)
    case b := <-strch:
        fmt.Println("get data : ", b)
    default:
        fmt.Println("no channel actvie")

    }

}

//结果:get data :  wd
复制代码

channel中定时器的使用

在对channel进行读写的时,可以对读写进行频率控制,通过time.Ticke实现

示例:

package main

import (
    "time"
    "fmt"
)

func main(){
    requests:= make(chan int ,5)
    for i:=1;i<5;i++{
        requests<-i
    }
    close(requests)
    limiter := time.Tick(time.Second*1)
    for req:=range requests{
        <-limiter
        fmt.Println("requets",req,time.Now()) //执行到这里,需要隔1秒才继续往下执行,time.Tick(timer)上面已定义
    }
}
//结果:
requets 1 2018-07-06 10:17:35.98056403 +0800 CST m=+1.004248763
requets 2 2018-07-06 10:17:36.978123472 +0800 CST m=+2.001798205
requets 3 2018-07-06 10:17:37.980869517 +0800 CST m=+3.004544250
requets 4 2018-07-06 10:17:38.976868836 +0800 CST m=+4.000533569
View Code

 

Guess you like

Origin www.cnblogs.com/X-knight/p/11363730.html