Go语言并发、锁、channel
2018-10-11 本文已影响0人
AuglyXu
- 多线程同时执行叫做并行
- 并发就是在不同线程中来回切换执行来达到并行的效果就是并发
- 通过go可以在当前线程中开启一个协程
- 保证协程被执行,那么主线程不能挂掉
runtime包中常用的方法
- runtime.Gosched()
- 作用:用于出让本次的执行权限
- 注意点:让出本次执行权限并不意味着不再执行
- 应用场景: 让某些协程执行的次数少一次, 让某些协程执行的次数多一些
- runtime.Goexit()
- 作用:终止调用它的go程,其他go程不受影响
- 注意点:这个go程将不会再被调用
-
runtime.NumCPU()
-
获取本地机器的逻辑CPU个数
-
runtime.GOMAXPROCS(num)
-
设置可同时执行的最大CPU数,并返回上一次设置的CPU个数
-
注意点:Go1.8之后, 系统会自动设置为最大值,了解------>忘记
互斥锁
- 当使用互斥锁后,当前go程将不被再次调用
- 案例一:有序打印
/*
需求: 定义个打印的函数, 可以逐个的打印传入字符串的每个字符
在两个协程中调用这个好方法, 并且还要保持输出的内容有序的
*/
var lock = sync.Mutex{}
func printer(str string){
lock.Lock()//添加锁,如果不添加那么可能执行输出hello也可能执行输出world,那么就是无序的
for _,val := range str{
fmt.Printf("%c", ch)
time.Sleep(time.Millisecond * 300)
}
lock.Unlock()
}
func main(){
go printer("hello")
go printer("world")
for{
;
}
}
互斥锁的资源抢夺问题
- 注意点:如果两个go程上了同一把锁,那么当一个go程被锁上时,另一个go程也会被锁住
- 案例二:生产者和消费者
var lock = sync.Mutex{}
var buff = [10]int
func producer(){
lock.Lock()
rand.Seed(time.Now().UnixNano())
for i:=0; i < 10 ;i++{
num := rand.Intn(100)
fmt.Println("生产者生产了",num)
buff[i] = num
time.Sleep(time.Millisecond * 300)
}
lock.Unlock()
}
func consumer(){
lock.Lock()
for i:=0; i < 10 ;i++{
buff[i] = num
fmt.Println("消费者消费到了",num)
}
lock.Unlock()
}
func main() {
go producer()
// 我们想要的是, 只有生产者生产了, 我们才能消费
// 注意点: 在多go程中, 如果生产者生产的太慢, 那么消费者就会消费到错误的数据
go consumer()
// 注意点: 看上去通过给生产者以及消费者同时加锁就能解决, 只有生产完了才能消费
// 但是取决于谁想执行加锁操作, 所以不完美
for{
;
}
}
-
在上述案例中,只能一个生产者对应一个消费者,当有第二个生产者或者第二个消费者时会因为并发而产生数据混乱。
-
在上述案例中,无法判断先执行消费者还是先执行生产者,如果先进入了调用者的go程,则会取不到数据,就会发生数据混乱
-
为了解决上述问题,我们可以用管道来解决这个问题
管道
- 管道是一种数据类型,和字典切片很像,不用make函数创建就使用会报错
- 格式: var 变量名称 chan 数据类型 ---------> var myCh chan int
- 作用:在Go语言的协程中, 一般都使用管道来保证多个协程的同步, 或者多个协程之间的通讯
var myCh chan int
myCh = make(chan int, 3)
- 以上代码创建了一个容量为3的管道(注意:长度默认为0,添加数据以后长度会动态变化)
管道的使用
管道写入数据
- myChan<-
var myCh chan int
myCh = make(chan int, 3)
myCh<-666 //写入了一个数据666
管道读取数据
- <-myChan
var myCh chan int
myCh = make(chan int, 3)
myCh<-666 //写入了一个数据666
fmt.Println(<-myCh) //读取了666
管道写入和读取的注意点
- 没有创建管道容量直接使用会报错
var myCh chan int
myCh<-666 //报错
- 管道中没有数据时读取会报错
var myCh chan int
myCh = make(chan int, 3)
fmt.Println(<-myCh) //报错
- 管道已满,再向管道中写入数据会报错
var myCh chan int
myCh = make(chan int, 3)
myCh<-1 //写入了一个数据1
myCh<-2 //写入了一个数据2
myCh<-3 //写入了一个数据3
myCh<-4 //报错
管道的关闭
- close(管道名称)
- 注意点:管道关闭以后不能再管道中写入数据,但是可以在管道中读取数据
管道的遍历
- 可以使用for循环, 也可以使用 for range循环, 以及死循环来遍历。
- 更推荐使用后两者。因为在企业开发中, 有可能我们不知道管道中具体有多少条数据, 所以如果利用for循环来遍历, 那么无法确定遍历的次数, 并且如果遍历的次数太多, 还会报错
for range遍历
- 注意点:在写完数据以后必须关闭管道,否则会报错
- 一般企业开发中,管道数据写完之后都会将管道关闭
var myCh chan int
myCh = make(chan int, 3)
myCh<-1 //写入了一个数据1
myCh<-2 //写入了一个数据2
myCh<-3 //写入了一个数据3
close()//管道必须关闭,否则报错
for v := range myChan{
fmt.Println(v) //先后输出 1 2 3
}
死循环遍历
- 注意点: 如果被遍历的管道没有关闭, 那么会报错
- 如果管道被关闭, 那么会将true返回给ok, 否则会将false返回给Ok
var myCh chan int
myCh = make(chan int, 3)
myCh<-1 //写入了一个数据1
myCh<-2 //写入了一个数据2
myCh<-3 //写入了一个数据3
close()//管道必须关闭,否则报错
for{
if value,ok:= <-myChan;ok{
fmt.Println(v) //先后输出 1 2 3
}
}
管道的阻塞现象(重点)
-
单独在主线程中操作管道, 写满了会报错, 没有数据去读取也会报错
-
只要在go程中操作管道, 无论有没有写满, 无论有没有数据都会发生管道阻塞的现象
-
阻塞现象(和输入缓冲区很相似)
-
在go程中,如果写满了,再写入数据,则不会报错,等待管道中的数据被取出后再添加
-
在go程中,没有数据还在被取出,则不会报错,等待管道中的数据被写入后再取出
-
利用管道阻塞实现并发串行
var myChan chan int
myChan = make(chan int,10)
func producer(){
rand.Seed(time.Now().UnixNano())
for i:=0; i < 10 ;i++{
num := rand.Intn(100)
myChan <- num
fmt.Println("生产者生产了",num)
time.Sleep(time.Millisecond * 300)
}
}
func producer2(){
rand.Seed(time.Now().UnixNano())
for i:=0; i < 10 ;i++{
num := rand.Intn(100)
myChan <- num
fmt.Println("生产者生产了",num)
time.Sleep(time.Millisecond * 300)
}
}
func consumer(){
for i:=0; i < 10 ;i++{
num := <-myChan
fmt.Println("消费者消费到了",num)
}
}
func main() {
go producer()
go producer2()
go consumer()
for{
;
}
}
- 以上代码可以不止一个生产者或者消费者
利用管道阻塞解决最后写死循环保证主线程不挂
- 注意点: 如果这个管道在协程中有被使用,那么编译器会认为主线程在取数据的时候会等待协程中输入,并不会报错
- go程执行完后才向管道中填充数据
var myCh = make(chan int, 3)
var exitCh = make(chan bool, 1)
go func() {
for i:=0; i<3; i++ {
fmt.Println("生产了数据", i)
myCh<-i
}
exitCh<-true
}()
fmt.Println("exitCh之前的代码")
<-exitCh // 阻塞
fmt.Println("exitCh之后的代码")
无缓冲管道
- 无缓冲管道没有容量,在主程中既不可以读也不可以写
- 无缓冲区管道只能存在在go程中,并且如果在同一个go程中,无论先读或者先写都会阻塞
- 读写必须都存在,但如果都在主程中会报错,在同一个go程中会阻塞
- 无缓冲管道如果存在在不同的go程中,先读或者先写无所谓
//在go程中可以只读或者只写,会阻塞
myCh := make(chan int, 0)
go func() {
fmt.Println("123")
myCh<-998
//<-myCh
fmt.Println("abc")
}()
无缓冲管道解决死循环
//定义一个没有缓冲的管道
exitCh := make(chan bool)
go func() {
for i:= 0; i < 5; i++ {
myCh<-i
fmt.Println("生产了", i)
}
exitCh<-true
}()
//for{
// ;
//}
//time.Sleep(time.Second)
<-exitCh
单向管道和双向管道
-
默认情况下所有的管道都是双向的管道(可读可写)
-
那么在企业开发中, 我们可能会需要将管道作为函数的参数, 并且还需要限制函数中如何使用管道,那么这个时候我们就可能会使用单向管道
-
格式:
双向格式:
var myCh chan int;
myCh = make(chan int, 5)
myCh = make(chan int)单向格式:
var myCh chan<- int; 只写的管道
var myCh <-chan int; 只读的管道 -
注意点:
1.双向管道可以赋值给单向管道
2.单向管道赋值给双向管道会报错
单向管道作为函数参数
- 增强了代码的语义
- 管道是地址传递
// 定义一个函数模拟生产者
func producer(buff chan<- int) {
rand.Seed(time.Now().UnixNano()) // 种随机种子
for i:=0; i<5;i++ {
// 产生随机数
num := rand.Intn(100)
fmt.Println("生产者生产了", num)
// 将生产好的数据放入缓冲区
buff<-num
//time.Sleep(time.Millisecond * 300)
}
}
// 定义一个函数模拟消费者
func consumer(buff <-chan int, exitCh chan<- int) {
for i:=0; i<5;i++ {
num := <-buff
fmt.Println("-------消费者消费到了", num)
}
exitCh<-666
}
func main() {
// 定义一个数组模拟缓冲区
var buff = make(chan int, 5)
var exitCh = make(chan int)
go producer(buff)
go consumer(buff, exitCh)
<-exitCh
fmt.Println("程序结束了")
//for{
// ;
//}
}
管道是指针类型
var myCh1 chan int = make(chan int, 5)
//fmt.Println(myCh1) // 0xc042094000
//fmt.Printf("%p\n", myCh1) // 0xc042094000
//fmt.Printf("%p\n", &myCh1) // 0xc042082018
myCh1<-1
myCh1<-2
var myCh2 <-chan int
myCh2 = myCh1 // 将双向的管道转换成单向的管道
// 打印单向管道的长度和容量
fmt.Println("len", len(myCh2), "cap", cap(myCh2))//len 2 cap 5
fmt.Println(<-myCh2)//1
// 打印双向管道的长度和容量
fmt.Println("len", len(myCh1), "cap", cap(myCh1))//len 1 cap 5
select结构
- select选择结构和switch很像,如果所有case不满足则会执行default
- 企业开发中一般不会使用default,因为容易经常跑进default
- 企业开发中,一般通过select用于消费多个管道中的数据
- 企业开发中,一般通过select控制是否超时
- 企业开发中,一般通过select控制退出主线程
- 以下是一个生产消费的案例
// 1.创建一个管道
myCh1 := make(chan int, 5)
myCh2 := make(chan int, 5)
exitCh := make(chan bool)
// 2.开启一个协程生产数据
go func() {
//time.Sleep(time.Second * 5) 如果存在这行数据会打印超时了,不存在则会正常消费
for i := 0; i < 10 ; i++ {
myCh1<-i
fmt.Println("生产者1生产了", i)
}
close(myCh1)
exitCh<-true
}()
go func() {
time.Sleep(time.Second * 5)
for i := 0; i < 10 ; i++ {
myCh2<-i
fmt.Println("生产者2生产了", i)
}
close(myCh2)
}()
for{
select {
case num1 := <-myCh1:
fmt.Println("------消费者消费了myCh1", num1)
case <-time.After(3):
fmt.Println("超时了")
return
}
time.Sleep(time.Millisecond)
}
fmt.Println("程序结束了")
定时器
- 想使用定时器需要使用time包
一次性定时器
NewTimer函数
- 有一个Time结构体
- type Timer struct {
C <-chan Time
r runtimeTimer
}
- type Timer struct {
- 作用, 就是让系统在指定时间之后, 往Timer结构体的C属性中写入当前的时间
- NewTimer的函数接收一个时间,代表阻塞多少秒后写入时间
- 注意:该函数返回的是一个Time结构体,要调用其属性必须 名称.属性
start := time.Now()
fmt.Println(start) //打印当前时间
timer := time.NewTimer(time.Second * 3) // Timer
fmt.Println(<-timer.C) //打印三秒后的时间
After函数
- After的函数接收一个时间,代表阻塞多少秒后写入时间
- 注意:该函数返回的是一个Time结构体中C的属性
start := time.Now()
fmt.Println(start)
timer := time.After(time.Second * 3) // Timer.C
fmt.Println(<-timer)
周期性定时器
- time包中有一个NewTicker的函数,接收一个时间,表明阻塞多少秒向Time结构体中写入数据
- 注意点:该函数会反复往结构体中写入数据,所以需要关闭,可以用stop函数进行关闭
start := time.Now()
fmt.Println(start)
ticker := time.NewTicker(time.Second * 2)
for{
fmt.Println(<-ticker.C)
}