【Go】 协程和 channel

🌈 个人主页:Zfox_

🔥 系列专栏:Go

目录

  • [一:🔥 协程](#一:🔥 协程)
    • [🦋 WaitGroup](#🦋 WaitGroup)
  • [二:🔥 channel](#二:🔥 channel)
    • [🦋 select](#🦋 select)
    • [🦋 协程超时处理](#🦋 协程超时处理)
  • [三:🔥 线程安全](#三:🔥 线程安全)
    • [🦋 同步锁](#🦋 同步锁)
    • [🦋 线程安全下的 map](#🦋 线程安全下的 map)
  • [四:🔥 共勉](#四:🔥 共勉)

一:🔥 协程

Goroutine 是 Go 运行时管理的轻量级线程

在 go 中,开启一个协程是非常简单的

go 复制代码
package main

import (
  "fmt"
  "time"
)

func sing() {
  fmt.Println("唱歌")
  time.Sleep(1 * time.Second)
  fmt.Println("唱歌结束")
}

func main() {
  go sing()
  go sing()
  go sing()
  go sing()
  time.Sleep(2 * time.Second)
}

如果我把这个主线程中的延时去掉之后,你会发现程序没有任何输出就结束了

这是为什么呢

那是因为主线程结束协程自动结束,主线程不会等待协程的结束

🦋 WaitGroup

我们只需要让主线程等待协程就可以了,它的用法是这样的

go 复制代码
package main

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

var (
  wait = sync.WaitGroup{}
)

func sing() {
  fmt.Println("唱歌")
  time.Sleep(1 * time.Second)
  fmt.Println("唱歌结束")
  wait.Done()
}

func main() {
  wait.Add(4)
  go sing()
  go sing()
  go sing()
  go sing()
  wait.Wait()
  fmt.Println("主线程结束")
}

二:🔥 channel

有没有想过一个问题,我在协程里面产生了数据,咋传递给主线程呢?

或者是怎么传递给其他协程函数呢?

这个时候 channel 来了

基本定义

go 复制代码
package main

import "fmt"

func main() {
  var c chan int // 声明一个传递整形的通道
  // 初始化通道
  c = make(chan int, 1) //  初始化一个 有一个缓冲位的通道
  c <- 1
  //c <- 2 // 会报错 deadlock
  fmt.Println(<-c) // 取值
  //fmt.Println(<-c) // 再取也会报错  deadlock

  c <- 2
  n, ok := <-c
  fmt.Println(n, ok)
  close(c) // 关闭协程
  c <- 3   // 关闭之后就不能再写或读了  send on closed channel
  fmt.Println(c)
}

当然,在同步模式下,channel 没有任何意义

需要在异步模式下使用 channel,在协程函数里面写,在主线程里面接收数据

go 复制代码
package main

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

var moneyChan = make(chan int) // 声明并初始化一个长度为0的信道

func pay(name string, money int, wait *sync.WaitGroup) {
  fmt.Printf("%s 开始购物\n", name)
  time.Sleep(1 * time.Second)
  fmt.Printf("%s 购物结束\n", name)

  moneyChan <- money

  wait.Done()
}

// 协程
func main() {
  var wait sync.WaitGroup
  startTime := time.Now()
  // 现在的模式,就是购物接力
  //shopping("张三")
  //shopping("王五")
  //shopping("李四")
  wait.Add(3)
  // 主线程结束,协程函数跟着结束
  go pay("张三", 2, &wait)
  go pay("王五", 3, &wait)
  go pay("李四", 5, &wait)

  go func() {
    defer close(moneyChan)
    // 在协程函数里面等待上面三个协程函数结束
    wait.Wait()
  }()

  for {
    money, ok := <-moneyChan
    fmt.Println(money, ok)
    if !ok {
      break
    }
  }

  //time.Sleep(2 * time.Second)

  fmt.Println("购买完成", time.Since(startTime))
  fmt.Println("moneyList", moneyList)
}

如果这样接收数据不太优雅,那还有更优雅的写法

go 复制代码
  for money := range moneyChan {
    moneyList = append(moneyList, money)
  }

如果通道被 close,for 循环会自己结束,十分优雅

🦋 select

如果一个协程函数,往多个 channel 里面写东西,在主线程里面怎么拿数据呢?

go 为我们提供了 select,用于异步的从多个 channel 里面去取数据

go 复制代码
package main

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

// 信道 存 int 类型
var moneyChan1 = make(chan int) // 声明并初始化一个长度为0的信道
var nameChan1 = make(chan string)
var doneChan = make(chan struct{})

func send(name string, money int, wait *sync.WaitGroup) {
	fmt.Printf("%s 开始购物\n", name)
	time.Sleep(1 * time.Second)
	fmt.Printf("%s 购物结束\n", name)

	moneyChan1 <- money // 信道赋值语句
	nameChan1 <- name

	wait.Done()
}

func main() {
	var wait sync.WaitGroup

	startTime := time.Now()
	wait.Add(3)
	// 协程
	go send("zhangsan", 2, &wait)
	go send("lisi", 3, &wait)
	go send("wangwu", 5, &wait)

	// 再开一个协程函数判断是否结束
	go func() {
		defer close(moneyChan1)
		defer close(nameChan1)
		defer close(doneChan)
		wait.Wait()
		// 再创建一个信道用于关闭
		// close(moneyChan)
	}()

	// 等价于下面的写法
	var moneyList []int
	var nameList []string

	// 多个 channel 的写法
	var event = func() {
		for {
			select {
			case money := <-moneyChan1:
				moneyList = append(moneyList, money)
			case name := <-nameChan1:
				nameList = append(nameList, name)
			case <-doneChan:
				return
			}
		}
	}
	event()

	fmt.Println("购买完成", time.Since(startTime))
	fmt.Println("moneyList", moneyList)
	fmt.Println("nameList", nameList)
}

🦋 协程超时处理

go 复制代码
package main

import (
  "fmt"
  "time"
)

var done = make(chan struct{})

func event() {
  fmt.Println("event执行开始")
  time.Sleep(2 * time.Second)
  fmt.Println("event执行结束")
  close(done)
}

func main() {
  go event()

  select {
  case <-done:
    fmt.Println("协程执行完毕")
  case <-time.After(1 * time.Second):
    fmt.Println("超时")
    return
  }

}

三:🔥 线程安全

什么是线程安全?

现在有两个协程,同时触发,一个协程对一个全局变量进行 100 完成 ++ 操作,另一个对全局变量---的操作

那么,两个协程结束,最后的值应该是0才对

go 复制代码
package main

import (
  "fmt"
  "sync"
)

var num int
var wait sync.WaitGroup

func add() {
  for i := 0; i < 1000000; i++ {
    num++
  }
  wait.Done()
}
func reduce() {
  for i := 0; i < 1000000; i++ {
    num--
  }
  wait.Done()
}

func main() {
  wait.Add(2)
  go add()
  go reduce()
  wait.Wait()
  fmt.Println(num)

}

但是你会发现,这个输出的结果完全无法预测

这是为什么呢?

根本原因是 CPU 的调度方法为抢占式执行,随机调度

🦋 同步锁

那么我们能不能通过给操作加锁来解决这个问题呢

答案是可以的

go 复制代码
package main

import (
  "fmt"
  "sync"
)

var num int
var wait  sync.WaitGroup
var lock  sync.Mutex

func add() {
  // 谁先抢到了这把锁,谁就把它锁上,一旦锁上,其他的线程就只能等着
  lock.Lock()
  for i := 0; i < 1000000; i++ {
    num++
  }
  lock.Unlock()
  wait.Done()
}
func reduce() {
  lock.Lock()
  for i := 0; i < 1000000; i++ {
    num--
  }
  lock.Unlock()
  wait.Done()
}

func main() {
  wait.Add(2)
  go add()
  go reduce()
  wait.Wait()
  fmt.Println(num)

}

🦋 线程安全下的 map

如果我们在一个协程函数下,读写 map 就会引发一个错误

go 复制代码
concurrent map read and map write

希望大家见到这个错误,就能知道,这个就是 map 的线程安全错误

go 复制代码
package main

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

var wait sync.WaitGroup
var mp = map[string]string{}

func reader() {
  for {
    fmt.Println(mp["time"])
  }
  wait.Done()
}
func writer() {
  for {
    mp["time"] = time.Now().Format("15:04:05")
  }
  wait.Done()
}

func main() {
  wait.Add(2)
  go reader()
  go writer()
  wait.Wait()

}

我们不能在并发模式下读写 map

如果要这样做

  1. 给读写操作加锁
  2. 使用sync.Map

加锁

go 复制代码
package main

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

var wait sync.WaitGroup
var mp = map[string]string{}
var lock sync.Mutex

func reader() {
  for {
    lock.Lock()
    fmt.Println(mp["time"])
    lock.Unlock()
  }
  wait.Done()
}
func writer() {
  for {
    lock.Lock()
    mp["time"] = time.Now().Format("15:04:05")
    lock.Unlock()
  }
  wait.Done()
}

func main() {
  wait.Add(2)
  go reader()
  go writer()
  wait.Wait()
}

sync.Map

go 复制代码
package main

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

var wait sync.WaitGroup
var mp = sync.Map{}

func reader() {
  for {

    fmt.Println(mp.Load("time"))
  }
  wait.Done()
}
func writer() {
  for {
    mp.Store("time", time.Now().Format("15:04:05"))
  }
  wait.Done()
}

func main() {
  wait.Add(2)
  go reader()
  go writer()
  wait.Wait()

}

其实看它源码,它的内部也是用了同步锁的

四:🔥 共勉

😋 以上就是我对 【Go】协程和 channel 的理解, 觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~ 😉

相关推荐
fliter2 分钟前
在 Async Rust 中实现请求合并(Request Coalescing)
后端
王立志_LEO2 分钟前
Gunicorn 启动django服务
后端
fliter3 分钟前
一个让我调试一周的 Rust match 陷阱
后端
高林雨露6 分钟前
kotlin 相关code
开发语言·kotlin
我还记得那天9 分钟前
函数的递归调用
c语言·开发语言·visualstudio
zhangfeng113310 分钟前
ThinkPHP5 事件系统的标准最佳实践 事件系统的完整设计逻辑tags.php tags.php(事件地图)
android·开发语言·php
xyq202413 分钟前
HTML 标签简写及全称
开发语言
tongluowan00714 分钟前
数据结构 Bitmap(位图)示例 - 用户签到系统
开发语言·数据结构·bitmap·用户签到系统
就叫_这个吧14 分钟前
Java线程池应用的四种方式+线程池底层实现原理
java·开发语言
一只大袋鼠14 分钟前
SpringBoot 初学阶段知识点汇总(一)
spring boot·笔记·后端