go并发编程(中)
目录
一、并发安全性
1.1 变量并发安全性
1.2 容器并发安全性
二、多路复用
三、协程常见的面试题
3.1交替打印奇数偶数
一、并发安全性
1.1 变量并发安全性
这个和C++中并发安全是一样的,主要是多个线程对临界资源的同时访问,最经典的就是 n++操作,因为这一步并不是原子操作的。
这里我们可以用到 atomic(原子操作,让n++变成一步),也可也使用加锁的办法
我们先来模拟一下错误的操作:这里我们开1000个协程,都执行 n++的操作
package main
import (
"fmt"
"sync"
)
var n int //n
func main() {
wg := sync.WaitGroup{}
wg.Add(1000)
for i := 0; i < 1000; i++ {
go func() {
defer wg.Done()
n++
}()
}
wg.Wait()
fmt.Println(n)
}
我们运行一下:
明显可以看到结果不是1000,这就是并发不安全的
1. 使用atomic
2. 用读写锁实现
1.2 容器并发安全性
读写一般的map是不可以的,我们需要用到sync.Map , 存数据是 mp.store( i , i ), 读数据是 mp.lode(i)
二、多路复用
我们都知道,当前的IO多路复用有三种: select 、poll、epoll ,在go语言当中,只有select(这里是针对不同的操作系统进行了封装,我们也不用考虑到底是什么,只管用就行了)
go的select在遍历的时候是只要有一个准备好了就会返回
// 倒计时 模拟火箭发射
func countDown(countCh chan int, n int, finishCh chan struct{}) {
if n <= 0 { //从n开始倒数
return
}
for {
countCh <- n //把n放入管道
time.sleep(time.second) //等待一秒
n-- //n减1
if n <= 0 { //n减到0时退出
finishCh <- struct{}{} //成功结束
break //退出for循环
}
}
}
// 中止 键盘有输入就表示要中断
func abort(ch chan struct{}) {
buffer := make([]byte, 1)
os.Stdin.Read(buffer) //阻塞式IO,如果标准输入里没数据,该行一直阻塞。注意在键盘上敲完后要按下Enter才会把输入发给Stdin
ch <- struct{}{}
}
func main() {
countCh := make(chan int)
finishCh := make(chan struct{})
go countDown(countCh, 10, finishCh) //开一个子协程,去往countCh和finishCh里放数据
abortCh := make(chan struct{})
go abort(abortCh) //开一个子协程,去往abortCh里放数据
LOOP:
for { //循环监听
select { //同时监听3个channel,谁先准备好就执行谁,然后进入下一次for循环
case n := <-countCh:
fmt.Println(n)
case <-finishCh:
fmt.Println("finish")
break LOOP //退出for循环。在使用for select时,单独一个break不能退出for循环
case <-abortCh:
fmt.Println("abort")
break LOOP //退出for循环
}
}
}
三、协程常见的面试题
3.1交替打印奇数偶数
方法一:使用无缓存的channel 进行通信
// PrintOddAndEven1 /*
func PrintOddAndEven1() {
//方法一,使用无缓冲的channel进行通信
var wg = new(sync.WaitGroup) //注意这里需要是指针go语言当中都是值传递
wg.Add(2)
ch := make(chan struct{}) //无缓冲channel
defer close(ch)
maxVal := 100
go func() {
defer wg.Done()
for i := 1; i <= maxVal; i++ {
ch <- struct{}{}
if i%2 == 1 { //奇数
fmt.Printf("the odd is %d\n", i)
}
}
}()
go func() {
defer wg.Done()
for i := 1; i <= maxVal; i++ {
<-ch //从管道当中读取一个数据
if i%2 == 0 { //偶数
fmt.Printf("the even is %d\n", i)
}
}
}()
wg.Wait()
}
func main() {
PrintOddAndEven1()
fmt.Println("over")
}
/*
原理:因为c1是无缓存的,所以只有当读写同时就绪才不会被阻塞,当同时就绪的时候,两个协程会同时进入if条件语句,他们的i值都是一样的,这时候只有一个是满足条件的,所以会按顺序交替打印出 1~100.
方法二:使用有缓存的channel进行通信
// 方法二:用有缓存的channel进行通信
func HaveCach() {
wg := sync.WaitGroup{}
wg.Add(2)
c1 := make(chan int, 1)
c2 := make(chan int, 1)
defer close(c1)
defer close(c2)
c1 <- 1 //先往c1当中写入一个数据,确保先打印奇数
go func() {
defer wg.Done()
for i := 1; i < 50; i += 2 {
<-c1
fmt.Printf("奇数 :%d\n", i)
c2 <- 1 //通知c2
}
}()
go func() {
defer wg.Done()
for i := 2; i < 50; i += 2 {
<-c2
fmt.Printf("偶数 :%d\n", i)
c1 <- 1 //通知c1
}
}()
wg.Wait()
}
func main() {
HaveCach()
fmt.Println("over")
}
第二个方法使用这个有缓冲的channel。有缓冲的channel当容量没有达到上限时写入不会阻塞在这里奇数协程的channel容量为1我们提前给他写入了一个数据因此当偶数和奇数协程都开始读取数据时,首先读取到数据的是奇数协程,奇数协程打印完之后在通知偶数协程打印,偶数协程打印完成之后在通知奇数协程重复下去就实现了交替打印的效果。