channel
- Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。
- channel就是通过通信共享内存;虽然也可以通过共享内存而实现通信;但是存在不同goroutine中容易发生竞态问题;虽然可以通过锁来解决,但是势必会影响效率
- goroutine是Go程序并发的执行体,channel就是它们之间的连接;channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。
- channel像一个传送带或者队列,总是遵循先入先出(First In First Out)
- channel是引用类型;需要使用make函数初始化之后才能使用
var ch1 chan int // 声明一个传递整型的通道
//通道是引用类型,通道类型的空值是nil
fmt.Println(ch1) // <nil>
//make初始化才可以使用,缓存大小可以选择设置
ch4 := make(chan int,2)
/*
channle就三种操作
发送(send)、接收(receive)和关闭(close)三种操作。
通道关闭后还是可以receive
*/
ch := make(chan int)
ch <- 10 // 把10发送到ch中
x := <- ch // 从ch中接收值并赋值给变量x
<-ch // 从ch中接收值,忽略结果
close(ch)//关闭通道;不是必须的;可以被垃圾回收机制回收的
/*
关于关闭的通道:关闭的通道可以取值但是不可以存值
- 对一个关闭的通道再发送值就会导致panic。
- 对一个关闭的通道进行接收会一直获取值直到通道为空。
- 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
- 关闭一个已经关闭的通道会导致panic。
- 关闭 channel 会产生一个广播机制,所有向 channel 读取消息的 goroutine 都会收到消息
*/
无缓存channel
- 无缓冲的通道必须有接收才能发送;接收操作先执行,接收方的goroutine将阻塞;直达发送方往通道发送数据成功
- 无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道
func noBufChan() {
//无缓冲的通道必须有接收才能发送。所以必须先定义一个接受方,才可以发送
c = make(chan int)
//c <- 10 //hang住了 程序无法执行下去了 值发送不进去,没有接受的缓存区;所以必须先定义一个goroutine来接受,再发送
go func() {
x := <-c
fmt.Println("X:", x)
}()
c <- 10
fmt.Println("C:", c)
close(c)
}
有缓存channel
- 有缓冲区 ,可以往里面先存值,通道可以先直接返送,而不需接受方
- 只能往通道中发送指定容量的数据,发送多个就会报错,原理同无缓存
func bufChan() {
c = make(chan int, 1) //定义1个就是说只能往通道中发送一个值,发送多个就会报错;原理同无buf
c <- 10
fmt.Println(c)
close(c)
}
channel遍历
- 当循环往通道外取值时,使用for x:=range y{};系统会自动判断是否结束;
- 使用for循环时 则需要判断是否OK;
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
// 开启goroutine将0~100的数发送到ch1中
go func() {
for i := 0; i < 100; i++ {
ch1 <- i
}
close(ch1)
}()
// 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中
go func() {
for {
i, ok := <-ch1 // 通道关闭后再取值ok=false
if !ok {
break
}
ch2 <- i * i
}
close(ch2)
}()
// 在主goroutine中从ch2中接收值打印
for i := range ch2 { // 通道关闭后会退出for range循环
fmt.Println(i)
}
}
单向通道
- 规定通道只能用来send 或者receive;一般用于函数参数的传递,来保证通道的操作唯一
- chan<- int:只能往通道中send int类型的数据
- <-chan int:只能从通道中receive int类型的数据
func f1(ch chan<- int) {
for i := 0; i < 100; i++ {
ch <- i
}
close(ch)
}
select多路复用
- 同一时刻对多个通道实现操作的场景;
- 可处理一个或多个channel的发送/接收操作。
- 如果多个case同时满足,select会随机选择一个。
- 对于没有case的select{}会一直等待,可用于阻塞main函数。
func main() {
var ch1 = make(chan int, 1) //0,2,4,6,8;因为只有一个缓存区;如果缓存大于1的话,case随机执行;无法预测结果
for i := 0; i < 10; i++ {
select { //哪个case可以执行就执行哪个
case x := <-ch1:
fmt.Println(x)
case ch1 <- i:
}
}
}
并发安全锁
- 当通过共享内存而实现通信时,就不得不考虑并发安全的问题
- go语言中的锁有两种;互斥锁、读写锁
互斥锁;sync.Mutex
var (
lock sync.Mutex
)
func add() {
lock.Lock()
x++
lock.Unlock()
}
读写锁;sync.RWMutex
- 主要用于读多写少的情况
- 分为读锁和写锁;当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待
```go
var (
rwlock sync.RWMutex
)
func write() {
rwlock.Lock() // 加写锁
x = x + 1
time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
rwlock.Unlock() // 解写锁
}
func read() {
rwlock.RLock() // 加读锁
time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
rwlock.RUnlock() // 解读锁
}
原子操作
- 代码中的加锁操作因为涉及内核态的上下文切换会比较耗时、代价比较高。
- 针对基本数据类型我们还可以使用原子操作来保证并发安全,因为原子操作是Go语言提供的方法它在用户态就可以完成,因此性能比加锁操作更好
- Go语言中原子操作由内置的标准库sync/atomic提供。
var (
x int64
wg sync.WaitGroup
lock sync.Mutex
)
func main() {
for i := 0; i < 200; i++ {
wg.Add(1)
//go add()
go atoAdd()
}
wg.Wait()
fmt.Println(x)
}
//加锁
func add() {
defer wg.Done()
lock.Lock()
x++
lock.Unlock()
}
//atomic 效能更高
func atoAdd() {
defer wg.Done()
atomic.AddInt64(&x, 1)
}