3.1 RWMutex
测试代码----读写者问题
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var rwlock sync.RWMutex
var wg sync.WaitGroup
wg.Add(6)
//写的goroutine
go func() {
time.Sleep(3 * time.Second)
defer wg.Done()
rwlock.Lock() //写锁,会方式别的别的写锁获取,和读锁获取
defer rwlock.Unlock()
fmt.Println("get write lock")
time.Sleep(5 * time.Second)
}()
time.Sleep(time.Second)
for i := 0; i < 5; i++ {
go func() {
defer wg.Done()
for {
rwlock.RLock() //加读锁,读锁不会防止别的写锁获取,和读锁获取
time.Sleep(500 * time.Millisecond)
fmt.Println("get read lock")
rwlock.RUnlock()
}
}()
}
wg.Wait()
}
问题:.比如下面的代码里,为什么每个goroutine都要先defer wg.Done()?为什么不直接wg.Done()?
答:无论函数中的代码如何执行,defer 后面的函数都会被执行。这对于清理资源(如关闭文件、解锁互斥锁等)非常有用。
3.2 通过channel进行goroutine之间的通信
使用channel
package main
import "fmt"
func main() {
/*
不要通过共享内存来通信,而要通过通信来实现内存共享
php,python,java等多线程编程的时候,两个goroutine之间
通信最常用的方式是---声明一个全局变量
也会提供消息队列的机制,python-queue 即消费者和生产者之间的关系
*/
/*
而go提供了channel 再加上语法糖让使用channel更加简单
*/
var msg chan string //默认值是nil
msg = make(chan string, 0) //channel的值为0的话,如果放值进去,会阻塞
msg <- "tc " //放到channel中
data := <-msg //从channel中取出,放到data中
fmt.Println(data)
}
此时,虽然是想使用无缓冲channel(msg = make(chan string, 0)
),但是这样的使用方法不对,会阻塞
需要在通过goroutine包装data :=<-msg
和fmt.Println(data)
操作
即,创建一个协程等在这输出,这时当msg <- "tc "
向channel输入数据的时候,无缓冲的直接读出数据就没问题。
完整代码:
package main
import (
"fmt"
"time"
)
func main() {
/*
不要通过共享内存来通信,而要通过通信来实现内存共享
php,python,java等多线程编程的时候,两个goroutine之间
通信最常用的方式是---声明一个全局变量
也会提供消息队列的机制,python-queue 即消费者和生产者之间的关系
*/
/*
而go提供了channel 再加上语法糖让使用channel更加简单
*/
var msg chan string //默认值是nil
msg = make(chan string, 0) //channel的值为0的话,如果放值进去,会阻塞
go func(msg chan string) {
data := <-msg //从channel中取出,放到data中
fmt.Println(data)
}(msg)
msg <- "tc " //放到channel中
time.Sleep(time.Second * 10)
}
/*
tc
*/
tips:无缓冲channel容易出现deadlock
waitgroup.Done()的缺失也容易出现deadlock
有缓冲channel和无缓存channel
无缓冲channel适用于通知,B要第一时间知道A是否已经完成
有缓冲channel适用于消费者-生产者之间的通信
3.3 forrange对channel进行遍历
先看例子:
package main
import (
"fmt"
"time"
)
func main() {
var msg chan int //默认值是nil
msg = make(chan int, 2) //channel的值为0的话,如果放值进去,会阻塞
go func(msg chan int) {
data := <-msg //从channel中取出,放到data中
fmt.Println(data)
}(msg)
msg <- 1 //放到channel中
msg <- 2 //放到channel中
time.Sleep(time.Second * 10)
}
/*1
进程 已完成,退出代码为 0
*/
只能打印出1,因为只有一个打印操作
使用forrange可以遍历输出
package main
import (
"fmt"
"time"
)
func main() {
var msg chan int //默认值是nil
msg = make(chan int, 2) //channel的值为0的话,如果放值进去,会阻塞
go func(msg chan int) {
for data := range msg {
fmt.Println(data)
}
fmt.Println("all Done")
}(msg)
msg <- 1 //放到channel中
msg <- 2 //放到channel中
msg <- 3 //放到channel中
msg <- 4 //放到channel中
msg <- 1 //放到channel中
//close(msg)
time.Sleep(time.Second * 3)
}
/*
1
2
3
4
1
进程 已完成,退出代码为 0
*/
如果不使用代码中的close(msg)的话,创建的goroutine会一直等待看有没有信息输入到channel中,从而不会执行fmt.Println("all Done")
语句,直到time.Sleep(time.Second * 3)
—3秒后程序运行结束,子goroutine也随之结束,所以答案里没有"all Done"
当使用close(msg),就会把msg这个channel关闭,从而答案变成
1
2
3
4
1
all Done
进程 已完成,退出代码为 0
关闭channel之后,不能再放值了,会报错(但是可以取值)
package main
import (
"fmt"
"time"
)
func main() {
var msg chan int //默认值是nil
msg = make(chan int, 2) //channel的值为0的话,如果放值进去,会阻塞
go func(msg chan int) {
for i := 0; i < 3; i++ {
data := <-msg
fmt.Println(data)
}
fmt.Println("all Done")
}(msg)
msg <- 1 //放到channel中
msg <- 2 //放到channel中
msg <- 3 //放到channel中
msg <- 4 //放到channel中
msg <- 1 //放到channel中
close(msg)
time.Sleep(time.Second)
data := <-msg
fmt.Println(data)
time.Sleep(time.Second * 3)
}
/*
1
2
3
all Done
4
进程 已完成,退出代码为 0
*/
3.4 单向channel的应用场景
默认情况下channel是双向的
但是我们经常用一个channel作为参数进行单向传递
一个生产者-消费者的例子:
package main
import (
"fmt"
"time"
)
func producer(out chan<- int) {
for i := 0; i < 10; i++ {
out <- i * i
}
close(out)
}
func consumer(in <-chan int) {
for v := range in {
fmt.Println(v)
}
}
func main() {
//默认情况下channel是双向的
//但是我们经常用一个channel作为参数进行单向传递
//var ch1 chan int //正常双向channel
//var ch2 chan<- float64 //单向channel 只能写入float64的数据
//var ch3 <-chan int //单向channel,只能读取
//初始化
//c := make(chan int, 3) // 创建一个双向的channel,接下来将其变成单向channel
//var send chan<- int = c // send-only
//var read <-chan int = c // read-only
//
//send <- 1
//data := <-read
//fmt.Println(data)
c := make(chan int) //
go producer(c)
go consumer(c)
time.Sleep(time.Second * 3)
}
3.5 通过channel实现交叉打印
package main
import (
"fmt"
"time"
)
var number, letter = make(chan bool), make(chan bool)
func printNum() {
i := 1
for {
<-number
//怎么交叉打印?--等待另一个goroutine(无缓冲)通知
fmt.Printf("%d%d", i, i+1)
i += 2
letter <- true
}
}
func printLetter() {
i := 0
str := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
for {
<-letter
if i >= len(str) {
return
}
//怎么交叉打印?--等待另一个goroutine(无缓冲)通知
fmt.Print(str[i : i+2])
i += 2
number <- true
}
}
func main() {
/*
使用两个goroutine交替打印,一个打印数字,一个打印字母,最终结果如下:
12AB34CD56EF78GH910IJ1112KL1314MN1516OP1718QR1920ST2122UV2324WX2526YZ2728
*/
go printNum()
go printLetter()
number <- true
time.Sleep(time.Second * 3)
}
3.6监控goroutine的执行
原方法
package main
import (
"fmt"
"sync"
"time"
)
var done bool
var lock sync.Mutex
func g1() {
//
time.Sleep(time.Second)
lock.Lock()
defer lock.Unlock()
done = true
}
func g2() {
time.Sleep(2 * time.Second)
lock.Lock()
defer lock.Unlock()
done = true
}
func main() {
//select 类似于switch case语句,但是select功能和Linux里面提供的io的select,poll,epoll相似
//select主要作用于多个channel
//
//现在有个需求,现在有两个goroutine都在执行,我现在需要知道我在主的goroutine,
//当某一个执行完成之后,我会立马知道
go g1()
go g2()
for {
if done {
fmt.Println("done")
time.Sleep(10 * time.Millisecond)
return
}
}
}
我们应该使用channel来进行goroutine之间的通信
使用channel的阻塞的方法
package main
import (
"fmt"
"time"
)
var done = make(chan struct{}) //channel是多线程安全的,多个线程对channel读写也是安全的
func g1() {
//
time.Sleep(time.Second)
done <- struct{}{}
}
func g2() {
time.Sleep(2 * time.Second)
done <- struct{}{}
}
func main() {
//select 类似于switch case语句,但是select功能和Linux里面提供的io的select,poll,epoll相似
//select主要作用于多个channel
//
//现在有个需求,现在有两个goroutine都在执行,我现在需要知道我在主的goroutine,
//当某一个执行完成之后,我会立马知道
go g1()
go g2()
<-done
fmt.Println("done")
}
这样的话就出现了新问题
如果有两个及以上的channel个数的话,哦那个这种直接阻塞的方法就会互相卡住。
所以使用select语句
package main
import (
"fmt"
"time"
)
func g1(ch chan struct{}) {
//
time.Sleep(2 * time.Second)
ch <- struct{}{}
}
func g2(ch chan struct{}) {
time.Sleep(time.Second)
ch <- struct{}{}
}
func main() {
//select 类似于switch case语句,但是select功能和Linux里面提供的io的select,poll,epoll相似
//select主要作用于多个channel
//
//现在有个需求,现在有两个goroutine都在执行,我现在需要知道我在主的goroutine,
//当某一个执行完成之后,我会立马知道
g1channel := make(chan struct{})
g2channel := make(chan struct{})
go g1(g1channel)
go g2(g2channel)
//某一个分支就绪了,就执行该分支
//如果两个都就绪了,那就是随机执行,目的是,防止饥饿!!!
select {
case <-g1channel:
fmt.Println("g1 done")
case <-g2channel:
fmt.Println("g2 done")
}
fmt.Println("done")
}
某一个分支就绪了,就执行该分支
如果两个都就绪了,那就是随机执行,目的是,防止饥饿!!!
问题:那么如果两个goroutine都阻塞住了,那主程序不就是也阻塞住了吗?
使用,引入超时机制,具体是使用select里的default
select {
case <-g1channel:
fmt.Println("g1 done")
case <-g2channel:
fmt.Println("g2 done")
default:
fmt.Println("default")
}
但是这样就直接default,来不及等待g1和g2完成,没有给他们时间
所以在外面使用ttime.NewTimer(time.Second)创建一个新的 channel,会在1s之后生成
package main
import (
"fmt"
"time"
)
func g1(ch chan struct{}) {
//
time.Sleep(2 * time.Second)
ch <- struct{}{}
}
func g2(ch chan struct{}) {
time.Sleep(time.Second)
ch <- struct{}{}
}
func main() {
//select 类似于switch case语句,但是select功能和Linux里面提供的io的select,poll,epoll相似
//select主要作用于多个channel
//
//现在有个需求,现在有两个goroutine都在执行,我现在需要知道我在主的goroutine,
//当某一个执行完成之后,我会立马知道
g1channel := make(chan struct{})
g2channel := make(chan struct{})
go g1(g1channel)
go g2(g2channel)
//某一个分支就绪了,就执行该分支
//如果两个都就绪了,那就是随机执行,目的是,防止饥饿!!!
timer := time.NewTimer(time.Second)
for {
select {
case <-g1channel:
fmt.Println("g1 done")
case <-g2channel:
fmt.Println("g2 done")
case <-timer.C:
fmt.Println("timeout")
}
}
}
3.7 通过context解决goroutine的信息传递
使用全局变量的方法
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func cpuIInfo() {
for {
time.Sleep(2 * time.Second)
fmt.Println("get cpu")
}
}
func main() {
//场景:有一个goroutine 监控cpu的信息
wg.Add(1)
go cpuIInfo()
wg.Wait()
fmt.Println("done")
}
现在有新的需求-----我可以主动退出监控程序
比较容易想到的方法,使用一个共享变量,for循环中不断查询是否要停止
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
var stop bool
func cpuIInfo() {
defer wg.Done()
for {
if stop {
break
}
time.Sleep(2 * time.Second)
fmt.Println("get cpu")
}
}
func main() {
//场景:有一个goroutine 监控cpu的信息
wg.Add(1)
go cpuIInfo()
time.Sleep(8 * time.Second)
stop = true
wg.Wait()
fmt.Println("done")
}
当然,由于主程序和子goroutine都对stop进行了读写,最好加一个锁(lock),这里就不演示了。
但是我们说过尽量不要使用共享变量的方式。
接下来演示消息(管道)方式
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func cpuIInfo(stop chan struct{}) {
defer wg.Done()
for {
select {
case <-stop:
fmt.Println("退出cpu监控")
return
default:
time.Sleep(2 * time.Second)
fmt.Println("get cpu")
}
}
}
func main() {
var stop = make(chan struct{})
//场景:有一个goroutine 监控cpu的信息
wg.Add(1)
go cpuIInfo(stop)
time.Sleep(8 * time.Second)
stop <- struct{}{}
wg.Wait()
fmt.Println("done")
}
接下来介绍一种更优雅的方式----context
package main
import (
"context"
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func cpuIInfo(ctx context.Context) { //context 是一个Interface
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Println("退出cpu监控")
return
default:
time.Sleep(2 * time.Second)
fmt.Println("get cpu")
}
}
}
func main() {
//场景:有一个goroutine 监控cpu的信息
wg.Add(1)
//有两个返回值,
ctx1, cancel1 := context.WithCancel(context.Background()) //Backgroud一般作为父的context
//context提供了三种函数,WithCancel,withTimeout,WithValue
//如果你的goroutine,函数中希望被控制,但是不希望影响原来的接口信息的时候
//函数参数中,第一个参数尽量选择context
ctx2, _ := context.WithCancel(ctx1) // 子cancel方法
go cpuIInfo(ctx2)
time.Sleep(8 * time.Second)
cancel1() //调用父亲的cancel,也会调用子context的cancel,也可以起到控制子cancel的作用
wg.Wait()
fmt.Println("done")
}
3.8 WithValue,WithTimeout的应用场景
WithTimeout方式:
使用ctx, _ := context.WithTimeout(context.Background(), 8*time.Second)
可以创建一个context,并且在6秒后自动调用cancel,可以达到之前
time.Sleep(6 * time.Second)
cancel()
相同的效果
WithValue方式:
简单来说就是可以传值
package main
import (
"context"
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func cpuIInfo(ctx context.Context) { //context 是一个Interface
fmt.Printf("tracid: %s\r\n", ctx.Value("tracid"))
//记录日志,这一次请求是哪个tracid打印的
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Println("退出cpu监控")
return
default:
time.Sleep(2 * time.Second)
fmt.Println("get cpu")
}
}
}
func main() {
//场景:有一个goroutine 监控cpu的信息
wg.Add(1)
//有两个返回值,
//ctx1, cancel1 := context.WithCancel(context.Background()) //Backgroud一般作为父的context
//context提供了三种函数,WithCancel,withTimeout,WithValue
//如果你的goroutine,函数中希望被控制,但是不希望影响原来的接口信息的时候
//函数参数中,第一个参数尽量选择context
//ctx2, _ := context.WithCancel(ctx1) // 子cancel方法
ctx, _ := context.WithTimeout(context.Background(), 6*time.Second)
value := context.WithValue(ctx, "tracid", "gjw12j")
go cpuIInfo(value)
wg.Wait()
fmt.Println("done")
}
结束!