Go并发编程 | 青训营笔记

104 阅读2分钟

这是我参与「第五届青训营 」笔记创作活动的第1天

1.Goroutine

为了更好理解Goroutine,先讲一下线程和协程的概念

线程(Thread) :有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。

线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程的切换一般也由操作系统调度。

协程(coroutine) :又称微线程与子例程(或者称为函数)一样,协程(coroutine)也是一种程序组件。相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛。

和线程类似,共享堆,不共享栈,协程的切换一般由程序员在代码中显式控制。它避免了上下文切换的额外耗费,兼顾了多线程的优点,简化了高并发程序的复杂。

Goroutine和其他语言的协程(coroutine)在使用方式上类似,但从字面意义上来看不同(一个是Goroutine,一个是coroutine),再就是协程是一种协作任务控制机制,在最简单的意义上,协程不是并发的,而Goroutine支持并发的。因此Goroutine可以理解为一种Go语言的协程。同时它可以运行在一个或多个线程上。

Go 程序中使用 go 关键字为一个函数创建一个 goroutine。一个函数可以被创建多个 goroutine,一个 goroutine 必定对应一个函数。

为一个普通函数创建 goroutine 的写法如下:

go 函数名( 参数列表 )

  • 函数名:要调用的函数名。
  • 参数列表:调用函数需要传入的参数。

例子如下:使用go关键字,将hello函数并发执行

package main

import (
   "fmt"
   "time"
)

func Hello(i int) {
   fmt.Println(i)
}

func main() {
   for i := 0; i < 5; i++ {
      go func(j int) {
         Hello(j)
      }(i)
   }
   time.Sleep(time.Second)
}

打印结果如下:

4
0
2
3
1

2.Channel

Channel即通道,每个通道都有与其相关的类型。该类型是通道允许传输的数据类型。(通道的零值为nil。nil通道没有任何用处,因此通道必须使用类似于map和切片的方法来定义。)

channel有两种形式的,一种是无缓冲的,一个线程向这个channel发送了消息后,会阻塞当前的这个线程,知道其他线程去接收这个channel的消息。无缓冲的形式如下:

intChan := make(chan int)

带缓冲的channel,是可以指定缓冲的消息数量,当消息数量小于指定值时,不会出现阻塞,超过之后才会阻塞,需要等待其他线程去接收channel处理,带缓冲的形式如下:

intChan := make(chan int3)

通过channel可以比较方便的实现生产者消费者模型,这里开启一个生产者线程,一个消费者线程,生产者线程往channel中发送消息,同时阻塞,消费者线程轮询获取channel中的消息,
进行处理,然后阻塞,这时生产者线程唤醒继续后面的逻辑,如此便形成了简单的生产者消费者模型。

例子:使用带缓冲的channel和无缓冲的channel进行打印0-9的平方

package main

func main() {
   src := make(chan int)
   dest := make(chan int, 2)
   go func() {
      defer close(src)
      for i := 0; i < 10; i++ {
         src <- i
      }
   }()
   go func() {
      defer close(dest)
      for i := range src {
         dest <- i * i
      }
   }()

   for i := range dest {
      //操作得到的数据
      println(i)
   }
}

3.Sync

Go语言的sync包提供了常见的并发编程同步原语

3.1互斥锁-Mutex

同一时刻只能有一个读或写的场景,一个互斥锁只能同时被一个goroutine锁定,其他go程将被阻塞直到互斥锁被解锁,重新争夺互斥锁 使用lock()进行上锁,使用Unlock()进行释放锁

package main

import (
   "sync"
   "time"
)

var (
   x    int64
   lock sync.Mutex
)

func main() {
   x = 0
   for i := 0; i < 5; i++ {
      go addWithOutLock()
   }
   time.Sleep(time.Second)
   println("WithOutLock  ----->  ", x)
   x = 0
   for i := 0; i < 5; i++ {
      go addWhitLock()
   }
   time.Sleep(time.Second)
   println("WhitLock  ----->  ", x)

}

func addWhitLock() {
   for i := 0; i < 2000; i++ {
      lock.Lock()
      x += 1
      lock.Unlock()
   }
}

func addWithOutLock() {
   for i := 0; i < 2000; i++ {
      x += 1
   }
}

执行结果:

WithOutLock  ----->   8286
WhitLock  ----->   10000

3.2等待组-WaitGroup

waitGroup.go中有三个常用的方法,分别是Add(int),Done(),Wait()。

1.创建子协程先调用Add增加等待计数

2.子协程结束后调用Done减少协程计数

3.主协程中调用Wait方法进行等待,直到计数器归零继续执行

package main

import (
   "fmt"
   "sync"
)

func hello(i int) {
   println(i)
}

func main() {
   var wg sync.WaitGroup
   wg.Add(5)
   for i := 0; i < 5; i++ {
      go func(j int) {
         defer wg.Done()
         hello(j)
      }(i)
   }
   wg.Wait()
   fmt.Println("main over")
}