Go协程简单学习

666 阅读4分钟

什么是协程?

  1. 协程类似于线程,但是比线程更加轻量。一个程序启动会占用一个进程 而一个进程可以拥有多个线程 ,一个线程可以拥有多个协程。
  2. 一个进程至少包含一个主线程,一个主线程可以有更多的子线程。 线程有两种调度策略,一是:分时调度,二是:抢占式调度。
  3. 对于操作系统来说 线程是最小的执行单元 进程是最小的资源管理单位 线程一般有五种状态:初始化、可运行、运行中、阻塞 、销毁。
  4. 协程是用户态执行,不由操作系统内核管理 是完全由程序自己调度和控制的 。
  5. 协程的创建、切换、挂起、销毁全部为内存操作。
  6. 协程属于线程,协程是在线程里面执行的。协程调度策略:协作式调度。

go的goroutine

go是一个种对并发非常友好的语言。它提供了两大机制的简单语法:协程(goroutine)和管道(channel)。

goroutine是轻量级的线程 go在语言层面就支持原生协程
go的协程相对于线程开销更小 大概在2kb 根据程序开销需求增大或者缩小 线程必须指定堆栈的大小 大小是固定的
goroutine 是通过 GPM 调度模型实现的。GPM 调度模型

简单使用协程原生支持

package main

import (
   "fmt"
 "time")

func main()  {
   fmt.Println("测试")
   // 这里开始异步

  go func() {
      time.Sleep(time.Microsecond*10)
      fmt.Println("测试3")
   }()
   fmt.Println("测试3")
   //延迟主程序退出
  time.Sleep(time.Microsecond*100)
}

Go属于多线程版协程,可以利用多核CPU,同一时间可以有多个协程在调度,会存在并发问题。

下面这段代码,执行结果如何。按正常应该是打印1-20

package main

import (
    "fmt"
    "time"
)
var count =0

func main()  {
    for i:=0;i<=20;i++ {
        go func() {
            count ++
            fmt.Println(count)
        }()
    }
   time.Sleep(time.Microsecond*100)
}
$ go run main.go //第一次执行
1
3
5
2
14
18
19
10
11
12
13
6
15
16
17
4
8
20
9
7
21
$ go run main.go //第二次执行
1
3
18
2
5
7
8
19
10
11
12
13
14
15
16
17
4
9
20
21
6

每次执行结果是不一样的。在做写入操作的时候 同时多个协程写入 导致数据乱七八糟的打印
从变量中读取变量是唯一安全的并发处理变量的方式。 你可以有想要多少就多少的读取者, 但是写操作必须要得同步。 有太多的方法可以做到这个了,包括使用一些依赖于特殊的 CPU 指令集的真原子操作。然而,常用的操作还是使用互斥量。

写入数据时给协程加锁

package main

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

var (
    lock sync.Mutex
    count =0
)

func main()  {
    for i:=0;i<=20;i++ {
        go func() {
            lock.Lock()
            defer lock.Unlock()
            count ++
            fmt.Println(count)
        }()
    }

    time.Sleep(time.Microsecond*100)
}

我们在给count变量自增时加锁,保证同一时间只有一个协程在写入 结果和我们希望的结果一至。

$ go run main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

看似我们解决了并发问题,但也违背并发编程的初心。而且容易造成死锁问题。使用单个锁时,这没有问题,但是如果你在代码中使用两个或者更多的锁,很容易出现一种危险的情况,当协程 A 拥有锁 lockA ,想去访问锁 lockB ,同时协程 B 拥有锁 lockB 并需要访问锁 lockA 。

通道

通道是多协程调度资源共享的一个强大机制 是协程之间传递数据的共享管道。一个协程可以通过管道向另外一个协程传递数据 所以在任意一个时间点 只有一个协程可以访问数据

创建一个管道

c := make(chan int)

这个通道的类型是 chan int。因此,要将通道传递给函数,我们的函数签名看起来是这个样子的:

func worker(c chan int) { ... }

管道支持两个操作

//接收数据
CHANNEL <- DATA
//发送数据
VAR := <-CHANNEL

使用 for 进行管道数据接收或者发送

举个例子

package main

import (
  "fmt"
  "time"
  "math/rand"
)

func main() {
  c := make(chan int)
  for i := 0; i < 5; i++ {
    worker := &Worker{id: i}
    go worker.process(c)
  }

  for {
    c <- rand.Int()
    time.Sleep(time.Millisecond * 50)
  }
}

type Worker struct {
  id int
}

func (w *Worker) process(c chan int) {
  for {
    data := <-c
    fmt.Printf("worker %d got %d\n", w.id, data)
  }
}

缓冲通道

无缓冲管道的发送和接收过程是阻塞的,还可以创建一个有缓冲(Buffer)的管道。
只在缓冲已满的情况,才会阻塞向缓冲管道(Bufferer Channel)发送数据。同样,只有在缓冲为空的时候,才会阻塞从缓冲管道接收数据。
通过向make函数再传递一个表示容量的参数(指定缓冲的大小),可以创建缓冲管道。
要让一个管道有缓冲,上面语法中的capacity应该大于0。无缓冲管道的容量默认为0.

ch := make (chan type, capacity)

select

即使有缓冲,在某些时候我们需要开始删除消息。我们不能为了让 worker 轻松而耗尽所有内存。为了实现这个,我们使用 Go 的 select:

select的作用

Go提供了一个关键字select。通过select可以监听channel上面的数据流动,由select开始一个选择 选择条件由case语句描述。
select使用限制,每个case语句里必须是一个IO操作。所以一般select需要配合协程管道来使用
举个例子:

for {
  select {
  case c <- rand.Int():
    // 可选的代码在这里
  default:
    // 这里可以留空以静默删除数据
    fmt.Println("dropped")
  }
  time.Sleep(time.Millisecond * 50)
}

超时

for {
  select {
  case c <- rand.Int():
  //指定时间后做相关操作 主要做同步工作例如请求接口需要返回数据 超过响应时常则报错防止协程一直阻塞
  case <-time.After(time.Millisecond * 100): 
    fmt.Println("timed out")
  default //默认执行 没有default,select会阻塞    
  }
  time.Sleep(time.Millisecond * 50)
}