3、Go语言并发编程 - 图1

一、并发编程基本概念

并发编程开发将一个过程按照并行算法拆分为多个可以独立执行的代码块,从而充分利用多核和多处理器提高系统吞吐率

顺序、并发与并行
a)顺序是指发起执行的程序只能有一个
b)并发是指同时发起执行(同时处理)的程序可以有很多个(单车道并排只能有一辆车,可同时驶入路段多辆车)
c)并行是指同时执行(同时做)的程序可以有很多个(多车道并排可以有多个车)
操作系统:进程和线程
进程:资源分配的基本单位
线程:CPU调度的基本单位

二、例程

Go语言中每个并发执行的单元叫Goroutine,使用go关键字后接函数调用 来创建Goroutine

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func testA() {
  7. for i := 0; i <= 10; i++ {
  8. fmt.Printf("%d", i)
  9. }
  10. fmt.Println()
  11. }
  12. func testB() {
  13. for i := 'A'; i <= 'Z'; i++ {
  14. fmt.Printf("%c", i)
  15. }
  16. fmt.Println()
  17. }
  18. func main() {
  19. fmt.Println("Start")
  20. go testA() // 启动一个例程
  21. go testB() // 启动一个例程
  22. time.Sleep(time.Second * 3)
  23. fmt.Println("End")
  24. // main例程 =》 主例程
  25. // go 工作例程
  26. // 主例程 不等待工作例程执行结束
  27. }

输出:
3、Go语言并发编程 - 图2

main函数也是由一个例程来启动执行,这个例程称为主例程,其他例程叫工作例程,主例程结束后工作例程也会随之销毁,使用sync.WaitGroup(技术信号量)来维护执行例程执行状态 可以通过runtime包中的GoSched让例程主动让出CPU,也可以通过time.Sleep让例程休眠从而让出CPU

2.1 技术信号量

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. )
  6. func testA(wg *sync.WaitGroup) {
  7. for i := 0; i <= 10; i++ {
  8. fmt.Printf("%d", i)
  9. }
  10. fmt.Println()
  11. // 等待信号量-1
  12. defer wg.Done()
  13. }
  14. func testB(wg *sync.WaitGroup) {
  15. for i := 'A'; i <= 'Z'; i++ {
  16. fmt.Printf("%c", i)
  17. }
  18. fmt.Println()
  19. // 等待信号量-1
  20. defer wg.Done()
  21. }
  22. func main() {
  23. fmt.Println("Start")
  24. // 计算信号量
  25. // 启动例程的之前,+1
  26. // 当例程执行结束时,-1
  27. //var wg sync.WaitGroup
  28. wg := new(sync.WaitGroup)
  29. wg.Add(2)
  30. go testA(wg) // 启动一个例程
  31. go testB(wg) // 启动一个例程
  32. //time.Sleep(time.Second * 3)
  33. wg.Wait()
  34. fmt.Println("End")
  35. }

2.2 闭包陷阱

    > 闭包:在一个函数的内部引用了函数外部的变量,使函数的外部变量的生命周期延长

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. )
  6. func main() {
  7. var wg sync.WaitGroup
  8. // 使用for循环启动3个例程
  9. for i := 0; i < 3; i++ {
  10. wg.Add(1)
  11. go func(i int) {
  12. fmt.Println(i)
  13. wg.Done()
  14. }(i)
  15. }
  16. wg.Wait()
  17. }

输出:
3、Go语言并发编程 - 图3
因为闭包使用函数外变量,当例程执行是,外部变量已经发生变化,导致打印内容不正确,可使用在创建例程时通过函数传递参数(值拷贝)方式避免

三、 并发程序的通信方式

3.1 共享数据(同步)

多个并发程序需要对同一个资源进行访问,则需要先申请资源的访问权限,同时再使用完成后释放资源的访问权。当资源被其他程序已申请访问权后,程序应该等待访问权被释放并被申请到时进行访问操作。同一时间资源只能被一个程序访问和操作

  1. package main
  2. import (
  3. "fmt"
  4. "runtime"
  5. "sync"
  6. )
  7. // 10 个例程分别给salary + 10 1000
  8. // 10 个例程分别给salary - 10 1000
  9. func main() {
  10. var salary int = 0
  11. var wg sync.WaitGroup
  12. // 定义锁
  13. var locker sync.Mutex
  14. fmt.Println("Start")
  15. for i := 0; i < 10; i++ {
  16. wg.Add(2)
  17. go func() {
  18. defer wg.Done()
  19. for i := 0; i < 1000; i++ {
  20. locker.Lock() // 加锁
  21. salary += 10
  22. locker.Unlock() // 释放锁
  23. runtime.Gosched()
  24. }
  25. }()
  26. go func() {
  27. defer wg.Done()
  28. for i := 0; i < 1000; i++ {
  29. locker.Lock()
  30. salary -= 10
  31. locker.Unlock()
  32. runtime.Gosched()
  33. }
  34. }()
  35. }
  36. wg.Wait()
  37. fmt.Println(salary)
  38. fmt.Println("End")
  39. }
  40. 输出:
  41. Start
  42. 0
  43. End

3.2 管道(异步)

a. 不带缓冲区的管道

死锁:无缓冲区的管道,如果一直往channel里面写数据,而不读取则会产生死锁
数据处理者处理完数据后将数据放入缓冲区中,数据接收者从缓冲区中获取数据,处理者不用等待接收者是否准备好处理数据

  1. package main
  2. import "fmt"
  3. type Element struct {
  4. }
  5. func main() {
  6. // 管道中放什么类型需要提前执行
  7. // 声明int类型的管道channel
  8. // var channel chan int =make(chan int)
  9. channel := make(chan int) // 无缓冲区管道
  10. // 初始化&赋值
  11. // make()
  12. // channel = make(chan int)
  13. // 操作
  14. // 读,写
  15. go func() {
  16. fmt.Println("go Start")
  17. channel <- 1 // 将1写入管道
  18. fmt.Println("go end")
  19. }()
  20. fmt.Println("channel begin")
  21. num := <-channel // 如果未读取到数据会进行阻塞
  22. fmt.Println("channel after") // go start 之后
  23. fmt.Println(num)
  24. }
  25. 输出:
  26. channel begin
  27. go Start
  28. go end
  29. channel after
  30. 1

如果管道关闭后则管道不能继续写close(channel),但是可以读取channel,但是读取到的channel值为0
判断管道是否关闭,通过v,ok := <-channel,如果oktrue则表示管道开启,否则为关闭

  • 遍历管道

    1. package main
    2. import "fmt"
    3. func main() {
    4. channel := make(chan int)
    5. go func() {
    6. channel <- 1
    7. channel <- 2
    8. channel <- 3
    9. close(channel)
    10. }()
    11. for num := range channel {
    12. fmt.Println(num)
    13. }
    14. }
    15. 输出:
    16. 1
    17. 2
    18. 3

    b.带缓冲区的管道

    死锁:有缓冲区的管道,如果一直往channel里面写数据超过最大长度后,不进行读取则会产生死锁

    c.只读只写管道

    chan<-:只写管道
    <-chan:只读管道

    1. package main
    2. import (
    3. "fmt"
    4. )
    5. // 只写
    6. func in(channel chan<- int) {
    7. channel <- 1
    8. channel <- 2
    9. }
    10. // 只读
    11. func out(channel <-chan int) {
    12. fmt.Println(<-channel)
    13. fmt.Println(<-channel)
    14. }
    15. func main() {
    16. channel := make(chan int, 3)
    17. in(channel)
    18. out(channel)
    19. }
    20. 输出:
    21. 1
    22. 2

    3.3多路复用

    1. package main
    2. import (
    3. "fmt"
    4. "time"
    5. )
    6. func main() {
    7. // 在多个管道中只要有一个操作成功就执行相应逻辑
    8. channelA := make(chan int)
    9. channelB := make(chan int)
    10. go func() {
    11. time.Sleep(3 * time.Second)
    12. //channelA <- 1
    13. close(channelA)
    14. }()
    15. for {
    16. select {
    17. case v, ok := <-channelA:
    18. fmt.Println("a", v, ok)
    19. case v, ok := <-channelB:
    20. fmt.Println("b", v, ok)
    21. default:
    22. fmt.Println("default")
    23. }
    24. }
    25. }

    程序执行超时时间:

    1. package main
    2. import (
    3. "fmt"
    4. "math/rand"
    5. "time"
    6. )
    7. func task(result chan<- int64) {
    8. interval := rand.Intn(10)
    9. fmt.Println("sleep:", interval)
    10. time.Sleep(time.Duration(interval) * time.Second) // 随机休眠n秒
    11. result <- time.Now().Unix()
    12. }
    13. func main() {
    14. rand.Seed(time.Now().Unix()) // 设置随机数种子
    15. //var result chan int64 =make(chan int64)
    16. //timeout := make(chan int)
    17. result := make(chan int64)
    18. go task(result)
    19. //go func() {
    20. // time.Sleep(3 * time.Second)
    21. // close(timeout)
    22. //}()
    23. select {
    24. case r := <-result:
    25. fmt.Println("success:", r)
    26. //case <-timeout:
    27. // fmt.Println("timeout")
    28. case <-time.After(3 * time.Second):
    29. fmt.Println("timeout")
    30. }
    31. fmt.Println(time.Now())
    32. }
    33. 输出:
    34. sleep: 7
    35. timeout
    36. 2021-08-19 18:27:19.9093326 +0800 CST m=+3.003692901

    四、sync包

  • sync.WaitGroup:计数信号量

  • sync.Mutex:锁 互斥锁

    - 共享数据 读
    - 共享数据 写

  • sync.RWMutex:读写锁
  • sync.Cond:条件锁

    - 多个例程,某个执行检查是否满足条件,不满足等待 Wait
    - 其他例程,当可能产生等待例程条件重新满足,通知等待例程 Signal/Boardcase

  • sync.Pool:线程池,连接池,…

    - 对象池,从池中获取对象,当池中无可用对象,创建并返回
    - 当使用完成会放入池中
    - 示例:

  1. package main
  2. mport (
  3. "fmt"
  4. "sync"
  5. unc main() {
  6. intPool := &sync.Pool{
  7. New: func() interface{} {
  8. fmt.Println("new")
  9. return 1
  10. },
  11. }
  12. v := intPool.Get() // 需要创建 -> New
  13. // v断言某类型,执行
  14. fmt.Println(v)
  15. // 使用完成放入池中
  16. intPool.Put(v) // 1个对象
  17. v1 := intPool.Get()
  18. v2 := intPool.Get() // 需要创建 -> New
  19. fmt.Println(v1, v2)
  20. 出:
  21. ew
  22. ew
  23. 1

五、runtime包

  • runtime.GOMAXPROCS(1):设置使用cpu核数,开发终端对cpu要求有限制 4核 1
  • runtime.GOROOT:获取GOROOT的安装目录
  • runtime.NumCPU:获取CPU的核数
  • runtime.NumGoroutine:获取例程的数量,默认为1个