chan用于high-level (高端的编程,比如说,两个程序(运行在同一台电脑,或者不同电脑上),或者两个线程(thread)之间的sync), 而锁用于low-level(低端编程,单个变量的sync).
如果把线程安全定义为允许多个goroutine同时去读写,那么golang 的channel 是线程安全的。不需要在并发读写同一个channe时加锁。
channel用于goroutine之间的通信
如果不用channel,使用共享全局变量的方式,需要加锁
// synchornized 同步
// golang中的 sync包中有互斥锁
var lock sync.Mutex // mutex 互斥
lock.Lock() // 上锁
// 多个goroutine同时对相同的数据进行修改
lock.Unlock() // 解锁
使用同步锁并发效率会很低
channel主要用于goroutine通信和解决主线程等待goroutine执行结束再退出的问题
basic concept of channel
本质上是一个FIFO的数据结构-队列
线程安全,不需要加锁
channel是有类型的,如字符串channel chan string,只能保存string数据
多线程同步问题
互斥锁
互斥锁的本质是当一个goroutine访问的时候, 其它goroutine都不能访问
这样就能实现资源同步, 但是在避免资源竞争的同时也降低了程序的并发
性能,程序由原来的并发执行变成了串行
打印案例
没有添加互斥锁, 那么两个人都有机会输出自己的内容
添加互斥锁,只有当一个输出完毕的时候,另一个才能输出
package main
import (
“fmt”
“sync”
“time”
)
//创建一个互斥锁
//可以让程序从并发状态变成并行状态
var lock = sync.Mutex{}
//定义一个打印字符的函数
func myprint(str string) {
//添加锁
lock.Lock()
for _, value := range str {
time.Sleep(time.Microsecond *300)
fmt.Printf(“%c”, value)
}
//解锁
lock.Unlock() }
//调用者1
func person1() {
myprint(“hello”)
}
//调用者2
func person2() {
myprint(“world”)
}
func main() {
//开启go程
go person1()
go person2()
//保证主线程不退出,程序不结束
for {
;
} } 生产者与消费者 生产者消费者模型 某个模块(函数)负责生产数据, 这些数据由另一个模块来负责处理 一般生产者消费者模型包含三个部分生产者、缓冲区、消费者 没有缓冲区,消费者发生变化, 会直接影响生产者, 耦合性太强 添加缓冲区可以提高效率 生产者和消费者资源竞争问题 生产者生产产比较慢, 而消费比较快, 就会导致消费者消费到错误数据 package main
import (
“fmt”
“math/rand”
“time”
)
//定义数组模拟缓冲区
var arr [10]int
//定义模拟生产者函数
func producter() {
//定义随机因子
rand.Seed(time.Now().UnixNano())
//产生随机数
for i := 0;i < 10 ;i++ {
num := rand.Intn(100)
fmt.Println(“生产者生产了”, num)
//将生产的数据放入缓冲区中
arr[i] = num
time.Sleep(time.Millisecond * 300)
} }
//定义函数模拟消费者
func consumer(){
for i := 0;i < 10 ;i++ {
value := arr[i]
fmt.Println(“——消费者消费了”,value)
}
}
func main() {
// 我们想要的是, 只有生产者生产了, 我们才能消费
// 注意点: 在多go程中, 如果生产者生产的太慢, 那么消费者就会消费到错误的数据
go producter()
// 注意点: 看上去通过给生产者以及消费者同时加锁就能解决, 只有生产完了才能消费
// 但是取决于谁想执行加锁操作, 所以不完美
go consumer()
for {
;
} } 利用互斥锁解决问题 package main
import (
“fmt”
“math/rand”
“sync”
“time”
)
//创建一把互斥锁
var lock = sync.Mutex{}
//定义数组模拟缓冲区
var arr [10]int
//定义模拟生产者函数
func producter() {
/*
为什么在生产者和消费者中都上锁之后, 就可以实现生产完再消费?
因为生产者和消费者中的锁都是同一把, 都是全局变量lock
调用Lock()函数的作用: 是修改Mutex结构体中的state属性的值, 将它改为一个非0的值
每次上锁的时候都会判断有没有被锁定, 如果已经锁定就不锁了, 并且不会执行后面的代码
调用Unlock函数的作用: 是修改Mutex结构体中的state属性的值, 将它改为0
*/
//上锁
// 这里锁定的是当前go程, 也就是当前函数
// 意味着其它的go程不能执行当前的函数, 只有当前锁定这个函数的go程才能执行这个函数
lock.Lock()
//定义随机因子
rand.Seed(time.Now().UnixNano())
//产生随机数
for i := 0;i < 10 ;i++ {
num := rand.Intn(100)
fmt.Println("生产者生产了", num)
//将生产的数据放入缓冲区中
arr[i] = num
//time.Sleep(time.Millisecond * 300)
}
//解锁
lock.Unlock() }
//定义函数模拟消费者
func consumer(){
//上锁
lock.Lock()
for i := 0;i < 10 ;i++ {
value := arr[i]
fmt.Println(“——消费者消费了”,value)
}
//解锁
lock.Unlock() }
func main() {
// 我们想要的是, 只有生产者生产了, 我们才能消费
// 注意点: 在多go程中, 如果生产者生产的太慢, 那么消费者就会消费到错误的数据
go producter()
// 注意点: 看上去通过给生产者以及消费者同时加锁就能解决, 只有生产完了才能消费
// 但是取决于谁想执行加锁操作, 所以不完美
go consumer()
for {
;
} } Go语言管道 管道的基本使用 Channel的本质是一个队列 Channel是线程安全的, 也就是自带锁定功能 Channel声明和初始化 var 变量名称 chan 数据类型 make(chan 数据类型, 容量) 管道和切片/字典一样,必须创建后才能使用,否则会报错 Channel和切片还有字典一样, 是引用类型,是地址传递 package main
import “fmt”
func main() {
/*
1.什么是管道:
管道就是一个队列, 具备先进先出的原则
是线程安全的, 也就是自带锁定功能
2.管道作用:
在Go语言的协程中, 一般都使用管道来保证多个协程的同步, 或者多个协程之间的通讯
3.如何声明一个管道, 和如何创建一个管道
管道在Go语言中和切片/字典一样也是一种数据类型
管道和切片/字典非常相似, 都可以用来存储数据, 都需要make之后才能使用
3.1管道声明格式:
var 变量名称 chan 数据类型
var myCh chan int
如上代码的含义: 声明一个名称叫做myCh的管道变量, 管道中可以存储int类型的数据
3.2管道的创建:
make(chan 数据类型, 容量)
myCh = make(chan int, 3);
路上代码的含义: 创建一个容量为3, 并且可以保存int类型数据的管道
4.管道的使用
4.1如何往管道中存储(写入)数据?
myCh<-被写入的数据
4.2如何从管道中获取(读取)数据?
<-myCh
对管道的操作是IO操作
例如: 过去的往文件中写入或者读取数据, 也是IO操作
例如: 过去的往屏幕上输出内容, 或者从屏幕获取内容, 也是IO操作
stdin / stdout / stderr
注意点:
和切片不同, 在切片中make函数的第二个参数表示的切片的长度(已经存储了多少个数据),
而第三个参数才是指定切片的容量
但是在管道中, make函数的第二个参数就是指定管道的容量, 默认长度就是0
*/
//1.定义一个管道
//var myChan chan int
//2.使用make创建管道
myChan := make(chan int, 3)
//3.往管道中存储数据
myChan<-1
myChan<-2
myChan<-3
//从管道中取出数据
fmt.Println(<-myChan)
fmt.Println(<-myChan)
fmt.Println(<-myChan)
//定义一个管道
var myChan chan int
//直接使用管道
//注意点: 会报错,管道定义完成后不创建是无法直接使用的
//myChan<-666
//fmt.Println(<-myChan)
//创建管道
myChan = make(chan int, 3)
//只要往管道中写入了数据, 那么len就会增加
myChan <- 2
//fmt.Println("len = ", len(myChan), "cap = ", cap(myChan))
myChan <- 4
//fmt.Println("len = ", len(myChan), "cap = ", cap(myChan))
myChan <- 6
//fmt.Println("len = ", len(myChan), "cap = ", cap(myChan))
//注意点: 如果len等于cap, 那么就不能往管道中再写入数据了, 否则会报错
//myChan <- 8
//管道未写入数据,使用管道去取数据会报错
//从管道中取数据,len会减少
//<-myChan
fmt.Println(<-myChan)
fmt.Println("len=",len(myChan),"cap = ", cap(myChan))
fmt.Println(<-myChan)
fmt.Println("len=",len(myChan),"cap = ", cap(myChan))
fmt.Println(<-myChan)
fmt.Println("len=",len(myChan),"cap = ", cap(myChan))
//注意点: 取数据个数也不可以超出写入的数据个数,否则会报错
//fmt.Println(<-myChan) } 管道的遍历和关闭 管道遍历推荐两种方式 for..range方法遍历 for死循环遍历 package main
import “fmt”
func main() {
/*
管道的遍历:
可以使用for循环, 也可以使用 for range循环, 以及死循环来遍历
但是更推荐使用后两者
因为在企业开发中, 有可能我们不知道管道中具体有多少条数据, 所以如果利用for循环来遍历, 那么无法确定遍历的次数, 并且如果遍历的次数太多, 还会报错
*/
//创建一个管道
myChan := make(chan int, 3)
//往管道中写入数据
myChan <- 2
myChan <- 4
myChan <- 6
//注意点: 如果不关闭管道,遍历管道会报错
close(myChan)
//第一种方法遍历管道
//for value := range myChan {
// fmt.Println(value)
//}
//第二种方法遍历管道
//注意点: 如果被遍历的管道没有关闭, 那么会报错
// 如果管道没有被关闭, 那么会将true返回给ok, 否则会将false返回给Ok
for {
if v,ok := <-myChan; ok {
fmt.Println(v)
fmt.Println(ok)
}else {
break
}
}
//注意点: 管道关闭后无法往里面写入数据,会报错,但是可以读取数据不会报错
myChan := make(chan int,3)
close(myChan)
//往管道中写入数据
//myChan<-1
//myChan<-2
//myChan<-3
//从管道中读取数据
<-myChan } 管道阻塞现象 阻塞现象只会发生在go程中运行才会发生,在普通线程中不会发生这种现象 package main
import “fmt”
//定义一个管道
var myChan = make(chan int,2)
func test() {
/myChan<-1
myChan<-2
fmt.Println(“管道满之前的代码”)
//这里不会报错, 会阻塞, 等到将管道中的数据读出去之后, 有的新的空间再往管道中写
myChan<-3
fmt.Println(“管道满之后的代码”)/
//这里不会报错,会阻塞,等到有人往管道中写入数据之后,有新的数据之后才会读取
fmt.Println(“读取之前的代码”)
<-myChan
fmt.Println(“读取之后的代码”)
}
func main() {
/*
单独在主线程中操作管道, 写满了会报错, 没有数据去读取也会报错
只要在go程中操作管道, 无论有没有写满, 无论有没有数据都会发生管道阻塞的现象
*/
go test()
for {
;
}
}
利用管道阻塞实现并发变串行
package main
import (
“fmt”
“time”
)
//创建一个管道
var myChan = make(chan bool)
func printer(str string) {
for _, value := range str {
fmt.Printf(“%c”, value)
time.Sleep(time.Microsecond * 300)
}
}
func person1() {
printer(“hello”)
//往管道中写入数据
//只有printer函数执行完,才会往管道中写入数据
myChan<-true
}
func person2() {
//从管道中读取数据
//只有管道中有数据才会读取,否则会阻塞
<-myChan
printer(“world”)
}
func main() {
go person1()
go person2()
for {
;
}
}
利用管道阻塞实现生产者和消费者模型
package main
import (
“fmt”
“math/rand”
“time”
)
//定义管道模拟缓冲区
var myChan = make(chan int, 10)
//定义生产者函数
func producter() {
//定义随机因子
rand.Seed(time.Now().UnixNano())
//生成随机数
for i := 0; i < 10; i++ {
num := rand.Intn(100)
fmt.Println(“生产者生产了”, num)
//将生产的数据存入到管道中
myChan <- num
}
}
//定义函数模拟消费者
func customer() {
//从管道中读取数据
for i := 0; i < 10; i++ {
num := <-myChan
fmt.Println(“—-消费者消费了”, num)
}
}
func main() {
//创建两个go程
//多个生产者和多个消费者
go producter()
go producter()
go customer()
go customer()
for {
;
} } 无缓冲区管道 管道容量为0的管道就是无缓冲区管道 package main
import “fmt”
func main() {
/*
// 管道总结:
// 管道一般都在go程中使用, 不会直接在主线程中使用, 无论是有缓冲的还是没有缓冲的
// 只要是在go程中使用, 无论是有缓冲的还是没有缓冲的, 都会出现阻塞现象
*/
//创建无缓冲管道
// 注意点:
// 没有缓冲的管道不能直接存储数据
// 没有缓冲的管道不能直接获取数据
// 注意点:
// 想使用没有缓冲的管道, 必须保证读和写同时存在, 而且还必须保证读和写是在不同的go程中,至少有一个在go程中
// 并且读必须写在写的前面
//myChan := make(chan int,0)
// 没有缓冲的管道不能直接获取数据
//fmt.Println(<-myChan)
// 没有缓冲的管道不能直接存储数据
//myChan<-1
/*//读取管道在go程中
go func() {
fmt.Println("读之前的代码")
fmt.Println(<-myChan)
fmt.Println("读之前的代码")
}()
//写入管道在主线程中
func(){
time.Sleep(time.Second * 5)
fmt.Println("写之前的代码")
myChan<-2
fmt.Println("写之后的代码")
}()*/
//无缓冲管道单个使用
//如果是在go程中使用无缓冲的管道, 那么就可以单个使用(只有读, 或者只有写)
myChan := make(chan int,0)
go func() {
//只读不会报错
//<-myChan
//只写不会报错
fmt.Println("写之前的代码")
myChan<-1
fmt.Println("写之后的代码")
}()
for {
;
} } 利用无缓冲区管道解决主线程结束问题 package main
import “fmt”
func main() {
//创建一个有缓冲管道
myChan := make(chan int,3)
//创建一个无缓冲管道
exitChan := make(chan bool,0)
//往管道中存储数据
go func() {
for i := 0; i < 3; i++ {
myChan<-i
fmt.Println("生产了",i)
}
//无缓冲管道存储数据
exitChan<-true
}()
<-exitChan
//for {
// ;
//} } 单向管道 默认情况下所有的管道都是双向的管道(可读可写),那么在企业开发中, 我们可能会需要将管道作为函数的参数, 并且还需要限制函数中如何使用管道,,那么这个时候我们就可能会使用单向管道 双向管道格式 var myCh chan int 单向管道格式 var myCh chan<- int; 只写的管道 var myCh <-chan int; 只读的管道 注意点: 双向管道可以转换为单向的管道 但是单向的管道不能转换为双向的管道 package main
import “fmt”
func main() {
/*
默认情况下所有的管道都是双向的管道(可读可写)
那么在企业开发中, 我们可能会需要将管道作为函数的参数, 并且还需要限制函数中如何使用管道,
那么这个时候我们就可能会使用单向管道
双向格式:
var myCh chan int;
myCh = make(chan int, 5)
myCh = make(chan int)
单向格式:
var myCh chan<- int; 只写的管道
var myCh <-chan int; 只读的管道
双向管道可以转换为单向的管道
但是单向的管道不能转换为双向的管道
*/
//定义一个双向管道
myChan := make(chan int,3)
//定义一个只读的单向管道
var myChan1 <-chan int
//定义一个只写的单向管道
//var myChan2 chan<- int
/*//将双向管道赋给只读的单向管道
myChan1 = myChan
fmt.Println(myChan1)
//将双向管道赋给只写的单向管道
myChan2 = myChan
fmt.Println(myChan2)*/
//将单向管道赋给双向管道
//会报错
//myChan = myChan1
//fmt.Println(myChan) } 单向管道作为函数参数 package main
import (
“fmt”
“math/rand”
“time”
)
//定义一个生产者
func producter(buff chan <- int) {
//定义随机因子
rand.Seed(time.Now().UnixNano())
//生成随机数
for i := 0;i < 5 ;i++ {
num := rand.Intn(100)
//将产生的随机数存储到管道中
buff <- num
fmt.Println(“生产者生产了”, num)
}
}
//定义消费者函数
func consumer(buff <- chan int, exitChan chan <- int ) {
for i := 0;i < 5 ;i++ {
//读取管道中的数据
num := <-buff
fmt.Println(“——消费者消费”, num)
}
//利用管道阻塞解决死循环问题
exitChan<- 666
}
func main() {
//定义两个双向管道
myChan := make(chan int, 5)
exitChan := make(chan int)
//开启两个go程
go producter(myChan)
go consumer(myChan,exitChan)
<-exitChan } select选择结构 在企业开发中, 一般情况下使用select都是用于同时消费多个管道中数据 在企业开发中, 一般情况下select中的default不用写 在企业开发中, 一般情况下使用select来控制退出主线程 在企业开发中, 一般情况下使用select来处理超时 package main
import (
“fmt”
“time”
)
func main() {
// 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) }()
// 2.在主线程中消费数据 //for i := 0; i < 10 ; i++ { // num := <-myCh // fmt.Println("------消费者消费了", num) //}
//for num := range myCh{
// fmt.Println(“——消费者消费了”, num)
//}
*/
// 注意点: 在企业开发中, 一般情况下使用select都是用于同时消费多个管道中数据
// 在企业开发中, 一般情况下select中的default不用写
// 在企业开发中, 一般情况下使用select来控制退出主线程
// 在企业开发中, 一般情况下使用select来处理超时
for{
//fmt.Println(“start”)
select {
case num1 := <-myCh1:
fmt.Println(“——消费者消费了myCh1”, num1)
//case num2 := <-myCh2:
// fmt.Println(“——消费者消费了myCh2”, num2)
//case <-exitCh:
// return
case <-time.After(3):
fmt.Println(“超时了”)
return
//default:
// fmt.Println(“生产者还没有生产好数据”)
}
//fmt.Println(“=====================”)
time.Sleep(time.Millisecond)
}
fmt.Println(“程序结束了”)
}
管道是地址传递
package main
import “fmt”
func main() {
//定义一个双向有缓冲区管道
var myChan = make(chan int,3)
fmt.Println(myChan) //0xc00007c080
fmt.Printf(“%p\n”, myChan) //0xc00007c080
fmt.Printf(“%p\n”, &myChan) //0xc000072018
//管道是地址传递
//定义一个单向管道
var myChan2 chan <- int
myChan2 = myChan
//打印单向管道len和cap
fmt.Println("len = ", len(myChan2),"cap = ", cap(myChan2))
//打印双向管道len和cap
fmt.Println("len = ", len(myChan),"cap = ", cap(myChan)) } 定时器 对时间的操作方法,一般都在time包中查找 package main
import (
“fmt”
“time”
)
func main() {
/*
type Timer struct {
C <-chan Time
r runtimeTimer
}
*/
//1.使用定时器,就要用到time包
// NewTimer作用, 就是让系统在指定时间之后, 往Timer结构体的C属性中写入当前的时间
// 让程序阻塞3秒, 3秒之后再执行
//func NewTimer(d Duration) *Timer
/*start := time.Now()
fmt.Println(start)
//使用定时器
timer := time.NewTimer(time.Second * 3)
fmt.Println(<-timer.C)*/
//2.func After(d Duration) <-chan Time
//这个定时器底层就是NewTimer实现的
/*start := time.Now()
fmt.Println(start)
//使用定时器
timer := time.After(time.Second * 3)
fmt.Println(<-timer)*/
// 以上的定时器都是一次性的定时器, 也就是只会执行一次
/*go func() {
start := time.Now()
fmt.Println(start)
timer := time.After(time.Second * 3)
for {
fmt.Println(<-timer)
}
}()
for {
;
}*/
//周期性定时器
start := time.Now()
fmt.Println(start)
//定义周期性定时器
timer := time.NewTicker(time.Second * 2)
for {
fmt.Println(<-timer.C)
timer.Stop()
} }
https://recomm.cnblogs.com/blogpost/11953584
https://blog.csdn.net/myz123321/article/details/89048002
在并发操作中为了防止多任务同时修改共享资源导致的不确定结果,我们可能会用到互斥锁和读写锁。
一:互斥锁
1.互斥锁有两种操作,获取锁和释放锁
2.当有一个goroutine获取了互斥锁后,任何goroutine都不可以获取互斥锁,只能等待这个goroutine将互斥锁释放
3.互斥锁适用于读写操作数量差不多的情况
二:读写锁
1.读写锁有四种操作 读上锁 读解锁 写上锁 写解锁
2.写锁最多有一个,读锁可以有多个(最大个数据说和CPU个数有关)
3.写锁的优先级高于读锁,这是因为为了防止读锁过多,写锁一直堵塞的情况发生
4.当有一个goroutine获得写锁时,其他goroutine不可以获得读锁或者写锁,知道这个写锁释放
5.当有一个goroutine获得读锁时,其他goroutine可以获得读锁,但是不能获得写锁。所以由此也可得知,如果当一个goroutine希望获取写锁时,不断地有其他goroutine在获得读锁和释放读锁会导致这个写锁一直处于堵塞状态,所以让写锁的优先级高于读锁可以避免这种情况,
6.读写锁适用于读多写少的情景。
从上文我们可以得知,互斥锁是非常霸道地,因为一旦有任何一个goroutine获取了互斥锁,其他goroutine都不能获取了,即使这个goroutine可能仅仅只是读取数据而不是修改数据。
而我们想想一个情景,假设现在有三个goroutine:G1,G2,G3都想要读取一段数据A,我们如果用互斥锁的话,就是以下的情形:
G1先加锁,然后读取A,然后释放;然后G2加锁,读取A,释放;G3加锁,读取A,然后释放…这个操作是串行的,由于每个goroutine都需要排队等待前一个goroutine释放锁,所以效率显然不高。
但是如果这个时候我们用读写锁就可以让G1,G2,G3同时读A,就可以大大的提升效率。
三:互斥锁和读写锁性能对比
但是读写锁的效率就一定比互斥锁高吗?这个问题还有待商榷,之前看到一个博主做了实验,认为互斥锁的效率更高,详情:https://www.cnblogs.com/shuiyuejiangnan/p/9457089.html
之后本人把这个博主的代码copy到本地跑了一下发现确实互斥锁不如读写锁优,不过他的代码中的对比有一些问题,在互斥锁的get操作中返回map然后再获取值,在读写锁的get函数中返回的就是int,我将两者都改为获取map的value值发现还是读写锁的性能要好。
原版代码:
更改后代码:
不过我想按照自己的思路来进行一下对比。
对比代码如下:
package main
import (
“fmt”
“sync”
“time”
)
const MAXNUM = 1000 //map的大小
const LOCKNUM = 1e7 //加锁次数
var lock sync.Mutex //互斥锁
var rwlock sync.RWMutex //读写锁
var lock_map map[int]int //互斥锁map
var rwlock_map map[int]int //读写锁map
func main() {
var lock_w = &sync.WaitGroup{}
var rwlock_w = &sync.WaitGroup{}
lock_w.Add(LOCKNUM)
rwlock_w.Add(LOCKNUM)
lock_ch := make(chan int, 10000)
rwlock_ch := make(chan int, 10000)
lock_map = make(map[int]int, MAXNUM)
rwlock_map = make(map[int]int, MAXNUM)
time1 := time.Now()
for i := 0; i < LOCKNUM; i++ {
go test1(lock_ch, i, lock_map, lock_w)
}
lock_w.Wait()
time2 := time.Now()
for i := 0; i < LOCKNUM; i++ {
go test2(rwlock_ch, i, rwlock_map, rwlock_w)
}
rwlock_w.Wait()
time3 := time.Now()
fmt.Println(“lock time:”, time2.Sub(time1).String())
fmt.Println(“rwlock time:”, time3.Sub(time2).String())
}
func init_map(a map[int]int, b map[int]int) { //初始化map
for i := 0; i < MAXNUM; i++ {
a[i] = i
b[i] = i
}
}
func test1(ch chan int, i int, mymap map[int]int, w *sync.WaitGroup) int {
lock.Lock()
defer lock.Unlock()
w.Done()
return mymap[i % MAXNUM]
}
func test2(ch chan int, i int, mymap map[int]int, w *sync.WaitGroup) int {
rwlock.RLock()
defer rwlock.RUnlock()
w.Done()
return mymap[i % MAXNUM]
}
这里列出来加锁次数从1e1到1e7互斥锁和读写锁的耗时对比,由于本人电脑比较渣,做到1e7次加锁就比较慢了,就不想上继续做了,对比表格如下:
加锁次数 互斥锁耗时 读写锁耗时 互斥锁性能好 读写锁性能好
1e1 0s 0s √ √
1e2 996.8µs 0s √
1e3 978.7µs 996.7µs √
1e4 3.9493ms 2.992ms √
1e5 23.9094ms 29.9204ms √
1e6 223.3684ms 298.2022ms √
1e7 2.3785913s 3.0448529s √
其实1e1-1e3次加锁时,互斥锁和读写锁的耗时是很不稳定的,有时互斥锁耗时多,有时读写锁耗时高,在这里我们主要看1e4以上的加锁对比就可以了
到这里我也是很疑惑的,为什么互斥锁的性能竟然比读写锁要好?这不科学啊!!!
在这里我有一点怀疑:是否golang中sync.Mutex的Lock和Unlock在底层实现的时候要比sync.RWMutex的RLock和RUnlock性能要好?
如果假设成立的话,是有可能出现读写锁的性能不如互斥锁的情况的。
下面我们来验证一下:
package main
import (
“fmt”
“sync”
“time”
)
const LOCKNUM = 1e8 //加锁次数
var locks sync.Mutex //互斥锁
var rwlocks sync.RWMutex //读写锁
func main() {
time1 := time.Now()
for i := 0; i < LOCKNUM; i++ {
locks.Lock()
locks.Unlock()
}
time2 := time.Now()
for i := 0; i < LOCKNUM; i++ {
rwlocks.RLock()
rwlocks.RUnlock()
}
time3 := time.Now()
fmt.Println(“lock time:”, time2.Sub(time1).String())
fmt.Println(“rwlock time:”, time3.Sub(time2).String())
}
直接说结论,两者性能差不多,但是互斥锁的耗时会稍微少一些
今天又做了一下测试,发现其实defer会对程序性能影响产生比较大的影响,于是更改了一下test函数,不使用defer关键字。然后看一下效果:
这时两者的性能其实已经差不多了,至于为什么互斥锁还会用时还是会稍微少一些其实是因为即使在不做任何读取操作仅仅是获取锁和释放锁,互斥锁的用时就要少啦,见下图:
最后我们再做一个实验,在每一个goroutine获取map的值时,让他等待一段时间,增加goroutine的冲突可能,然后再看看效果:
func test1(ch chan int, i int, mymap map[int]int, w *sync.WaitGroup) int {
lock.Lock()
defer lock.Unlock()
w.Done()
time.Sleep(time.Nanosecond)
//ch <- i
return mymap[i % MAXNUM]
}
func test2(ch chan int, i int, mymap map[int]int, w *sync.WaitGroup) int {
rwlock.RLock()
defer rwlock.RUnlock()
w.Done()
time.Sleep(time.Nanosecond)
//ch <- i
return mymap[i % MAXNUM]
}
lock time: 17.5834104s
rwlock time: 5.9241ms
可以看到在增强了goroutine的冲突可能之后,读写锁的优势一下子就体现出来了,而且非常明显。
总结:\color{#FF0000}{总结:}总结:
1.在单纯的只是获取锁和释放锁时,互斥锁的用时要少一些,这主要是因为多个线程同时获取读写锁的情况比较少出现。\color{#FF0000}{1.在单纯的只是获取锁和释放锁时,互斥锁的用时要少一些,这主要是因为多个线程同时获取读写锁的情况比较少出现。 }1.在单纯的只是获取锁和释放锁时,互斥锁的用时要少一些,这主要是因为多个线程同时获取读写锁的情况比较少出现。
2.golang底层实现上,互斥锁确实要比读写锁的性能要好一些,这主要是因为读写锁的底层实现其实是互斥锁加上计数器。\color{#FF0000}{ 2.golang底层实现上,互斥锁确实要比读写锁的性能要好一些,这主要是因为读写锁的底层实现其实是互斥锁加上计数器。}2.golang底层实现上,互斥锁确实要比读写锁的性能要好一些,这主要是因为读写锁的底层实现其实是互斥锁加上计数器。
3.在增强协程互相冲突的效果后,读写锁的性能要明显高于互斥锁。\color{#FF0000}{3.在增强协程互相冲突的效果后,读写锁的性能要明显高于互斥锁。}3.在增强协程互相冲突的效果后,读写锁的性能要明显高于互斥锁。
面对并发问题,是用channel解决,还是用Mutex解决?
如果自己心里还没有清晰的答案,那就读下这篇文章,你会了解到:
使用channel解决并发问题的核心思路和示例
channel擅长解决什么样的并发问题,Mutex擅长解决什么样的并发问题
一个并发问题该怎么入手解解决
一个重要的plus思维
前戏
前面很多篇的文章都在围绕channel介绍,而只有前一篇sync的文章介绍到了Mutex,不是我偏心,而是channel在Golang是first class级别的,设计在语言特性中的,而Mutex只是一个包中的。这就注定了一个是主角,一个是配角。
并且Golang还有一个并发座右铭,在《Effective Go》的channel介绍中写到:
Share memory by communicating, don’t communicate by sharing memory.
通过通信共享内存,而不是通过共享内存而通信。
Golang以如此明显的方式告诉我们:面对并发问题,你首先想到的应该是channel,因为channel是线程安全的并且不会有数据冲突,比锁好用多了。
既生瑜,何生亮。既然有channel了,为啥还提供sync.Mutex呢?
主角不是万能的,他也需要配角。在Golang里,channel也不是万能的,这是由channel的特性和局限造成的。
下面就给大家介绍channel的特点、核心方法和缺点。
channel解决并发问题的思路和示例
channel的核心是数据流动,关注到并发问题中的数据流动,把流动的数据放到channel中,就能使用channel解决这个并发问题。这个思路是从Go语言的核心开发者的演讲中学来的,然而视频我已经找不到了,不然直接共享给大家,他提到了Golang并发的核心实践的4个点:
DataFlow -> Drawing -> Pipieline -> Exiting
DataFlow指数据流动,Drawing指把数据流动画出来,Pipeline指的是流水线,Exit指协程的退出。DataFlow + Drawing就是我提到到channel解决并发问题的思路,Pipeline和Exit是具体的实践模式,Pipeline和Exit我都写过文章,有需要自取:
Golang并发模型:轻松入门流水线模型
Golang并发模型:轻松入门流水线FAN模式
Golang并发模型:并发协程的优雅退出
下面我使用例子具体解释DataFlow + Drawing。借用《Golang并发的次优选择:sync包》中银行的例子,介绍如何使用channel解决例子中银行的并发问题:银行支持多个用户的同时操作。顺便看下同一个并发问题,使用channel和Mutex解决是什么差别。
一起分析下多个用户同时操作银行的数据流动:
每个人都可以向银行发起请求,请求可以是存、取、查3种操作,并且包含操作时必要的数据,包含的数据只和自身相关。
银行处理请求后给用户发送响应,包含的数据只和操作用户相关。
你一定发现了上面的数据流动:
请求数据:个人请求数据流向银行。
响应数据:银行处理结果数据流向用户。
channel是数据流动的通道/管道,为流动的数据建立通道,这里需要建立2类channel:
reqCh:传送请求的channel,把请求从个人发送给银行。
retCh:传送响应的channel,把响应从银行发给个人。
我们把channel添加到上图中,得到下面的图:
以上就是从数据流动的角度,发现如何使用channel解决并发问题。思路有了,再思考下代码层面需要怎么做:
银行:
定义银行,只保存1个map即可
银行操作:接收和解析请求,并把请求分发给存、取、查函数
实现存、取、查函数:处理请求,并把结果写入到用户提供的响应通道
定义请求和响应
用户:创建请求和接收响应的通道,发送请求后等待响应,提取响应结果
mian函数:创建银行和用户间的请求通道,创建银行、用户等协程,并等待操作完成
以上,我们这个并发问题的逻辑实现和各块工作就清晰了,写起来也方便、简单。代码实现有200多行,公众号不方便查看,可以点阅读原文,一键直达。
代码不能贴了,运行结果还是可以的,为了方便理解结果,介绍下示例代码做了什么。main函数创建了银行、小明、小刚3个并发协程:
银行:从reqCh接收请求,依次处理每个请求,直到通道关闭,把请求交给处理函数,处理函数把结果写入到请求中的retCh。
用户小明:创建了存100、取20、查余额的3个请求,每个请求得到响应后,再把下一个请求写入到reqCh。
用户小刚:流程和小明相同,但存100取200,造成取钱操作失败,他查询下自己又多少钱,得到100。
main函数最后使用WaitGroup等待小明、小刚结束后退出。
下面是运行结果:
$ go run channel_map.go
xiaogang deposite 100 success
xiaoming deposite 100 success
xiaogang withdraw 200 failed
xiaoming withdraw 20 success
xiaogang has 100
xiaoming has 80
Bank exit
1
2
3
4
5
6
7
8
这一遭搞完,发现啥没有?用Mutex直接加锁、解锁完事了,但channel搞出来一坨,是不是用channel解决这个问题不太适合?是的。对于当前这个问题,和Mutex的方案相比,channel的方案显的有点“重”,不够简洁、高效、易用。
但这个例子展示了3点:
使用channel解决并发问题的核心在于关注数据的流动
channel不一定是某个并发问题最好的解决方案
map在并发中,可以不用锁进行保护,而是使用channel
现在,回到了开篇的问题:同一个并发问题,你是用channel解决,还是用mutex解决?下面,一起看看怎么选择。
channel和mutex的选择
面对一个并发问题的时候,应当选择合适的并发方式:channel还是mutex。选择的依据是他们的能力/特性:channel的能力是让数据流动起来,擅长的是数据流动的场景,《Channel or Mutex》中给了3个数据流动的场景:
传递数据的所有权,即把某个数据发送给其他协程
分发任务,每个任务都是一个数据
交流异步结果,结果是一个数据
mutex的能力是数据不动,某段时间只给一个协程访问数据的权限擅长数据位置固定的场景,《Channel or Mutex》中给了2个数据不动场景:
缓存
状态,我们银行例子中的map就是一种状态
提供解决并发问题的一个思路:
先找到数据的流动,并且还要画出来,数据流动的路径换成channel,channel的两端设计成协程
基于画出来的图设计简要的channel方案,代码需要做什么
这个方案是不是有点复杂,是不是用Mutex更好一点?设计一个简要的Mutex方案,对比&选择易做的、高效的
channel + mutex思维
面对并发问题,除了channel or mutex,你还有另外一个选择:channel plus mutex。
一个大并发问题,可以分解成很多小的并发问题,每个小的并发都可以单独选型:channel or mutex。但对于整个大的问题,通常不是channel or mutex,而是channel plus mutex。
如果你是认为是channel and mutex也行,但我更喜欢plus,体现相互配合。
总结
读到这里,感觉这篇文章头重脚轻,channel的讲了很多,而channel和mutex的选择却讲的很少。在channel和mutex的选择,实际并没有一个固定答案,也没有固定的方法,但提供了一个简单的思路:设计出channel和Mutex的简单方案,然后选择最适合当前业务、问题的那个。
思考比结论更重要,希望你有所收获:
关注数据的流动,就可以使用channel解决并发问题。
不流动的数据,如果存在并发访问,尝试使用sync.Mutex保护数据。
channel不一定某个并发问题的最优解。
不要害怕、拒绝使用mutex,如果mutex是问题的最优解,那就大胆使用。
对于大问题,channel plus mutex也许才是更好的方案。
参考资料
《Effective Go》,https://golang.org/doc/effective_go.html#sharing
《Mutex Or Channel》,https://github.com/golang/go/wiki/MutexOrChannel
https://blog.csdn.net/m0_43499523/article/details/86483484
https://mp.weixin.qq.com/s/YB5XZ5NatniHSYBQ3AHONw
https://mp.weixin.qq.com/s/68FGjm7PFN5VbVF0zL-PlQ
https://mp.weixin.qq.com/s/RjomKnfwCTy7tC9gbpPxCQ
https://mp.weixin.qq.com/s/UpYbmFTowjCPU83W3DxP6Q
https://golang.org/doc/effective_go.html#sharing
Golang的并发安全
1、通道channel(CAP模型)
channel是Go中代替共享内存的通信方式,channel从底层实现上是一种队列,在使用的时候需要通道的发送方和接收方需要知道数据类型和具体通道。如果有一端没有准备好或消息没有被处理会阻塞当前端。
Actor模型和CAP模型
Actor模型:在Actor模型中,主角是Actor,类似一种worker,Actor彼此之间直接发送消息,不需要经过什么中介,消息是异步发送和处理的
Actor模型描述了一组为了避免并发编程的常见问题的公理:
1.所有Actor状态是Actor本地的,外部无法访问。
2.Actor必须只有通过消息传递进行通信。
3.一个Actor可以响应消息:推出新Actor,改变其内部状态,或将消息发送到一个或多个其他参与者。
4.Actor可能会堵塞自己,但Actor不应该堵塞它运行的线程。
Cap模型:Cap模型中,worker之间不直接彼此联系,而是通过不同channel进行消息发布和侦听。消息的发送者和接收者之间通过Channel松耦合,发送者不知道自己消息被哪个接收者消费了,接收者也不知道是哪个发送者发送的消息。
Go语言的CSP模型是由协程Goroutine与通道Channel实现:
Go协程goroutine: 是一种轻量线程,它不是操作系统的线程,而是将一个操作系统线程分段使用,通过调度器实现协作式调度。是一种绿色线程,微线程,它与Coroutine协程也有区别,能够在发现堵塞后启动新的微线程。
通道channel: 类似Unix的Pipe,用于协程之间通讯和同步。协程之间虽然解耦,但是它们和Channel有着耦合。
Cap模型和Actor模型的区别:
Cap的worker彼此之间不直接连接,通信是通过channel实现的
Actor之间是直接通信
channel不但可以传递消息(数据),也可以用作事件通知。
package main
func main(){
done := make(chan struct{}) //发送空结构体(通知)
c := make(chan string) //数据传输通道
go func() {
s := <-c //接收消息
println(s)
close(done) //关闭通道,为结束通知
}()
c <- “hi!” //发送消息
<-done //阻塞
}
同步模式下需要有goroutine配合,否则会一直阻塞。
异步模式时当缓冲区没有满或者数据未读完的时候,不会出现阻塞:
package main
func main(){
c := make(chan int, 3) //创建带有3个缓冲区的异步通道
c <- 1
c <- 2 //缓冲区没满
println(<-c) //缓冲区有数据不会阻塞
println(<-c)
}
//在程序中异步通道可以提高程序的性能减少排队阻塞
//channel变量本身为指针
channel的收发,channel中还可以使用ok-idom和range模式处理数据
package main
func main() {
done := make(chan struct{})
c := make(chan int)
go func() {
defer close(done)
for {
x, ok := <-c
if !ok { //判断通道是否关闭
return
}
println(x)
}
}()
c <- 1
c <- 2
c <- 3
close(c)
<-done
donr := make(chan struct{})
cr := make(chan int)
go func() {
defer close(donr)
for x := range cr { //循环获取消息
println(x)
}
}()
cr <- 1
cr <- 2
cr <- 3
close(cr)
<-donr }
//及时使用close函数关闭通道引发结束通知,避免出现可能的死锁
channel的关闭中:close和sync.Cond
一次性事件使用close效率更高,没有多余的开销。
使用sync.Cond实现单播或广播事件
使用close或nil通道时的原则
向已关闭通道发送数据,引发panic
从已关闭接收数据,返回以缓冲数据或零值(在使用channel发送结束时最好使用空struct)
无论收发,nil通道都会阻塞
在使用goroutine和channel时一般使用工厂方法绑定
package main
import “sync”
type receive struct{
sync.WaitGroup
date chan int
}
func newR() *receive{
r := &receive{
date :make(chan int),
}
r.Add(1)
go func() {
defer r.Done()
for x := range r.date{
println(“recv : “,x)
}
}()
return r
}
func main(){
r := newR()
r.date <- 1
r.date <- 2
close(r.date) //关闭通道
r.Wait()
}
//recv : 1
//recv : 2
通道可能会引发goroutine leak,指goroutine处于发送或接收阻塞状态,但没有未被唤醒。GC并不收集此类资源,导致他们在队列里长久休眠,形成资源泄露
package main
import (
"runtime"
"time"
)
func testv(){
c := make(chan int)
for i := 0; i < 10; i++{
go func() {
<-c
}()
}
}
func main(){
testv()
for {
time.Sleep(time.Second)
runtime.GC() //强制垃圾回收
}
} //GODEBUG="gctrace=1,schedtrace=1000,scheddetail=1" ./channel5 监控程序goroutine状态,查看监控结果可以看出有大量的goroutine处于chan receive状态,不能结束
https://blog.csdn.net/alvin_666/article/details/84933164
https://www.cnblogs.com/bigben0123/p/8602408.html
https://www.jdon.com/47912
https://www.jtolio.com/2016/03/go-channels-are-bad-and-you-should-feel-bad/
http://open.qiniudn.com/where-can-you-use-golang.pdf
http://open.qiniudn.com/go-next-c.pdf
http://open.qiniudn.com/golang-and-cloud-storage.pdf
http://open.qiniudn.com/golang-and-cloud-storage.pdf
http://open.qiniudn.com/why-i-choose-erlang.pdf
https://www.csdn.net/article/2012-09-14/2809984-will-go-be-the-new-go-to-programming-lan
http://open.qiniudn.com/[Joe-Armstrong][CN]Making-reliable-distributed-systems-in-the-presence-of-software-errors.pdf
http://open.qiniudn.com/[Joe-Armstrong]Making-reliable-distributed-systems-in-the-presence-of-software-errors.pdf
http://open.qiniudn.com/thinking-in-go.mp4