Golang并发编程之channel
1、channel
单纯将函数实现并发是没有任何意义的,函数与函数之间需要交换数据才能够体现并发执行函数的意义。
虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine
中容易发生竞态情况。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法肯定会造成性能影响。
Go语言的并发模型是CSP
,提倡通过通信共享内存而不是通过共享内存而实现通信。
如果说goroutine
是Go程序并发的执行体,channel
就是他们之间的连接。channel
是可以让一个goroutine
发送特定值到另一个goroutine
的通信机制。
Go语言中的通道(channel)是一种特殊的类型,通道像一个传送带或者队列,总是遵循先入先出(FIFO)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel
的时候需要为其制定元素类型。
1.1、channel类型
声明通道类型的格式如下:
var 变量 chan 元素类型
举几个例子
var a1 chan int // 声明一个int类型的chan
var a2 chan str // 声明一个str类型的chan
var a3 chan []int // 声明一个int slice的chan
1.2、创建channel
channel是一个引用类型,往channel里写入数据相当于是拿到其内存地址操作,必须使用make函数初始化(通道类型的控制是nil
)
package main
import "fmt"
var a chan int
func main(){
a = make(chan int,15) // 通道初始化(带缓冲区)
fmt.Println(a) // 0xc000098000
}
1.3、channel操作
通道有发送(send)、接收(receive)和关闭(close)三种操作,发送和接收都是用<-
符号。
先定义一个通道
b := make(chan int,15)
发送
将一个值发送到通道中
b <- 20 // 把20发送到b中
接收
x := <- b // 从b中接收值并赋值给变量x
<-b // 从b中接收值
关闭
通过调用内置的close
函数来关闭通道
close(b)
关于关闭通道需要注意的是,只有在通知接收方goroutine所有的数据都是发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件机制是不一样的,在结束操作之后关闭文件时必须要做的,但是关闭通道不是必须的。
1.4、channel练习题
需求:
1.启动一个goroutine,生成100个数发送到ch1
2.启动一个goroutine,从ch1中取值,计算其平方放到ch2中
3.在main中,从ch2取值打印出来
代码:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func f1(ch1 chan int){
defer wg.Done()
for i:=0;i<100;i++{
ch1 <- i
}
close(ch1)
}
func f2(ch1,ch2 chan int){
defer wg.Done()
for {
x,ok := <- ch1
if !ok{
break
}
ch2 <- x * x
}
close(ch2)
}
func main(){
a := make(chan int,100)
b := make(chan int,100)
wg.Add(2)
go f1(a)
go f2(a,b)
wg.Wait()
for ret := range b {
fmt.Println(ret)
}
}
2、单向通道
有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如只能发送或只能接收。Go语言中提供了单向通道来处理这种情况,例如。我们把上面的例子改造如下:
var wg sync.WaitGroup
func f1(ch1 chan <- int){
defer wg.Done()
for i:=0;i<100;i++{
ch1 <- i
}
close(ch1)
}
func f2(ch1 <- chan int, ch2 chan <- int){
defer wg.Done()
for {
x,ok := <- ch1
if !ok{
break
}
ch2 <- x * x
}
close(ch2)
}
func main(){
a := make(chan int,100)
b := make(chan int,100)
wg.Add(2)
go f1(a)
go f2(a,b)
wg.Wait()
for ret := range b {
fmt.Println(ret)
}
}
其中,chan <- int
是一个只能发送的通道,可以发送但是不能接收;<- chan int
是一个只能接收的通道,可以接收但是不能发送。
3、worker pool(goroutine池)
编写代码实现一个计算随机数的每个位置数字之和的程序,要求使用goroutine
和channel
构建生产者和消费者模式,可以指定启动的goroutine
数量
在工作中我们通常会使用workerpool
模式,控制goroutine
数量
4、select多路复用
在某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。
for {
data,ok := <- ch1
data,ok := <- ch2
}
这种方式虽然可以实现在多通道接受值的需求,但是运行性能会差很多,为了应对这个场景,Go内置select
关键字,可以同时响应多个通道的操作,select的使用类似于switch语句,它有一些列cae分支和一个默认的分支。每个case会对应一个通道通信(接收和发送)过程。select
会一直等待,直到某个case
的通信操作完成时,就会执行case
分支对应的语句。具体格式如下:
select {
case <- ch1:
...
case data := <- ch2
...
case ch3 <- data:
...
default:
默认操作
}
举个例子来演示select
的使用
package main
import "fmt"
func main(){
ch := make(chan int,1)
for i:=0;i<10;i++{
select {
case x := <-ch:
fmt.Println(x)
case ch <- i:
}
}
}
5、sync
package main
import (
"fmt"
"sync"
)
// 锁
var x = 0
var wg sync.WaitGroup
func add(){
for i:=0;i<5000;i++{
x = x + 1
}
wg.Done()
}
func main(){
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}
上面的代码中我们开启两个goroutine
去累加变量x的值,这两个goroutine
在访问和修改x
变量的时候就会存在数据竞争,导致最后的结果与预期不符
6、互斥锁
互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine
可以访问共享资源。Go语言中使用sync
包的Mutex
类型来实现互斥锁。使用互斥锁来实现上面代码的问题
package main
import (
"fmt"
"sync"
)
// 锁
var x = 0
var wg sync.WaitGroup
var lock sync.Mutex // 声明一个全局锁
func add(){
for i:=0;i<5000;i++{
lock.Lock() // 上锁
x = x + 1
lock.Unlock() // 释放锁
}
wg.Done()
}
func main(){
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}
使用互斥锁能够保证同一时间有且只有一个goroutine
进入临界区,其它的goroutine
则在等待锁;当互斥锁释放后,等待的goroutine
才可以获取锁进入临界区,多个goroutine
同时等待一个锁时,唤醒的策略是随机的。
7、读写互斥锁
互斥锁是完全互斥的,但是有很多实际的场景下是读多写少,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加载锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync
包中的RWMtex
类型。
读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其它的goroutine
如果是获取读锁会继续获取锁,如果是获取写锁就会等待;大概一个goroutine
获取写锁之后,其它的goroutine
无论是获取读锁还是写锁都会等待。
package main
import (
"fmt"
"sync"
"time"
)
var (
x = 0
wg sync.WaitGroup
lock sync.Mutex
rwLock sync.RWMutex
)
// 读操作
func read(){
defer wg.Done()
//lock.Lock()
rwLock.RLock()
fmt.Println(x)
time.Sleep(time.Millisecond)
//lock.Unlock()
rwLock.RUnlock()
}
// 写操作
func write(){
defer wg.Done()
//lock.Lock()
rwLock.Lock()
x = x + 1
time.Sleep(time.Millisecond * 5)
//lock.Unlock()
rwLock.Unlock()
}
func main(){
start := time.Now()
for i:=0;i<100;i++{
go write()
wg.Add(1)
}
time.Sleep(time.Second)
for i:=0;i<1000;i++{
go read()
wg.Add(1)
}
wg.Wait()
fmt.Println(time.Now().Sub(start))
}
8、sync.Once
在编程的很多场景中,我们需要确保某些操作在高并发场景中只执行一次,例如只加载一次配置文件、只关闭一次通道等。
Go语言中sync
包中提供了一个针对只执行一次场景的解决方案sync.Once
。
sync.Once
只有一个Do
方法,其签名如下:
func (o *Once) Do(f func()){}
如果要执行函数
f
需要传递参数就需要搭配闭包来使用。
sync.map
Go语言中内置的map不是并发安全的。请看下面实例:
package main
import (
"fmt"
"strconv"
"sync"
)
var m2 = sync.Map{}
func main(){
wg := sync.WaitGroup{}
for i:=0;i<21;i++{
wg.Add(1)
go func(n int){
key := strconv.Itoa(n)
m2.Store(key,n)
value,_ := m2.Load(key)
fmt.Printf("k=:%v,v=:%v\n",key,value)
wg.Done()
}(i)
}
wg.Wait()
}