Go-25 Go语言中goroutine channel 实现并发和并行
// goroutine channel 实现并发和并行
/*
相关概念如下:
二、进程、线程、以及并行、并发
1.关于进程和线程
【进程】是程序在操作系统的一次执行过程,是系统进行资源分配和调度的基本单位,进程是一个动态概念,是程序在执行过程中分配和管理资源的基本单位,每一个进程
都有一个自己的地址空间。一个进程至少有5种基本状态,他们是初始态,执行态,等待状态,就绪状态,终止状态。通俗的讲进程就是一个正在执行的程序;
【线程】是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位。一个进程可以创建多个线程,同一个进程中的多个线程可以并发执行,
一个程序要运行的话,至少有一个进程。 先开启进程,再开启线程。
2.关于并行和并发
【并发】:多个线程同时竞争一个位置,竞争到的才可以执行,每一个时间段只有一个线程在执行。
【并行】:多个线程可以同时执行,每一个时间段,可以有多个线程同时执行。
通俗的讲,多线程程序在单核CPU上面运行就是并发,多线程程序在多核CPU上运行就是并行,如果线程数大于CPU核数,则多线程程序在多个CPU上面运行既有并行又有并发。
【并发的特点】:1.多个任务作用在一个CPU上面。2.同一时间点只能由一个任务执行。3.同一时间段内执行多个任务。
【并行的特点】:1.多个任务作用在多个CPU上面。2.同一时刻执行多个任务。
三、Golang中的协程goroutine以及主线程
golang中的主线程:可以理解为线程/也可以理解为进程,在一个golang程序的主线程上可以起多个协程。golang中多协程可以实现并行或者并发。
协程:可以理解为用户级线程,这是对内核透明的,也就是系统并不知道有协程的存在,是完全由用户自己的程序进行调度的。golang的一个特色就是从语言层面原生支持协程,
在函数或者方法前面加go关键字就可以创建一个协程,可以说golang中的协程就是goroutine。
golang中主线程,可以开启多个协程。一个协程占用内存非常小,只有2KB左右,多协程goroutine切换调度开销方面远比线程要少,这也是为什么越来越多的大公司使用golang的原因之一。
四、goroutine的使用以及sync.WaitGroup
并行执行需求:
在主线程(可以理解成进程)中,开启一个goroutine,该协程每隔50毫秒输出“你好golang”在主线程中也每隔50毫秒输出“你好golang”,输出10次后,退出程序,要求主线程和goroutine同时执行。
package main
import (
"fmt"
"strconv"
"time"
)
func test(){
for i:=1;i<=10;i++{
fmt.Println("test() hello,world" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
func main(){
go test() // 开启了一个协程
for i:=1;i<=10;i++{
fmt.Println("main() hello,golang" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
上面代码看上去没有问题,但是要注意主线程执行完毕后即使协程没有执行完成,程序也会退出,所以我们需要对上面的代码进行改造。
【sync.WaitGroup可以实现主线程等待协程执行完毕。】
package main
import (
"fmt"
"strconv"
"sync"
"time"
)
var wg sync.WaitGroup // 1.定义全局的WaitGroup
func test(){
for i:=1;i<=10;i++{
fmt.Prrintln("test() 你好golang" + strconv.Itoa(i))
time.Sleep(time.Millisecond * 50)
}
wg.Done() // 4.goroutine结束就登记-1
}
func main(){
wg.Add(1) // 2.启动一个goroutine就登记+1
go test()
for i:=1;i<=2;i++{
fmt.Println("main() 你好golang" + strconv.Itoa(i))
time.Sleep(time.Millisecond * 50)
}
wg.Wait() // 3.等待所有登记的goroutine都结束
}
五、启动多个goroutine
在Go语言中实现并发就是这样简单,我们还可以启动多个goroutine。让我们再来一个例子:这里使用了sync.WaitGroup来实现等待goroutine执行完毕。
var wg sync.WaitGroup
func hello(i int){
defer wg.Done() // goroutine 结束就登记-1
fmt.Println("hello goroutine ", i)
}
func main(){
for i:=0;i<10;i++{
wg.Add(1) // 启动一个goroutine就登记+1
go hello(i)
}
wg.Wait() // 等待所有登记的goroutine都结束
}
多次执行上面的代码,会发现每次打印的数字的顺序都不一致,这是因为10个goroutine是并发执行的,而goroutine的调度是随机的。
六、设置golang并行运行的时候占用的CPU数量
Go运行的时候调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行GO代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go
代码同时调度到8个OS线程上。
Go语言中可以通过runtime.GOMAXPROCES()函数设置当前程序并发时占用的CPU裸机核心数。
Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。
package main
import (
"fmt"
"runtime"
)
func main(){
// 获取当前计算机上面的CPU个数
cpuNum := runtime.NumCPU()
fmt.Println("cpuNum=", cpuNum)
// 可以自己设置使用多个CPU
runtime.GOMAXPROCS(cpuNum-1)
fmt.Println("ok")
}
七、Goroutine 统计素数
需求:要统计1-120000的数字中哪些是素数?
1.通过传统的for循环来统计
func main(){
start := time.Now().Unix()
for num :=1;num<=120000;num++{
flag := true // 假设是素数
for i:=2; i<num;i++{
if num%i == 0{ // 说明该num不是素数
flag=false
break
}
}
if flag {
fmt.Println(num)
}
}
end := time.Now().Unix()
fmt.Println("普通的方法耗时=", end-start)
}
2.通过goroutine开启多个协程统计
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func fn1(n int){
for num :=(n-1) * 30000+1;num<=n*30000;num++{
flag := true // 假设是素数
for i:=2;i<num;i++{
if num%i == 0 {
flag=false
break
}
}
if flag{
fmt.Println(num)
}
}
wg.Done()
}
func main(){
start := time.Now.Unix()
for i:=1;i<=4;i++{
wg.Add(1)
go fn1(i)
}
wg.Wait()
end := time.Now().Unix()
fmt.Println("普通的方法耗时=",end-start)
}
【问题】:上面我们使用了goroutine已经能大大的提升性能了,但是如果我们想统计数据和打印数据同时进行,这个时候如何实现呢?这个时候我们就可以使用管道。
八、Channel管道
管道是golang在语言级别上提供的goroutine间的通讯方式,我们可以使用channel在多个goroutine之间传递消息。如果说goroutine是Go程序并发的执行体。
channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。
golang的并发模型是CSP,提倡通过通信共享内存而不是通过共享内存而实现通信。
Go语言中的管道channel是一种特殊的类型。管道像一个传送带或者队列,总是遵循先入先出的规则,保证收发数据的顺序。每一个管道都是一个具体类型的导管,
也就是声明channel的时候需要为其指定元素类型。
1、channel类型
channel是一种类型,一种引用类型。声明管道类型的格式如下:
var 变量 chan 元素类型
举几个例子:
var ch1 chan int // 声明一个传递整形的管道
var ch2 chan bool // 声明一个传递布尔型的管道
var ch3 chan []int // 声明一个传递int切片的管道
2、创建channel
声明的管道后需要使用make函数初始化之后才能使用。
创建channel的格式如下:
make(chan 元素类型, 容量)
举几个例子:
ch1 := make(chan int, 10) // 创建一个能存储10个int类型数据的管道
ch2 := make(chan bool, 4) // 创建一个能存储4个bool类型数据的管道
ch3 := make(chan []int, 3) // 创建一个能存储3个[]int切片类型数据的管道
3、channel操作
管道由发送send,接收receive和关闭close三种操作。
发送和接收都使用<-符号。
现在我们先使用以下语句定义一个管道:
ch := make(chan int, 3)
3.1、发送(将数据放在管道内)
将一个值发送到管道中。
ch <- 10 // 把10 发送到ch管道中
3.2、接收(从管道内取值)
从一个管道中接收值
x := <- ch // 从 ch 中接收值并赋值给变量x
<- ch // 从ch中接收值,忽略结果
3.3、关闭管道
我们通过调用内置的close函数来关闭管道
close(ch) // 关闭上面的ch管道
关于关闭管道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭管道。管道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,
在结束操作之后关闭文件是必须要做的,但关闭管道不是必须的。
关闭后的管道有以下特点:
1.对一个关闭的管道再发送值就会导致panic。
2.对一个关闭的管道进行接收会一直获取值,直到管道为空。
3.对一个关闭的并且没有值的管道执行接收操作会得到对应类型的零值。
4.关闭一个已经关闭的管道会导致panic。
4、管道阻塞
4.1.无缓冲的管道:
如果创建管道的时候没有指定容量,那么我们可以叫这个管道为无缓冲的管道。无缓冲的管道又称为阻塞的管道。我们来看一下下面的代码:
func main(){
ch := make(chan int)
ch <- 10
fmt.Println("发送成功")
}
上面这段代码能够通过编译,但是执行的时候会出现以下错误:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
D:/go_demo/main.go:10 +0x5b
exit status 2
4.2.有缓冲的管道
解决上面问题的方法还有一种就是使用有缓冲区的管道。我们可以在使用make函数初始化管道的时候为其指定管道的容量,例如:
func main(){
ch := make(chan int, 1) // 创建一个容量为1的有缓冲区管道
ch <- 10
fmt.Println("发送成功")
}
只要管道的容量大于0,那么该管道就是有缓冲的管道,管道的容量表示管道中能存放元素的数量。就像你小区的快递柜只有那么多个格子,格子满了就装不下了,就阻塞了,
等到别人取走一个快递员就能往里面再放一个。
管道阻塞具体代码如下:
func main(){
ch := make(chan int, 1)
ch <- 10
ch <- 12
fmt.Println("发送成功")
}
解决办法:
func main(){
ch := make(chan int, 1)
ch <- 10 // 放进去
<- ch // 取走
ch <- 12 // 放进去
<- ch // 取走
ch <- 17 //还可以放进去
fmt.Println("发送成功")
}
5、for range 从管道中循环取值
当向管道中发送完数据时,我们可以通过close函数来关闭管道。
当管道被关闭时,再往该管道发送值会引发panic,从该管道取值的操作会取完管道中的值,再然后取到的值一直都是对应类型的零值。拿如何判断一个管道是否被关闭了呢?
我们来看下面的这个例子:
package main
import "fmt"
// 循环遍历管道数据
func main(){
var ch1 = make(chan int, 5)
for i := 0;i<5;i++{
ch1 <- i+1
}
close(ch1) //关闭管道
// 使用for range 遍历管道,当管道被关闭的时候就会退出for range,如果没有关闭管道就会报个错误fatal error:all goroutines are asleep - deadlock!
// 通过for range 来遍历管道数据,管道没有key
for val := range ch1 {
fmt.Println(val)
}
}
结果中会取出来 1 2 3 4 5
从上面的例子中我们看到有两种方式再接收值的时候判断该管道是否被关闭,不过我们通常使用的是for range方式,使用for range遍历管道,当管道被关闭的时候就会退出for range循环
九、Goroutine结合Channel管道
需求1:定义两个方法,一个方法给管道里面写数据,一个给管道里面读取数据。要求同步进行。
1.开启一个fn1的协程给管道inChan中写入100条数据
2.开启一个fn2的协程读取inChan中写入的数据
3.注意:fn1和fn2同时操作一个管道
4.主线程必须等待操作完成后才可以退出
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func fn1(intChan chan int){
for i:=0;i<100;i++{
intChan <- i+1
fmt.Println("writeDate 写入数据--", i+1)
time.Sleep(time.Millisecond * 100)
}
close(intChan)
wg.Done()
}
func fn2(intChan chan int){
for v := range intChan{
fmt.Printf("readData 读到数据=%v \n", v)
time.Sleep(time.Millisecond * 50)
}
wg.Done()
}
func main(){
allChan := make(chan int, 100)
wg.Add(1)
go fn1(allChan)
wg.Add(1)
go fn2(allChan)
wg.Wait()
fmt.Println("......读取完毕......")
}
需求2:goroutine 结合channel实现统计1-120000的数字中哪些是素数?
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
// 向intChan放入1-120000个数
func putNum(intChan chan int){
for i:=1;i<=1000;i++{ // 此处用1000来举个例子
intChan <- i
}
// 关闭intChan
close(intChan)
wg.Done()
}
// 从intChan取出数据,并判断是否为素数,如果是,就放入到primeChan
func primeNum(intChan chan int,primeChan chan int,exitChan chan bool){
for num:=range intChan{
var flag bool=true
for i:=2;i<num;i++{
if num%i==0{ // 说明该num不是素数
flag=false
break
}
}
if flag{
// 将这个素数放入到primeChan
primeChan <- num // 函数执行完,此处将所有素数放入到这个管道中
}
}
exitChan <- true
// 每个协程执行完素数计算后向exitChan管道中放入一个bool类型的true。
// 如果primeNUm()函数开启了8个协程并全部执行完毕,那么exitChan管道中最后就会有8个true的数据。
// 如果exitChan管道中有8个数据了,那么primeChan 管道就可以close了,通过这个方式来判断什么时候关闭primeChan管道。
wg.Done()
}
// 打印素数的方法
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,20000) // 用来放入结果
// 标识退出的管道
exitChan :=make(chcan bool, 8) // 8个容量
// 开启一个协程,向intChan放入1-120000个数
wg.Add(1)
go putNum(intChan)
// 开启8个协程,从intChan取出数据,并判断是否为素数,如果是,就放入到primeChan中
for i:=0;i<8;i++{
wg.Add(1)
go primeNum(intChan,primeChan,exitChan)
}
// 打印素数
wg.Add(1)
go printPrime(primeChan)
// 判断什么时候退出
wg.Add(1)
go func(){
for i:=0;i<8;i++{
<-exitChan
}
// 当我们从exitChan取出了8个结果,就可以放心的关闭primeChan管道了
close(primeChan)
wg.Done()
}()
wg.Wait()
end:=time.Now().Unix()
fmt.Println("共计耗时=",end-start)
fmt.Println("main线程退出")
}
十、单向管道
有的时候我们会将管道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用管道都会对其进行限制,比如限制管道在函数中只能发送或只能接收。
例如:
// 1.在默认情况下,管道是双向的,可读可写
// var chan1 chan int // 可读可写
// 2.声明为只写的int类型管道
var chan2 chan<- int
chan2 = make(chan int, 3)
chan2 <- 20
// num := <-chan2 // error 这个是错误的,因为是只能写入的管道,不能读取
fmt.Println("chan2=", chan2)
// 3.声明为只读
var chan3 <-chan int
num2 := <-chan3
// chan3 <- 30 // error 这个是错误的,因为是只能读取的管道,不能写入
十、select多路复用
传统的方法在遍历管道时,如果不关闭会阻塞而导致deadlock,在实际开发中,可能我们不好确定什么时候关闭该管道。
你也许会写出如下代码使用遍历的方式来实现:
for{
// 尝试从ch1接收值
data,ok := <-ch1
// 尝试从ch2接收值
data,ok := <-ch2
...
}
这种方式虽然可以实现从多个管道接收值的需求,但是运行性能会差很多。为了应对这种场景,Go内置了select关键字,可以同时响应多个管道的操作。
select 的使用类似于switch语句,它有一系列case分支和一个默认的分支。每个case会对应一个管道的通信(接收或者发送)的过程。select会一直等待,
直到某个case的通信操作完成时,就会执行case分支对应的语句,具体格式如下:
select{
case <-ch1:
...
case data := <-ch2:
...
case ch3<-data:
...
default:
默认操作
}
举个小例子来演示下select的使用:
package main
import(
"fmt"
"time"
)
func main(){
// 使用select可以解决从管道取数据的阻塞问题,传统的方法在遍历管道时,如果不关闭会阻塞而导致deadlock,在实际开发中,可能我们不好确定什么时候关闭该管道。
// 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)
}
for{
select{
// 注意:这里,如果intChan一直没有关闭,不会一直阻塞而deadlock,会自动到下一个case匹配
case v:= <- intChan:
fmt.Printf("从intChan读取的数据%d \n", v)
time.Sleep(time.Second)
case v:= <- stringChan:
fmt.Printf("从stringChan读取的数据%s \n", v)
time.Sleep(time.Second)
default:
fmt.Printf("都取不到了,不玩了,程序员可以加入逻辑\n")
time.Sleep(time.Second)
return
}
}
}
使用select语句能够提高代码的可读性。
·可处理一个或多个channel的发送/接收操作。
·如果多个case同时满足,select会随机选择一个。
·对于没有case的select{}会一直等待,可用于阻塞main函数。
十、Golang并发安全和锁
需求:现在要计算1-60的各个数的阶乘,并且把各个数的阶乘放入到map中。最后显示出来。要求使用goroutine完成。
思路:
.编写一个函数,来计算各个数的阶乘,并放入到map中。
.启动多个协程,将统计的结果放入到map中。
只使用goroutine实现,运行的时候可能会出现资源争夺问题concurrent map writes:
package main
import (
"fmt"
"sync"
_"time"
)
var (
myMap = make(map[int]int)
wg sync.WaitGroup
)
// test 函数就是计算阶乘后将这个结果放入到myMap中
func test(n int){
res := 1
for i:=1;i<=n;i++{
res *=i
}
myMap[n]=res
wg.Done()
}
func main(){
for i:=1;i<=60;i++{
wg.Add(1)
go test(i)
}
wg.Wait()
for i,v := range myMap{
fmt.Printf("map[%d]=%d \n", i,v)
}
}
上面代码会出现资源争夺问题,多个协程在操作同一个map写入数据,造成异常。使用互斥锁来进行修复上面代码的问题。
10.1、互斥锁
互斥锁时一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync包的Mutex类型来实现互斥锁。
使用互斥锁来修复上面代码的问题:
package main
import (
"fmt"
"sync"
_"time"
)
var (
myMap = make(map[int]int)
wg sync.WaitGroup
lock sync.Mutex
)
// test 函数就是计算阶乘后将结果放入到myMap中
func test(n int){
res := 1
for i:=1;i<=10;i++{
res *= i
}
// 加锁
lock.Lock()
myMap[n] = res
//解锁
lock.Unlock()
wg.Done()
}
func main(){
for i:=1;i<=60;i++{
wg.Add(1)
go test(i)
}
wg.Wait()
for i,v := range myMap{
fmt.Printf("map[%d]=%d \n",i,v)
}
}
使用互斥锁能够保证同一时间有且只有一个goroutine进入临界区,其他的goroutine则在等待锁;当互斥锁释放后,等待的gproutine才可以获取锁进入临界区,
多个goroutine同时等待一个锁时,唤醒的策略是随机的。
虽然使用互斥锁能够解决资源争夺问题,但是并不完美,通过全局变量加锁同步来实现通讯,并不利于多个协程对全局变量的读写操作。
这个时候我们也可以通过另一种方式来实现上面的功能管道channel。
10.2、读写互斥锁
读写互斥锁非常适合多读少写的场景。
互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的。当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。
读写锁在Go语言中使用sync包中的RWMutex类型。
读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;
当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。
读写锁示例:
var (
x int64
wg sync.WaitGroup
lock sync.Mutex
rwlock sync.RWMutex
)
func write(){
// lock.Lock 加互斥锁
rwlock.Lock() // 加 写锁
x = x + 1
time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
rwlock.Unlock() // 解 写锁
// lock.Unlock() 解互斥锁
wg.Done()
}
func read(){
// lock.Lock() 加互斥锁
rwlock.RLock() // 加 读锁
time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
rwlock.RUnlock() // 解 读锁
// lock.Unlock() 解互斥锁
wg.Done()
}
func main(){
start := time.Now()
for i:=0;i<10;i++{ // 少写
wg.Add(1)
go write()
}
for i:=0;i<1000;i++{ // 多读
wg.Add(1)
go read()
}
wg.Wait()
end := time.Now()
fmt.Println("共计耗时=",end.Sub(start))
}
需要注意的是读写锁非常适合多读少写的场景,如果读和写的操作差别不大,读写锁的优势就发挥不出来。
十一、Goroutine Recover 解决协程中出现的Panic
package main
import (
"fmt"
"time"
)
// 函数
func sayHello(){
for i:=0;i<10;i++{
time.Sleep(time.Second)
fmt.Println("hello,world")
}
}
// 函数
func test(){
// 这里我们可以使用defer+recover
defer func(){
// 捕获test抛出的panic
if err:=recover();err != nil{
fmt.Println("test() 发生错误", err)
}
}()
// 定义了一个map
var myMap map[int]string
myMap[0] = "golang" // error,这里会引发panic异常,因为myMap只定义并没有初始化,所以直接赋值会引发错误。
正确的如下面所示:
// myMap := make(map[int]string)
// myMap[0] = "golang"
}
func main(){
go sayHello()
go test()
for i:=0;i<10;i++{
fmt.Println("main() ok=", i)
time.Sleep(time.Second)
}
}
end
*/
package main
import (
"fmt"
)
func main() {
var ch1 = make(chan int, 5)
for i := 0; i < 5; i++ {
ch1 <- i + 1
}
close(ch1) //关闭管道
for val := range ch1 {
fmt.Println(val)
}
println("******************************************")
// var chan3 <-chan int // 定义一个只读的int类型的管道
// num2 := <-chan3
// chan3 <- 30 // error 这个是错误的,因为是只能读取的管道,不能写入
// 初始化一个只读的管道
OnlyReadChan := make(<-chan int, 8)
fmt.Println("num2", OnlyReadChan) // num2 0xc00001c0c0
println("******************************************")
//var rwlock sync.RWMutex
//rwlock.Lock()
//rwlock.RLock()
//rwlock.RUnlock()
//rwlock.Unlock()
println("******************************************")
var myMap map[int]string
myMap[0] = "golang" // error,这里会引发panic异常,因为myMap只定义并没有初始化,所以直接赋值会引发错误。
}