🌈 个人主页:Zfox_
🔥 系列专栏:Go

一:🔥 协程

Goroutine 是 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

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

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 来了

基本定义

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,在协程函数里面写,在主线程里面接收数据

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)
}

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

  for money := range moneyChan {
    moneyList = append(moneyList, money)
  }

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

🦋 select

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

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

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)
}

🦋 协程超时处理

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才对

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 的调度方法为抢占式执行,随机调度

🦋 同步锁

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

答案是可以的

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 就会引发一个错误

concurrent map read and map write

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

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

加锁

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

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 的理解, 觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~ 😉
在这里插入图片描述

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐