一、为什么需要channel
1、需求:
现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到map中。最后显示出来。要求使用goroutine完成
1)、分析思路:
使用goroutine 来完成,效率高,但是会出现并发/并行安全问题.
这里就提出了不同goroutine如何通信的问题
2)、代码实现
使用goroutine来完成(看看使用gorotine并发完成会出现什么问题? 然后我们会去解决)
在运行某个程序时,如何知道是否存在资源竞争问题。 方法很简单,在编译该程序时,增加一个参数 -race
即可
3)、示意图
4)、代码实现:
package utils
import (
"sync"
"fmt"
"time"
)
// 需求:现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到map中。
// 最后显示出来。要求使用goroutine完成
// 思路
// 1. 编写一个函数,来计算各个数的阶乘,并放入到 map中.
// 2. 我们启动的协程多个,统计的将结果放入到 map中
// 3. map 应该做出一个全局的.
var (
myMap = make(map[int]int, 10)
)
// cacluFactorial 函数就是计算 n!, 让将这个结果放入到 myMap
func cacluFactorial(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
//这里我们将 res 放入到myMap
myMap[n] = res //concurrent map writes?
}
func FactorialDemo() {
// 我们这里开启多个协程完成这个任务[200个]
for i := 1; i <= 200; i++ {
go cacluFactorial(i)
}
//休眠10秒钟【第二个问题 】
time.Sleep(time.Second * 10)
//这里我们输出结果,变量这个结果
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
}
2、不同goroutine之间如何通讯
- 全局变量的互斥锁
- 使用管道channel来解决
3、使用全局变量加锁同步改进程序
因为没有对全局变量 m 加锁,因此会出现资源争夺问题,代码会出现错误,提示concurrent map writes
解决方案:加入互斥锁
我们的数的阶乘很大,结果会越界,可以将求阶乘改成 sum += uint64(i)
代码改进
package utils
import (
"sync"
"fmt"
"time"
)
// 需求:现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到map中。
// 最后显示出来。要求使用goroutine完成
// 思路
// 1. 编写一个函数,来计算各个数的阶乘,并放入到 map中.
// 2. 我们启动的协程多个,统计的将结果放入到 map中
// 3. map 应该做出一个全局的.
var (
myMap = make(map[int]int, 10)
/*同步锁改进代码*/
//声明一个全局互斥锁
lock sync.Mutex //sync包提供了基本的同步基元,如互斥锁。Mutex是一个互斥锁,
)
// cacluFactorial 函数就是计算 n!, 让将这个结果放入到 myMap
func cacluFactorial(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
/*同步锁改进代码*/
lock.Lock()//加锁
//这里我们将 res 放入到myMap
myMap[n] = res //concurrent map writes?
lock.Unlock()//解锁
}
func FactorialDemo() {
// 我们这里开启多个协程完成这个任务[200个]
for i := 1; i <= 200; i++ {
go cacluFactorial(i)
}
//休眠10秒钟【第二个问题 】
time.Sleep(time.Second * 10)
/*同步锁改进代码*/
lock.Lock()//加锁
//这里我们输出结果,变量这个结果
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
lock.Unlock()//解锁
}
4、为什么需要channel
前面使用全局变量加锁同步来解决goroutine的通讯,但不完美。
主线程在等待所有goroutine全部完成的时间很难确定,我们这里设置10秒,仅仅是估算。
如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有goroutine处于工作状态,这时也会随主线程的退出而销毁。
通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作。
上面种种分析都在呼唤一个新的通讯机制-channel
二、channel的基本介绍
1、channle本质就是一个数据结构-队列
数据是先进先出【FIFO : first in first out】
线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全的
channel有类型的,一个string的channel只能存放string类型数据。
示意图:
2、定义/声明channel
语法
var 变量名 chan 数据类型
举例:
var intChan chan int (intChan用于存放int数据)
var mapChan chan map[int]string (mapChan用于存放map[int]string类型)
var perChan chan Person
var perChan2 chan *Person
…
说明
channel是引用类型
channel必须初始化才能写入数据, 即make后才能使用
管道是有类型的,intChan 只能写入 整数 int
三、快速入门案例
package utils
import (
"fmt"
)
//管道的初始化,写入数据到管道,从管道读取数据及基本的注意事项
func main() {
//演示一下管道的使用
//1. 创建一个可以存放3个int类型的管道
var intChan chan int
intChan = make(chan int, 3)
//2. 看看intChan是什么
fmt.Printf("intChan 的值=%v intChan本身的地址=%p\n", intChan, &intChan)
//3. 向管道写入数据
intChan<- 10
num := 211
intChan<- num
intChan<- 50
// intChan<- 98//注意点, 当我们给管写入数据时,不能超过其容量
//4. 看看管道的长度和cap(容量)
fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan)) // 3, 3
//5. 从管道中读取数据
var num2 int
num2 = <-intChan
fmt.Println("num2=", num2)
fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan)) // 2, 3
//6. 在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报告 deadlock
num3 := <-intChan
num4 := <-intChan
num5 := <-intChan
fmt.Println("num3=", num3, "num4=", num4, "num5=", num5)
}
测试结果:
channel len= 3 cap=3
num2= 10
channel len= 2 cap=3
fatal error: all goroutines are asleep - deadlock!
总结:channel使用的注意事项
channel中只能存放指定的数据类型
channle的数据放满后,就不能再放入了
如果从channel取出数据后,可以继续放入
在没有使用协程的情况下,如果channel数据取完了,再取,就会报dead lock
四、channel的遍历和关闭
1 、channel的关闭
使用内置函数close可以关闭channel, 当channel关闭后,就不能再向channel写数据了,但是仍然可以从该channel读取数据
2、channel的遍历
channel支持for–range的方式进行遍历,请注意两个细节
在遍历时,如果channel没有关闭,则回出现deadlock的错误
在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
3、案列演示
package utils
import (
"fmt"
)
func TranslateDemo() {
intChan := make(chan int, 3)
intChan<- 100
intChan<- 200
close(intChan) // close
//这是不能够再写入数到channel
//intChan<- 300
fmt.Println("okook~")
//当管道关闭后,读取数据是可以的
n1 := <-intChan
fmt.Println("n1=", n1)
//遍历管道
intChan2 := make(chan int, 100)
for i := 0; i < 100; i++ {
intChan2<- i * 2 //放入100个数据到管道
}
//遍历管道不能使用普通的 for 循环
// for i := 0; i < len(intChan2); i++ {
// }
//在遍历时,如果channel没有关闭,则会出现deadlock的错误
//在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
close(intChan2)
for v := range intChan2 {
fmt.Println("v=", v)
}
}
五、 goroutine和channel结合案例
package utils
import (
"fmt"
)
/*goroutine和channel结合
请完成goroutine和channel协同工作的案例,具体要求:
开启一个writeData协程,向管道intChan中写入50个整数.
开启一个readData协程,从管道intChan中读取writeData写入的数据。
注意: writeData和readDate操作的是同一个管道
主线程需要等待writeData和readDate协程都完成工作才能退出【管道】
*/
func writeData(intChan chan int) {
for i := 1; i <= 50; i++ {
//放入数据
intChan<- i
fmt.Println("writeData ", i)
//time.Sleep(time.Second)
}
close(intChan) //关闭
}
//read data
func readData(intChan chan int, exitChan chan bool) {
for {
v, ok := <-intChan
if !ok {
break
}
//time.Sleep(time.Second)
fmt.Printf("readData 读到数据=%v\n", v)
}
//readData 读取完数据后,即任务完成
exitChan<- true
close(exitChan)
}
func Test() {
//创建两个管道
intChan := make(chan int, 50)
exitChan := make(chan bool, 1)
go writeData(intChan)
go readData(intChan, exitChan)
//time.Sleep(time.Second * 10)
for {
_, ok := <-exitChan
if !ok {
break
}
}
}
六、 channel使用细节和注意事项
1、channel可以声明为只读,或者只写性质 【案例演示】
2、channel只读和只写的最佳实践案例
3、使用select可以解决从管道取数据的阻塞问题【案例演示】
package utils
// 使用select可以解决从管道取数据的阻塞问题
import (
"fmt"
"time"
)
func SelectDemo() {
//使用select可以解决从管道取数据的阻塞问题
//1.定义一个管道 10个数据int
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan<- i
}
//2.定义一个管道 5个数据string
stringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
stringChan <- "hello" + fmt.Sprintf("%d", i)
}
//传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock
//问题,在实际开发中,可能我们不好确定什么关闭该管道.
//可以使用select 方式可以解决
//label:
for {
select {
//注意: 这里,如果intChan一直没有关闭,不会一直阻塞而deadlock
//,会自动到下一个case匹配
case v := <-intChan :
fmt.Printf("从intChan读取的数据%d\n", v)
time.Sleep(time.Second)
case v := <-stringChan :
fmt.Printf("从stringChan读取的数据%s\n", v)
time.Sleep(time.Second)
default :
fmt.Printf("都取不到了,不玩了, 程序员可以加入逻辑\n")
time.Sleep(time.Second)
return
//break label
}
}
}
4、goroutine中使用recover,解决协程中出现panic,导致程序崩溃问题.【案例演示】
说明: 如果我们起了一个协程,但是这个协程出现了panic, 如果我们没有捕获这个panic,就会造成整个程序崩溃,这时我们可以在goroutine中使用recover来捕获panic, 进行处理,这样即使这个协程发生的问题,但是主线程仍然不受影响,可以继续执行。
package utils
import (
"fmt"
"time"
)
//函数
func sayHello() {
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
fmt.Println("hello,world")
}
}
//函数
func testRecover() {
//这里我们可以使用defer + recover
defer func() {
//捕获test抛出的panic
if err := recover(); err != nil {
fmt.Println("test() 发生错误", err)
}
}()
//定义了一个map
var myMap map[int]string
myMap[0] = "golang" //error
}
func RecoverDemo() {
go sayHello()
go testRecover()
for i := 0; i < 10; i++ {
fmt.Println("main() ok=", i)
time.Sleep(time.Second)
}
}