假设我们现在有这么一个需求:
计算 1-200 之间各个数的阶乘, 并将每个结果保存在 mao 中, 最终显示出来, 要求使用 goroutime.
分析:
(1) 使用 goroutime 完成, 效率高, 但是会出现并发 / 并行安全问题;
(2) 不同协程之间如何通信;
对于 (1): 不同协程之间可能同时对一块内存进行操作, 导致数据的混乱, 即并发 / 并行不安全; 主协程运行完了, 计算阶乘的协程却没有运行完, 功能并不能够准确实现; 可利用互斥锁解决该问题;
对于 (2): 可以利用利用管道;
正常的代码:
- package main
- import (
- "fmt"
- "sync"
- )
- var (
- myMap = make(map[int]int, 10)
- )
- func cal(n int) {
- res := 1
- for i := 1; i <= n; i++ {
- res *= i
- }
- myMap[n] = res
- }
- func main() {
- for i := 1; i <= 15; i++ {
- go cal(i)
- }
- for i, v := range myMap {
- fmt.Printf("map[%d]=%d\n", i, v)
- }
- }
运行结果:
1. 利用互斥锁
- package main
- import (
- "fmt"
- "sync"
- ""
- )
- var (
- myMap = make(map[int]int, 10)
- //lock 是全局互斥锁, synchornized
- lock sync.Mutex
- )
- func cal(n int) {
- res := 1
- for i := 1; i <= n; i++ {
- res *= i
- }
- lock.Lock()
- myMap[n] = res
- lock.Unlock()
- }
- func main() {
- for i := 1; i <= 15; i++ {
- go cal(i)
- }
- for i, v := range myMap {
- fmt.Printf("map[%d]=%d\n", i, v)
- }
- }
有可能主程序运行完了而 cal 还没运行完 (上面结果只到 13, 没有 14,15), 需要加上 time.Sleep(time.Seconde*3), 而在输出时, 由于主协程并不知道程序已经完成了, 底层仍然可能出现竞争资源, 所以在输出阶段也要加上互斥锁. 最终代码如下:
- package main
- import (
- "fmt"
- "sync"
- )
- var (
- myMap = make(map[int]int, 10)
- //lock 是全局互斥锁, synchornized
- lock sync.Mutex
- )
- func cal(n int) {
- res := 1
- for i := 1; i <= n; i++ {
- res *= i
- }
- lock.Lock()
- myMap[n] = res
- lock.Unlock()
- }
- func main() {
- for i := 1; i <= 15; i++ {
- go cal(i)
- }
- time.Sleep(time.Second * 4)
- lock.Lock()
- for i, v := range myMap {
- fmt.Printf("map[%d]=%d\n", i, v)
- }
- lock.Unlock()
- }
为什么需要管道?
(1) 主线程在等待所有协程全部完成的时间很难确定;
(2) 如果主线程休眠时间长了, 会加长等待时间, 如果等待时间短了, 可能协程还处于工作状态, 这时也会随着主协程的结束而销毁;
(3) 通过全局变量加锁同步来实现通讯, 也并不利于多个协程对全局变量的读写操作;
管道的介绍:
(1) 管道的本质就是一种数据结构 -- 队列;
(2) 数据先进先出;
(3) 线程安全, 多协程访问时, 不需要加锁;
(4) 管道只能存储相同的数据类型;
管道的声明:
- var intChan chan int;
- var stringChan chan string;
- var mapChan chan map[int]string;
- var perChan chan Person;
- var perChan chan *Person;
注意: 管道是引用类型; 管道必须初始化后才能写入数据; 管道是有类型的, 即 IntChan 只能写入 int;
管道初始化:
- var intChan chan int
- intChan = make(chan int,10)
向管道中读写数据:
- num := 10
- intChan<-num
- var num2 int
- num2<-intChan
注意: 管道容量满了则不能继续写入, 在没有使用协程的情况下, 管道空了不能继续读取.
如何使管道中存储任意数据类型?
channel 的关闭:
使用内置的 close 可以关闭管道, 关闭后不能再进行写入, 但是可以进行读取;
channel 的遍历:
channel 可以使用 for range 进行遍历 , 但是要注意:
在遍历时, 如果 channel 没有关闭, 则会出现 deadlock 错误;
在遍历时, 如果 channel 已经关闭, 则会正常遍历数据, 遍历完成后退出;(即在遍历前需要先关闭管道)
2. 利用管道实现边写边读
流程图:
- package main
- import (
- "fmt"
- )
- var (
- myMap = make(map[int]int, 10)
- )
- func cal(n int) map[int]int {
- res := 1
- for i := 1; i <= n; i++ {
- res *= i
- }
- myMap[n] = res
- return myMap
- }
- func write(myChan chan map[int]int) {
- for i := 0; i <= 15; i++ {
- myChan <- cal(i)
- fmt.Println("writer data:", cal(i))
- }
- close(myChan)
- }
- func read(myChan chan map[int]int, exitChan chan bool) {
- for {
- v, ok := <-myChan
- if !ok {
- break
- }
- fmt.Println("read data:", v)
- }
- exitChan <- true
- close(exitChan)
- }
- func main() {
- var myChan chan map[int]int
- myChan = make(chan map[int]int, 20)
- var exitChan chan bool
- exitChan = make(chan bool, 1)
- go write(myChan)
- go read(myChan, exitChan)
- for {
- _, ok := <-exitChan
- if !ok {
- break
- }
- }
- }
结果:
来源: https://www.cnblogs.com/xiximayou/p/11953584.html