goroutine(协程)。

进程、线程?

进程,线程都是os层面的系统调度方式。

协程是用户层面的调用方式,利用更少的资源进行切换,而不需要system call。

但协程是调用的os的线程在执行。

当一个函数为def abc()时,使用go abc() 即为开一个协程去调用这个函数

goroutine在遇到文件i/o的时候,(线程和goroutine会与逻辑处理器)会分隔开,然后os新创建一个线程,将其绑定这个逻辑处理器接着运行,当之前的系统调用执行完成的时候,那个goroutine会放到等到队列,线程也会保存好,等待下次使用。

网络i/o稍有不同。网络i/o中,goroutine会与逻辑处理器分离,一旦该轮询器指示的某个网络读/网络写完成后,goroutine就会绑定对应的逻辑处理器来出来,处理完后又分离。

看起来线程池与协程有点像?,看下面的解释。

(协程在对堆上分配堆栈~,跟常见的上下文切换,栈保存信息有挺大区别)

协程在上下文切换的时候,信息保存在goroutine中。

Go的CSP并发模型:

1、多线程共享内存。

2、通信的方式共线数据。

goroutine在调度器调度的时候,也会出现防饿死而转到另一个goroutine的情况

 

package main

import (
    "fmt"
    "runtime"
    "sync"
    "time"
)

func general_func(){
   fmt.Println("it is bad")
}

func nice(){
   fmt.Println("it is nice")
}

func main(){
 
   fmt.Println(runtime.NumCPU())
   runtime.GOMAXPROCS(1)  //限制cpu核数

   //wg用来等待程序完成,计数器为2表示要等待两个goroutine结束
   var wg sync.WaitGroup
   wg.Add(2)
   //函数创建前加上go,即为创建goroutine 
   go nice() //这么调用goroutine
   go func(){ //声明一个匿名函数(也就是可以不要函数名),并创建一个goroutine
      defer fmt.Println("1") //当一个函数有多个defer的时候,会像调用栈那样,先进后出
      defer fmt.Println("2")
    //  defer wg.Done() //defer xx,表示xx在该函数退出的时候调用(常用于释放资源或错误处理)

      for count:=0 ; count <3; count++{
         for char:='a';char<'a'+4;char++{
            fmt.Printf("%c\n",char)
         }
      }
   }() //这里加个括号是闭包函数的使用,意思为直接调用该函数。

   go func(){ //声明一个匿名函数(也就是可以不要函数名),并创建一个goroutine
      defer fmt.Println("1") //当一个函数有多个defer的时候,会像调用栈那样,先进后出
      defer fmt.Println("2")
  //    defer wg.Done() //defer xx,表示xx在该函数退出的时候调用(常用于释放资源或错误处理)

      for count:=0 ; count <3; count++{
         for char:='a';char<'a'+4;char++{
            fmt.Printf("%c\n",char)
         }
      }
   }() //这里加个括号是闭包函数的使用,意思为直接调用该函数。
   
   //sleep 2s
   time.Sleep(time.Duration(2)*time.Second)  //如果不加这句话,也没有wg.Wait,有可能goroutine直接跑完而不显示上面的字符了
   //wg.Wait()
   fmt.Println("it sound good")
   
}

同步goroutine的原子函数:

原子函数底层通过加锁的访问,还挺神奇。

package main

import (
    "fmt"
    "runtime"
    "sync"
    "sync/atomic"
)
//define 多个变量
var ( 
   counter int64
   wg sync.WaitGroup
)

func incCounter(id int){
   defer wg.Done()
   for count:=0;count<2;count++{
      //atomic,原子函数,安全的加1
      atomic.AddInt64(&counter,1)
      //从当前线程退出,并将其放入队列
      runtime.Gosched()
   }
   fmt.Println(counter)
}

func main(){
   wg.Add(2) //计数器+2
   go incCounter(1)
   go incCounter(2)
   fmt.Println("it is good")
   wg.Wait() //等待计算器为0
   
}

类似的还有LoadInt64和StoreInt64,这两个一起用来分别读和写,那么不会进入竞争状态,原子函数会保持同步

互斥锁,跟其它语言一样。

channel,用make建立,分为有缓冲和无缓冲。(buffer为了平衡读写差异,稳)

无缓冲channel需要读写均准备好,才会传输数据。(确保同时传输数据)

使用channel时,会处于阻塞状态。

Example:

/*
来自书本的一个例子,两个人玩球,分别从channel拿数据,当拿到channel被关闭时,其获胜。
当其随机数%x为0时,其失败,并且close channel
*/
package main

import (
    "fmt"
    "sync"
    "math/rand"
)
//define 多个变量
var ( 
   counter int64
   wg sync.WaitGroup
)

func Play(name string,buffered chan int){
   defer wg.Done() //记得加上这句话,因为主goroutine需要wait 2个计数器
   for {
      ball,ok := <-buffered
      if !ok {
         fmt.Printf("it is good,Player %s won\n",name)
         return 
      }
      //生成一个100内的随机数
      n := rand.Intn(100)
      if n%15==0 { //%7 丢球
         fmt.Printf("it is bad,Player %s missed\n",name)
         close(buffered) //close 一个channel
         return 
      }
      fmt.Printf("Player %s Hit %d\n",name,ball) //打中的次数
      ball++
      buffered<-ball
   }

}

func main(){
   buffered := make(chan int)
  // unbuffered := make(chan string,10)
   wg.Add(2) //计数器+2
   go Play("AAA",buffered) //channel 需要像参数一样传递
   go Play("BBB",buffered)
   buffered <- 1 //初始为1 
   wg.Wait() //等待计算器为0
   
}

对于有缓冲的buffer:

只有在目标buffer满了,发送端的channel才会阻塞;

只有在获取的buffer满了,读动作的buffer才会阻塞;