为什么要使用goroutine呢

需求:要统计1-10000000的数字中那些是素数,并打印这些素数?
素数:就是除了1和它本身不能被其他数整除的数
实现方法:

  • 传统方法,通过一个for循环判断各个数是不是素数
  • 使用并发或者并行的方式,将统计素数的任务分配给多个goroutine去完成,这个时候就用到了goroutine
  • goroutine 结合 channel

    进程、线程以及并行、并发

    进程

    进程(Process)就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位,进程是一个动态概念,是程序在执行过程中分配和管理资源的基本单位,每一个进程都有一个自己的地址空间。一个进程至少有5种基本状态,它们是:初始态,执行态,等待状态,就绪状态,终止状态。
    通俗的讲进程就是一个正在执行的程序。

    线程

    线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位
    一个进程可以创建多个线程,同一个进程中多个线程可以并发执行 ,一个线程要运行的话,至少有一个进程

    并发和并行

    并发:多个线程同时竞争一个位置,竞争到的才可以执行,每一个时间段只有一个线程在执行。
    并行:多个线程可以同时执行,每一个时间段,可以有多个线程同时执行。
    通俗的讲多线程程序在单核CPU上面运行就是并发,多线程程序在多核CUP上运行就是并行,如果线程数大于CPU核数,则多线程程序在多个CPU上面运行既有并行又有并发
    使用Goroutine Channel实现并发和并行 - 图1
    使用Goroutine Channel实现并发和并行 - 图2

    Golang中协程(goroutine)以及主线程

    golang中的主线程:(可以理解为线程/也可以理解为进程),在一个Golang程序的主线程上可以起多个协程。Golang中多协程可以实现并行或者并发。
    协程:可以理解为用户级线程,这是对内核透明的,也就是系统并不知道有协程的存在,是完全由用户自己的程序进行调度的。Golang的一大特色就是从语言层面原生持协程,在函数或者方法前面加go关键字就可创建一个协程。可以说Golang中的协程就是goroutine。
    使用Goroutine Channel实现并发和并行 - 图3
    Golang中的多协程有点类似于Java中的多线程

    多协程和多线程

    多协程和多线程:Golang中每个goroutine(协程)默认占用内存远比Java、C的线程少。
    OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB左右),一个goroutine(协程)占用内存非常小,只有2KB左右,多协程goroutine切换调度开销方面远比线程要少。
    这也是为什么越来越多的大公司使用Golang的原因之一。

    goroutine的使用以及sync.WaitGroup

    并行执行需求

    在主线程(可以理解成进程)中,开启一个goroutine,该协程每隔50毫秒秒输出“你好golang”
    在主线程中也每隔50毫秒输出“你好golang”,输出10次后,退出程序,要求主线程和goroutine同时执行。
    这是时候,我们就可以开启协程来了,通过 go关键字开启

    1. // 协程需要运行的方法
    2. func test() {
    3. for i := 0; i < 5; i++ {
    4. fmt.Println("test 你好golang")
    5. time.Sleep(time.Millisecond * 100)
    6. }
    7. }
    8. func main() {
    9. // 通过go关键字,就可以直接开启一个协程
    10. go test()
    11. // 这是主进程执行的
    12. for i := 0; i < 5; i++ {
    13. fmt.Println("main 你好golang")
    14. time.Sleep(time.Millisecond * 100)
    15. }
    16. }

    运行结果如下,我们能够看到他们之间不存在所谓的顺序关系了

    1. main 你好golang
    2. test 你好golang
    3. main 你好golang
    4. test 你好golang
    5. test 你好golang
    6. main 你好golang
    7. main 你好golang
    8. test 你好golang
    9. test 你好golang
    10. main 你好golang

    但是上述的代码其实还有问题的,也就是说当主进程执行完毕后,不管协程有没有执行完成,都会退出
    使用Goroutine Channel实现并发和并行 - 图4
    这是使用我们就需要用到 sync.WaitGroup等待协程
    首先我们需要创建一个协程计数器

    1. // 定义一个协程计数器
    2. var wg sync.WaitGroup

    然后当我们开启协程的时候,我们要让计数器加1

    1. // 开启协程,协程计数器加1
    2. wg.Add(1)
    3. go test2()

    当我们协程结束前,我们需要让计数器减1

    1. // 协程计数器减1
    2. wg.Done()

    完整代码如下

    1. // 定义一个协程计数器
    2. var wg sync.WaitGroup
    3. func test() {
    4. // 这是主进程执行的
    5. for i := 0; i < 1000; i++ {
    6. fmt.Println("test1 你好golang", i)
    7. //time.Sleep(time.Millisecond * 100)
    8. }
    9. // 协程计数器减1
    10. wg.Done()
    11. }
    12. func test2() {
    13. // 这是主进程执行的
    14. for i := 0; i < 1000; i++ {
    15. fmt.Println("test2 你好golang", i)
    16. //time.Sleep(time.Millisecond * 100)
    17. }
    18. // 协程计数器减1
    19. wg.Done()
    20. }
    21. func main() {
    22. // 通过go关键字,就可以直接开启一个协程
    23. wg.Add(1)
    24. go test()
    25. // 协程计数器加1
    26. wg.Add(1)
    27. go test2()
    28. // 这是主进程执行的
    29. for i := 0; i < 1000; i++ {
    30. fmt.Println("main 你好golang", i)
    31. //time.Sleep(time.Millisecond * 100)
    32. }
    33. // 等待所有的协程执行完毕
    34. wg.Wait()
    35. fmt.Println("主线程退出")
    36. }

    设置Go并行运行的时候占用的cpu数量

    Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个oS线程上。
    Go 语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。
    Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。

    1. func main() {
    2. // 获取cpu个数
    3. npmCpu := runtime.NumCPU()
    4. fmt.Println("cup的个数:", npmCpu)
    5. // 设置允许使用的CPU数量
    6. runtime.GOMAXPROCS(runtime.NumCPU() - 1)
    7. }

    for循环开启多个协程

    类似于Java里面开启多个线程,同时执行

    1. func test(num int) {
    2. for i := 0; i < 10; i++ {
    3. fmt.Printf("协程(%v)打印的第%v条数据 \n", num, i)
    4. }
    5. // 协程计数器减1
    6. vg.Done()
    7. }
    8. var vg sync.WaitGroup
    9. func main() {
    10. for i := 0; i < 10; i++ {
    11. go test(i)
    12. vg.Add(1)
    13. }
    14. vg.Wait()
    15. fmt.Println("主线程退出")
    16. }

    因为我们协程会在主线程退出后就终止,所以我们还需要使用到 sync.WaitGroup来控制主线程的终止。

    Channel管道

    管道是Golang在语言级别上提供的goroutine间的通讯方式,我们可以使用channel在多个goroutine之间传递消息。如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。
    Golang的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。
    Go语言中的管道(channel)是一种特殊的类型。管道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个管道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

    channel类型

    channel是一种类型,一种引用类型。声明管道类型的格式如下:

    1. // 声明一个传递整型的管道
    2. var ch1 chan int
    3. // 声明一个传递布尔类型的管道
    4. var ch2 chan bool
    5. // 声明一个传递int切片的管道
    6. var ch3 chan []int

    创建channel

    声明管道后,需要使用make函数初始化之后才能使用

    1. make(chan 元素类型, 容量)

    举例如下:

    1. // 创建一个能存储10个int类型的数据管道
    2. ch1 = make(chan int, 10)
    3. // 创建一个能存储4个bool类型的数据管道
    4. ch2 = make(chan bool, 4)
    5. // 创建一个能存储3个[]int切片类型的管道
    6. ch3 = make(chan []int, 3)

    channel操作

    管道有发送,接收和关闭的三个功能
    发送和接收 都使用 <- 符号
    现在我们先使用以下语句定义一个管道:

    1. ch := make(chan int, 3)

    发送

    将数据放到管道内,将一个值发送到管道内

    1. // 把10发送到ch中
    2. ch <- 10

    取操作

    1. x := <- ch

    关闭管道.

    通过调用内置的close函数来关闭管道

    1. close(ch)

    完整示例

    1. // 创建管道
    2. ch := make(chan int, 3)
    3. // 给管道里面存储数据
    4. ch <- 10
    5. ch <- 21
    6. ch <- 32
    7. // 获取管道里面的内容
    8. a := <- ch
    9. fmt.Println("打印出管道的值:", a)
    10. fmt.Println("打印出管道的值:", <- ch)
    11. fmt.Println("打印出管道的值:", <- ch)
    12. // 管道的值、容量、长度
    13. fmt.Printf("地址:%v 容量:%v 长度:%v \n", ch, cap(ch), len(ch))
    14. // 管道的类型
    15. fmt.Printf("%T \n", ch)
    16. // 管道阻塞(当没有数据的时候取,会出现阻塞,同时当管道满了,继续存也会)
    17. <- ch // 没有数据取,出现阻塞
    18. ch <- 10
    19. ch <- 10
    20. ch <- 10
    21. ch <- 10 // 管道满了,继续存,也出现阻塞

    for range从管道循环取值

    当向管道中发送完数据时,我们可以通过close函数来关闭管道,当管道被关闭时,再往该管道发送值会引发panic,从该管道取值的操作会取完管道中的值,再然后取到的值一直都是对应类型的零值。那如何判断一个管道是否被关闭的呢?

    1. // 创建管道
    2. ch := make(chan int, 10)
    3. // 循环写入值
    4. for i := 0; i < 10; i++ {
    5. ch <- i
    6. }
    7. // 关闭管道
    8. close(ch)
    9. // for range循环遍历管道的值(管道没有key)
    10. for value := range ch {
    11. fmt.Println(value)
    12. }
    13. // 通过上述的操作,能够打印值,但是出出现一个deadlock的死锁错误,也就说我们需要关闭管道

    注意:使用for range遍历的时候,一定在之前需要先关闭管道
    思考:通过for循环来遍历管道,需要关闭么?

    1. // 创建管道
    2. ch := make(chan int, 10)
    3. // 循环写入值
    4. for i := 0; i < 10; i++ {
    5. ch <- i
    6. }
    7. for i := 0; i < 10; i++ {
    8. fmt.Println(<- ch)
    9. }

    上述代码没有报错,说明通过for i的循环方式,可以不关闭管道

    Goroutine 结合 channel 管道

    需求1:定义两个方法,一个方法给管道里面写数据,一个给管道里面读取数据。要求同步进行。

  • 开启一个fn1的的协程给向管道inChan中写入10条数据

  • 开启一个fn2的协程读取inChan中写入的数据
  • 注意:fn1和fn2同时操作一个管道
  • 主线程必须等待操作完成后才可以退出
    1. func write(ch chan int) {
    2. for i := 0; i < 10; i++ {
    3. fmt.Println("写入:", i)
    4. ch <- i
    5. time.Sleep(time.Microsecond * 10)
    6. }
    7. wg.Done()
    8. }
    9. func read(ch chan int) {
    10. for i := 0; i < 10; i++ {
    11. fmt.Println("读取:", <- ch)
    12. time.Sleep(time.Microsecond * 10)
    13. }
    14. wg.Done()
    15. }
    16. var wg sync.WaitGroup
    17. func main() {
    18. ch := make(chan int, 10)
    19. wg.Add(1)
    20. go write(ch)
    21. wg.Add(1)
    22. go read(ch)
    23. // 等待
    24. wg.Wait()
    25. fmt.Println("主线程执行完毕")
    26. }
    管道是安全的,是一边写入,一边读取,当读取比较快的时候,会等待写入

    goroutine 结合 channel打印素数

    使用Goroutine Channel实现并发和并行 - 图5 ```go // 想intChan中放入 1~ 120000个数 func putNum(intChan chan int) { for i := 2; i < 120000; i++ {
    1. intChan <- i
    } wg.Done() close(intChan) } // cong intChan取出数据,并判断是否为素数,如果是的话,就把得到的素数放到primeChan中 func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) { for value := range intChan {
    1. var flag = true
    2. for i := 2; i <= int(math.Sqrt(float64(value))); i++ {
    3. if i % i == 0 {
    4. flag = false
    5. break
    6. }
    7. }
    8. if flag {
    9. // 是素数
    10. primeChan <- value
    11. break
    12. }
    } // 这里需要关闭 primeChan,因为后面需要遍历输出 primeChan exitChan <- true wg.Done() } // 打印素数 func printPrime(primeChan chan int) { for value := range primeChan {
    1. fmt.Println(value)
    } wg.Done() } var wg sync.WaitGroup func main() { // 写入数字 intChan := make(chan int, 1000) // 存放素数 primeChan := make(chan int, 1000) // 存放 primeChan退出状态 exitChan := make(chan bool, 16) // 开启写值的协程 go putNum(intChan) // 开启计算素数的协程 for i := 0; i < 10; i++ {
    1. wg.Add(1)
    2. go primeNum(intChan, primeChan, exitChan)
    } // 开启打印的协程 wg.Add(1) go printPrime(primeChan) // 匿名自运行函数 wg.Add(1) go func() {
    1. for i := 0; i < 16; i++ {
    2. // 如果exitChan 没有完成16次遍历,将会等待
    3. <- exitChan
    4. }
    5. // 关闭primeChan
    6. close(primeChan)
    7. wg.Done()
    }() wg.Wait() fmt.Println(“主线程执行完毕”)

}

  1. <a name="PqIeO"></a>
  2. ## 单向管道
  3. 有时候我们会将管道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中,使用管道都会对其进行限制,比如限制管道在函数中只能发送或者只能接受
  4. > 默认的管道是 可读可写
  5. ```go
  6. // 定义一种可读可写的管道
  7. var ch = make(chan int, 2)
  8. ch <- 10
  9. <- ch
  10. // 管道声明为只写管道,只能够写入,不能读
  11. var ch2 = make(chan<- int, 2)
  12. ch2 <- 10
  13. // 声明一个只读管道
  14. var ch3 = make(<-chan int, 2)
  15. <- ch3

Select多路复用

在某些场景下我们需要同时从多个通道接收数据。这个时候就可以用到golang中给我们提供的select多路复用。 通常情况通道在接收数据时,如果没有数据可以接收将会发生阻塞。
比如说下面代码来实现从多个通道接受数据的时候就会发生阻塞
这种方式虽然可以实现从多个管道接收值的需求,但是运行性能会差很多。为了应对这种场景,Go内置了select关键字,可以同时响应多个管道的操作。
select的使用类似于switch 语句,它有一系列case分支和一个默认的分支。每个case会对应一个管道的通信(接收或发送)过程。select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句。具体格式如下:

  1. intChan := make(chan int, 10)
  2. intChan <- 10
  3. intChan <- 12
  4. intChan <- 13
  5. stringChan := make(chan int, 10)
  6. stringChan <- 20
  7. stringChan <- 23
  8. stringChan <- 24
  9. // 每次循环的时候,会随机中一个chan中读取,其中for是死循环
  10. for {
  11. select {
  12. case v:= <- intChan:
  13. fmt.Println("从initChan中读取数据:", v)
  14. case v:= <- stringChan:
  15. fmt.Println("从stringChan中读取数据:", v)
  16. default:
  17. fmt.Println("所有的数据获取完毕")
  18. return
  19. }
  20. }

tip:使用select来获取数据的时候,不需要关闭chan,不然会出现问题

Goroutine Recover解决协程中出现的Panic

  1. func sayHello() {
  2. for i := 0; i < 10; i++ {
  3. fmt.Println("hello")
  4. }
  5. }
  6. func errTest() {
  7. // 捕获异常
  8. defer func() {
  9. if err := recover(); err != nil {
  10. fmt.Println("errTest发生错误")
  11. }
  12. }()
  13. var myMap map[int]string
  14. myMap[0] = "10"
  15. }
  16. func main {
  17. go sayHello()
  18. go errTest()
  19. }

当我们出现问题的时候,我们还是按照原来的方法,通过defer func创建匿名自启动

  1. // 捕获异常
  2. defer func() {
  3. if err := recover(); err != nil {
  4. fmt.Println("errTest发生错误")
  5. }
  6. }()

Go中的并发安全和锁

如下面一段代码,我们在并发环境下进行操作,就会出现并发访问的问题

  1. var count = 0
  2. var wg sync.WaitGroup
  3. func test() {
  4. count++
  5. fmt.Println("the count is : ", count)
  6. time.Sleep(time.Millisecond)
  7. wg.Done()
  8. }
  9. func main() {
  10. for i := 0; i < 20; i++ {
  11. wg.Add(1)
  12. go test()
  13. }
  14. time.Sleep(time.Second * 10)
  15. }

互斥锁

互斥锁是传统并发编程中对共享资源进行访问控制的主要手段,它由标准库sync中的Mutex结构体类型表示。sync.Mutex类型只有两个公开的指针方法,Lock和Unlock。Lock锁定当前的共享资源,Unlock 进行解锁

  1. // 定义一个锁
  2. var mutex sync.Mutex
  3. // 加锁
  4. mutex.Lock()
  5. // 解锁
  6. mutex.Unlock()

完整代码

  1. var count = 0
  2. var wg sync.WaitGroup
  3. var mutex sync.Mutex
  4. func test() {
  5. // 加锁
  6. mutex.Lock()
  7. count++
  8. fmt.Println("the count is : ", count)
  9. time.Sleep(time.Millisecond)
  10. wg.Done()
  11. // 解锁
  12. mutex.Unlock()
  13. }
  14. func main() {
  15. for i := 0; i < 20; i++ {
  16. wg.Add(1)
  17. go test()
  18. }
  19. time.Sleep(time.Second * 10)
  20. }

通过下面命令,build的时候,可以查看是否具有竞争关系

  1. // 通过 -race 参数进行构建
  2. go build -race main.go
  3. // 运行插件
  4. main.ext

读写互斥锁

互斥锁的本质是当一个goroutine访问的时候,其他goroutine都不能访问。这样在资源同步,避免竞争的同时也降低了程序的并发性能。程序由原来的并行执行变成了串行执行。
其实,当我们对一个不会变化的数据只做“读”操作的话,是不存在资源竞争的问题的。因为数据是不变的,不管怎么读取,多少goroutine同时读取,都是可以的。
所以问题不是出在“读”上,主要是修改,也就是“写”。修改的数据要同步,这样其他goroutine才可以感知到。所以真正的互斥应该是读取和修改、修改和修改之间,读和读是没有互斥操作的必要的。
因此,衍生出另外一种锁,叫做读写锁。
读写锁可以让多个读操作并发,同时读取,但是对于写操作是完全互斥的。也就是说,当一个goroutine进行写操作的时候,其他goroutine既不能进行读操作,也不能进行写操作。
GO中的读写锁由结构体类型sync.RWMutex表示。此类型的方法集合中包含两对方法: