前言
并发指的是同时进行多个任务的程序,Web处理请求,读写处理操作,I/O操作都可以充分利用并发增长处理速度,随着网络的普及,并发操作逐渐不可或缺
一、Goroutines
1、Goroutine的定义
在Golang中一个Goroutines就是一个执行单元,而每个程序都应该有一个主函数main也就是主Goroutines,从某种意义上你也可以把goroutine当作一个线程。
2、Goroutine的使用
Go语言中使用 goroutine 非常简单,只需要在函数或方法调用前加上go关键字就可以创建一个 goroutine,从而让该函数或方法在新创建的 goroutine 中执行。
go func
3、Goroutine的特点
在主Goroutine结束之后其他的所有Goroutine都会直接退出。例如:
import "fmt"
func main() {
go getnum([]int{
1, 5, 4, 84, 8, 4})
fmt.Println("程序已经结束啦")
}
func getnum(s []int) {
for _, v := range s {
fmt.Println(v)
}
}
//程序已经结束啦
如果让主Gororutines睡一会
package main
import (
"fmt"
"time"
)
func main() {
go getnum([]int{
1, 5, 4, 84, 8, 4})
time.Sleep(time.Second)
fmt.Println("程序已经结束啦")
}
func getnum(s []int) {
for _, v := range s {
fmt.Print(v)
}
}
//1548484程序已经结束啦
差别显而易见
4、多个Goroutine的使用
要想使用多个Goroutine可以使用到waitgroup
package main
import (
"fmt"
"sync"
)
// 声明全局等待组变量
var wg sync.WaitGroup
func worker(i int) {
fmt.Printf("%d 开始working...\n", i)
wg.Done() // 告知当前goroutine完成,释放资源
}
func main() {
for i := 0; i <= 10; i++ {
wg.Add(1) // 登记1个goroutine,开放一个进程供其使用
go worker(i)
}
wg.Wait() // 阻塞等待登记的goroutine完成,不然主Go结束了就都退出了
fmt.Println("程序结束了...")
/*
10 开始working...
2 开始working...
0 开始working...
4 开始working...
1 开始working...
7 开始working...
8 开始working...
5 开始working...
6 开始working...
9 开始working...
3 开始working...
程序结束了...
*/
}
其中Add,wait,Done的作用在代码块中已经描述出来了。
那么如果想让每个working有序的打印那么就只需要将wg.Wait放在for循环当中就可以了
但是这样子的话仔细思考一下,它和串行还有什么区别呢?那么想要真正做到并发且有序打印应该怎么做?
在学习笔记之go语句的执行规则中我有学习过
二、Channels
1、介绍
Go语言中goroutine是程序的并发体,而channel则是它们这些并发体的通信工具,它可以让不同的goroutine之间发送信息
2、 channel的使用
channel跟map类似的在使用之前都需要使用make进行初始化ch1 := make(chan int, 5)
未初始化的channel零值默认为nil
var ch chan int
fmt.Println(ch) // <nil>
channel也拥有close方法,当channel使用结束后就可以close释放掉channel
channel缓冲区
看这个程序:
package main
import "fmt"
func main() {
ch1 := make(chan int)
ch1 <- 5
rec := <-ch1
fmt.Println("ch1被接受,程序结束:rec:,", rec)
}
//fatal error: all goroutines are asleep - deadlock!
由于ch1没有缓冲区,channel没有缓冲区的话:
只有在有接收方能够接收值的时候才能发送成功,否则会一直处于等待发送的阶段。同理,如果对一个无缓冲通道执行接收操作时,没有任何向通道中发送值的操作那么也会导致接收操作阻塞。
如果想要运行成功那么在发送信息前就应该有另外的进程等待着接收
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
go receive(ch1)
ch1 <- 5
time.Sleep(time.Second)
}
func receive(ch1 chan int) {
for {
select {
case rec2 := <-ch1:
fmt.Println("ch1被接受,程序结束:rec:,", rec2)
}
}
}
//ch1被接受,程序结束:rec:, 5
但是如果有缓冲区就能避免程序阻塞,可以将发送的channel放在缓冲区直至有接收方将它接收
3、 单向通道
<- chan int // 只接收通道,只能接收不能发送
chan <- int // 只发送通道,只能发送不能接收
而对于close方法只能是发送通道拥有
4、select与channel配合使用
Select 的使用方式类似于 switch 语句,它也有一系列 case 分支和一个默认的分支。
每个 case分支会对应一个通道的通信(接收或发送)过程。select 会一直等待,直到其中的某个 case 的通信操作完成时,就会执行该 case分支对应的语句。
具体格式如下:
select {
case <-ch1:
//...
case rec := <-ch2:
//...
case ch3 <- 10:
//...
default:
//默认操作
}
使用select的话
可处理一个或多个 channel 的发送/接收操作。
如果多个 case 同时满足,select 会随机选择一个执行。
对于没有 case 的 select 会一直阻塞,可用于阻塞 main 函数,防止退出
三、并发锁
1、互斥锁
使用
var tex sync.Mutex
tex.Lock()
tex.Unlock()
当一个 goroutine 获取互斥锁之后,其他的 goroutine 将等待直至解锁。
2、读写锁
使用
var tex sync.RWMutex
tex.Lock()
tex.Unlock()
读写锁分为两种:读锁和写锁。
当一个 goroutine 获取到读锁之后,其他的 goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;而当一个 goroutine 获取写锁之后,其他的 goroutine无论是获取读锁还是写锁都会等待。
3、使用场景
如果并发过程中绝大部分都是读操作,那么使用读写锁将会优于互斥锁,如果读操作并不是特别多那将不会有太大差别
4、使用原因
多个 goroutine 同时操作一个资源(临界区)的情况,这种情况下就会发生竞态问题(数据竞态)
例如
package main
import (
"fmt"
"sync"
)
var (
x int64
wg sync.WaitGroup // 等待组
)
// add 对全局变量x执行5000次加1操作
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)
}
//结果都是小于5000的数字
为什么结果不是5000呢,因为在并发的过程有可能会出现多个go同时执行x=x+1
,这样只会有一个是有效的