Golang并发
golang并发
goroutine
并发 多线程单cpu上运行,同一时间内只有一个进程执行
并行 多线程多cpu运行 同一时刻多个进程一起执行
如果线程数大于cpu数 既有并行又有并发
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
var wg sync.WaitGroup
func test() {
for i := 0; i < 10; i++ {
fmt.Println("t", i)
time.Sleep(time.Second * 2)
}
wg.Done() //协程计数器-1
}
func main() {
cpuNum := runtime.NumCPU() //获取CPU个数
runtime.GOMAXPROCS(4) //设置可以使用的cpu个数
wg.Add(1) //协程计数器+1
go test()
for i := 0; i < 10; i++ {
fmt.Println(i)
time.Sleep(time.Second)
}
wg.Wait() //等待协程执行完毕
}
管道
var c1 chan int //声明一个传递int类型的管道
var c2 chan string //声明一个传递string类型的管道
创建channel
make (chan 元素类型,容量)
ch := make(chan,int,3)
ch <- 10 //把10发送到ch
x := <-ch //从管道取值
<-ch //取值但不赋值
close(ch) //关闭管道
管道循环
var ch1 = make(chan int,10)
for i :=1;i<=10,i++ {
ch1 <- i
}
close(ch1)
//管道没有key
for v := range ch1 {
fmt.Println(v)
}
//for循环遍历管道无需关闭 ,for range需要关闭
管道结合goroutine
//打印1-12000之间的素数
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
// 向 intChan放入 1-120000个数
func putNum(intChan chan int) {
for i := 2; i < 120000; i++ {
intChan <- i
}
close(intChan)
wg.Done()
}
// 从 intChan取出数据,并判断是否为素数,如果是,就把得到的素数放在primeChan
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
for num := range intChan {
var flag = true
for i := 2; i < num; i++ {
if num%i == 0 {
flag = false
break
}
}
if flag {
primeChan <- num //num是素数
}
}
//要关闭 primeChan
// close(primeChan) //如果一个channel关闭了就没法给这个channel发送数据了
//什么时候关闭primeChan?
//给exitChan里面放入一条数据
exitChan <- true
wg.Done()
}
// printPrime打印素数的方法
func printPrime(primeChan chan int) {
for v := range primeChan {
fmt.Println(v)
}
wg.Done()
}
func main() {
start := time.Now().Unix()
intChan := make(chan int, 1000)
primeChan := make(chan int, 50000)
exitChan := make(chan bool, 8) //标识primeChan close
//存放数字的协程
wg.Add(1)
go putNum(intChan)
//统计素数的协程
for i := 0; i < 8; i++ {
wg.Add(1)
go primeNum(intChan, primeChan, exitChan)
}
//打印素数的协程
wg.Add(1)
go printPrime(primeChan)
//判断exitChan是否存满值
wg.Add(1)
go func() {
for i := 0; i < 8; i++ {
<-exitChan
}
//关闭primeChan
close(primeChan)
wg.Done()
}()
wg.Wait()
end := time.Now().Unix()
fmt.Println("执行完毕....", end-start, "毫秒")
}
单向管道
读写管道
c :=make(chan int,2)
单向管道只读
c := make(<-chan int,2)
单向管道只写
c := make(chan<- int,2)
select多路复用
select语句是一种用于处理并发操作的重要机制。它允许在多个通信操作中选择一个可用的操作进行执行,从而实现对并发任务的控制
select语句由case子句组成,每个case子句描述一个通信操作。其基本语法如下
package main
import (
"fmt"
"time"
)
func main() {
// 在某些场景下我们需要同时从多个通道接收数据,这个时候就可以用到golang中给我们提供的select多路复用
//1.定义一个管道 10个数据int
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i
}
//2.定义一个管道 5个数据string
stringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
stringChan <- "hello" + fmt.Sprintf("%d", i)
}
//使用select来获取channel里面的数据的时候不需要关闭channel
for {
select {
case v := <-intChan:
fmt.Printf("从 intChan 读取的数据%d\n", v)
time.Sleep(time.Millisecond * 50)
case v := <-stringChan:
fmt.Printf("从 stringChan 读取的数据%v\n", v)
time.Sleep(time.Millisecond * 50)
default:
fmt.Printf("数据获取完毕")
return //注意退出...
}
}
}
panic
在Go语言中,panic是一种用于处理程序错误和异常情况的机制。当程序遇到无法处理的错误或异常时,可以触发panic,并停止当前函数的执行,然后逐层向上返回,直到被recover捕获或程序终止
可以使用panic关键字来触发panic panic(value)
value:可以是任意类型的值,表示触发panic时传递的信息。
当程序遇到无法恢复的错误时,可以使用panic中断程序的正常执行流程。这些错误可能包括非法参数、不可预料的状态或不可修复的运行时错误。通过触发panic,可以停止当前函数的执行并向上传播错误信息。
func divide(a, b int) int {
if b == 0 {
panic("除数不能为零")
}
return a / b
}
在一些特殊情况下,当发生错误时,我们希望程序能够立即停止,并输出错误信息,而不是继续执行下去。这种情况下,可以使用panic来触发错误,然后在程序的顶层函数中使用recover来捕获并处理这些错误。
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("发生错误:", err)
}
}()
// 一些可能触发panic的操作
}
互斥锁和读写锁
互斥锁
Mutex是互斥锁的意思,也叫排他锁,同一时刻一段代码只能被一个线程运行,使用只需要关注方法Lock(加锁)和Unlock(解锁)即可。
在Lock()和Unlock()之间的代码段称为资源的临界区(critical section),是线程安全的,任何一个时间点都只能有一个goroutine执行这段区间的代码。
package main
//go build -race main.go 编译后运行查看
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
var mutex sync.Mutex
var m = make(map[int]int, 0)
func test(num int) {
mutex.Lock()
var sum = 1
for i := 1; i <= num; i++ {
sum *= i
}
m[num] = sum
// fmt.Println(m[num])
fmt.Printf("key=%v value=%v\n", num, sum)
time.Sleep(time.Millisecond)
mutex.Unlock()
wg.Done()
}
func main() {
for r := 0; r < 40; r++ {
wg.Add(1)
go test(r)
}
wg.Wait()
}
读写锁
Mutex在大量并发的情况下,会造成锁等待,对性能的影响比较大。
如果某个读操作的协程加了锁,其他的协程没必要处于等待状态,可以并发地访问共享变量,这样能让读操作并行,提高读性能。
RWLock就是用来干这个的,这种锁在某一时刻能由什么问题数量的reader持有,或者被一个writer持有
主要遵循以下规则 :
- 读写锁的读锁可以重入,在已经有读锁的情况下,可以任意加读锁。
- 在读锁没有全部解锁的情况下,写操作会阻塞直到所有读锁解锁。
- 写锁定的情况下,其他协程的读写都会被阻塞,直到写锁解锁。
Go语言的读写锁方法主要有下面这种
- Lock/Unlock:针对写操作。
不管锁是被reader还是writer持有,这个Lock方法会一直阻塞,Unlock用来释放锁的方法 - RLock/RUnlock:针对读操作
当锁被reader所有的时候,RLock会直接返回,当锁已经被writer所有,RLock会一直阻塞,直到能获取锁,否则就直接返回,RUnlock用来释放锁的方法
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
var mutex sync.RWMutex
//写的方法
func write() {
mutex.Lock()
fmt.Println("执行写操作")
time.Sleep(time.Second * 2)
mutex.Unlock()
wg.Done()
}
//读的方法
func read() {
mutex.RLock()
fmt.Println("---执行读操作")
time.Sleep(time.Second * 2)
mutex.RUnlock()
wg.Done()
}
func main() {
//开启10个协程执行读操作
for i := 0; i < 10; i++ {
wg.Add(1)
go write()
}
// 开启10个协程执行写操作
for i := 0; i < 10; i++ {
wg.Add(1)
go read()
}
wg.Wait()
}