Channel讲解
上节回顾
1、程序、进程、线程、协程
2、go
3、runtime
4、临界资源安全问题(单线程下不会出现的问题,在多线程下会出现)
5、锁 sync
-
mutex
-
waitgroup
Channel
1、Go语言不建议我们使用锁机制来解决多线程问题、建议我们使用通道。
2、通信的角色,必须在2个以上。1个人,不能叫做通信。
3、chan ,必须要作用在两个及两个以上的 goroutine .
4、一个goroutine需要将一些信息告诉另外一个goroutine ,就直接将数据信息放入chan即可。
通道:可以被认为是 Gr 通信管道。
类似于水管,数据可以从一端流到另一端。
“不要通过共享内存来通信,而应该通过通信来共享内存(chan)” 这是一句风靡golang社区的经典语
// chan 类型 ,通道 var a chan int a = make(chan int) // 使用规则(存 chan<-、取 <-chan) a <- 1 data := <- a
package main import ( "fmt" "time" ) // 定义通道 chan // 这个 goroutine 希望告诉 main 线程,我还没结束。(通信) func main() { // 定一个bool的通道 var ch chan bool ch = make(chan bool) // 在一个goroutine中去往通道中放入数据 go func() { for i := 0; i < 10; i++ { fmt.Println("goroutine-", i) } time.Sleep(time.Second * 3) ch <- true }() // 另一个goroutine可以从通道中取出数据。(线程之间的通信) // 阻塞等待ch拿到值。有另外一个goroutine往里放值。 data := <-ch fmt.Println("ch data:", data) }
一个通道发送和接收数据,默认是阻塞的。
当一个数据被发送到通道时,在发送语句中被阻塞,直到另一个Goroutine从该通道读取数据。
相对地,当从通道读取数据时,读取被阻塞,直到一个Goroutine将数据写入该通道。
本身channel就是同步的, 意味着同一时间,只能有一条goroutine来操作。
最后:通道是goroutine之间的连接,所有通道的发送和接收必须处在不同的goroutine中。
这些通道的特性是帮助Goroutines有效地进行通信,而无需像使用其他编程语言中非常常见的显式锁或条件变量。
死锁
如果创建了chan,没有 Goroutine 来使用了,则会出现死锁。
使用通道时要考虑的一一个重要因素是死锁。如果Goroutine在一 个通道 上发送数据,那么预计其他的Goroutine应该接收数据。如果这种情况不发生,那么程序将在运行时出现死锁。
类似地,如果Goroutine 正在等待从通道接收数据,那么另一些Goroutine将会在该通道上写入数据,否则程序将会死锁。
package main import ( "fmt" ) // 定义通道 chan // 这个 goroutine 希望告诉 main 线程,我还没结束。(通信) func main() { // 定一个bool的通道 var ch chan bool ch = make(chan bool) 在一个goroutine中去往通道中放入数据 go func() { for i := 0; i < 10; i++ { fmt.Println("goroutine-", i) } //time.Sleep(time.Second * 3) ch <- true }() // 定义好通道之后,如果没有 goroutine来使用(必须在两个及以上goroutine),那么就会产生死锁 // deadlock! data := <-ch fmt.Println("ch data:", data) // 死锁的产生,没有goroutine来消耗通道(存取) ch2 := make(chan int) ch2 <- 10 }
死锁的其他情况:Go语言之死锁的4种常见情况_go 死锁-CSDN博客
1、单线的使用,没有其他的goroutine消费
2、两个chan,互相需要对方的数据,但是由于判断,拿不到对方的数据。
3、sync 锁产生的死锁。
关闭通道
package main import ( "fmt" "time" ) // 关闭通道 // 告诉接收方,我不会再有其他数据发送到chan了。 func main() { // 在main线程中定义的通道 ch1 := make(chan int) go test7(ch1) // 读取chan中的数据 for { time.Sleep(time.Second) // ok 判断chan的状态是否是关闭,如果是关闭,不会再取值了。 // ok, 如果是true,就代表我们还在读数据 // ok, 如果是fasle,就说明该通道已关闭 data, ok := <-ch1 if !ok { fmt.Println("读取完毕", ok) break } fmt.Println("ch1 data:", data) } } // 通道可以参数传递 func test7(ch chan int) { for i := 0; i < 10; i++ { ch <- i } // 关闭通道,告诉接收方,不会在往ch中放入数据 close(ch) }
通过ok来判断是否读取完毕数据。
for range 可以简化开发
package main import ( "fmt" "time" ) // 关闭通道 // 告诉接收方,我不会再有其他数据发送到chan了。 func main() { // 在main线程中定义的通道 ch1 := make(chan int) go test7(ch1) // 读取chan中的数据, for 一个个取,并且会自动判断chan是否close 迭代器 for data := range ch1 { time.Sleep(time.Second) fmt.Println(data) } fmt.Println("end") } // 通道可以参数传递 func test7(ch chan int) { for i := 0; i < 10; i++ { ch <- i } // 关闭通道,告诉接收方,不会在往ch中放入数据 close(ch) }
缓冲通道(chan)
非缓冲通道
chan , 只能存放一个数据,发送和接受都是阻塞的。一次发送对应一个接收。
缓冲通道
通道带了一个缓冲区,发送的数据直到缓冲区填满为止,才会被阻塞,接收的也是,只有缓冲区清空,才会阻塞。
chan如果只有一个容量,老是阻塞,效率是很低的。
package main import ( "fmt" "strconv" "time" ) // 缓冲通道 chan,cap func main() { // 非缓冲通道 ch1 := make(chan int) fmt.Println(cap(ch1), len(ch1)) // 0 0 //ch1 <- 100 // 缓冲通道 // 缓冲区通道,放入数据,不会产生死锁,它不需要等待另外的线程来拿,它可以放多个数据。 // 如果缓冲区满了,还没有人取,也会产生死锁。 ch2 := make(chan string, 5) fmt.Println(cap(ch2), len(ch2)) // 5 0 ch2 <- "1" fmt.Println(cap(ch2), len(ch2)) // 5 1 , 可以通过len来判断缓冲通道中的数据数量 ch2 <- "2" ch2 <- "3" fmt.Println(cap(ch2), len(ch2)) // 5 3 ch2 <- "4" ch2 <- "5" fmt.Println(cap(ch2), len(ch2)) // 5 5 data := <-ch2 ch2 <- "6" // deadlock! fmt.Println(data) ch3 := make(chan string, 4) go test8(ch3) fmt.Println("--------------------------") for s := range ch3 { time.Sleep(time.Second) fmt.Println("main中读取的数据:", s) } fmt.Println("main-end") } func test8(ch chan string) { for i := 0; i < 10; i++ { ch <- "test - " + strconv.Itoa(i) fmt.Println("子goroutine放入数据:", "test - "+strconv.Itoa(i)) } close(ch) }
缓冲通道,可以定义缓冲区的数量
如果缓冲区没有满,可以继续存放,如果满了,也会阻塞等待
如果缓冲区空的,读取也会等待,如果缓冲区中有多个数据,依次按照先进先出的规则进行读取。
如果缓冲区满了,同时有两个线程在读或者写,这个时候和普通的chan一样。一进一出。
定向通道
双向通道
channel 是用来实现 goroutine 通信的。一个写、一个读、这是双向通道。
ch <- data data := <- ch
单向通道 只能读或者只能写
package main import ( "fmt" "time" ) // 单向通道使用场景 func main() { ch1 := make(chan int) // 可读可写 go writeOnly(ch1) go readOnly(ch1) time.Sleep(time.Second * 3) } // 作为函数的参数或者返回值之类的。 // 指定函数去写,不让他读取,防止通道滥用 func writeOnly(ch chan<- int) { // 函数的内部,处理一些写数据的操作 ch <- 100 } // 指定函数去读,不让他写,防止通道滥用 func readOnly(ch <-chan int) int { // 取出通道的值,做一些操作,不可写的。 data := <-ch fmt.Println(data) return data }
Select
select 只能在通道中使用。
package main import ( "fmt" "time" ) // select func main() { ch1 := make(chan int) ch2 := make(chan int) go func() { time.Sleep(time.Second * 2) ch1 <- 100 }() go func() { time.Sleep(time.Second * 2) ch2 <- 200 }() // 读取chan数据,无论谁先放入,我们就用谁,抛弃其他的. // select 和 swtich 只是在通道中使用,case表达式需要是一个通道结果 select { case num1 := <-ch1: fmt.Println(num1) case num2 := <-ch2: fmt.Println(num2) //default: // fmt.Println("default") } }
1、每一个case必须是一个通道的操作 <-
2、所有chan操作都有要结果(通道表达式都必须会被求值)
3、如果任意的通道拿到了结果。它就会立即执行该case、其他就会被忽略
4、如果有多个case都可以运行,select是随机选取一个执行,其他的就不会执行。
5、如果存在default,执行该语句,如果不存在,阻塞等待 select 直到某个通道可以运行。
Timer 定时器
通道的应用场景一。
可以控制程序在某个事件做某件事情。
package main import ( "fmt" "time" ) // 定时器(time : 当下,xxx之前before,xxxx之后 after) func main() { // 创建一个定时器 NewTimer // Timer C <-chan Time //timer := time.NewTimer(time.Second * 3) // 当前时间 //fmt.Println(time.Now()) // timer.C, 时间通道,这个通道中存放的值 2023-03-02 21:49:48.0453619 +0800 CST m=+3.013284501 // 就是我们在定义定时器的时候,存放的时间,等待对应的时间。 //timeChan := timer.C //fmt.Println(<-timeChan) // 定时器(提前关闭) // 会向Timer.C 放入一个时间。 timer2 := time.NewTimer(time.Second * 5) go func() { <-timer2.C // 消费 fmt.Println("end") }() timer2.Stop() // 手动停止定时器。 }
1、定时发邮件
2、定时保存数据库文件 sql
After Before
package main import ( "fmt" "time" ) func main() { // chan 放入当前时间之后的某个时间 //timerChan := time.After(time.Second * 3) //fmt.Println(time.Now()) //chanTime := <-timerChan //fmt.Println(chanTime) // 在3s以后执行这个函数 time.AfterFunc(time.Second*3, mail) time.Sleep(5 * time.Second) } func mail() { // 发邮件 fmt.Println("发邮件") }
作业:
1、通过chan来解决售票问题
2、了解chan的一些其他应用在Go的源码中
3、gouroutine的使用,必须要熟练,掌握死锁的情况。
4、了解GPM模型(Golang关于gouroutine的一个底层模型)