概述
初识协程
- 协程
- Golang中协程的特点
- 程序演示
- goroutine的调度模型
- 查询CPU逻辑个数与设置可使用的Cpu个数
- 案例演示
- Lock
- WaitGroup的使用
- channel(管道)
- channel的关闭
- channel的遍历
协程
1.协程是轻量级的线程,具体表现为逻辑态。编译器在底层做了优化。
2.主线程是一个物理的线程,直接作用在CPU上,重量级,非常消耗cpu资源
3.协程是从主线程开启的,是轻量级的线程,对资源的消耗相对较小
4.Golang可以轻松开启上万个协程。其他编程语言的并发机制是基于线程的,资源耗费大,这里就凸现出Golang在处理并发上面的优势。
Golang中协程的特点
- 有独立的栈空间
- 共享程序堆空间
- 调度由用户控制
- 协程是轻量级的线程
程序演示
实例1
package main
import (
"fmt"
"strconv"
"time"
)
func test() {
for i := 0; i < 10; i++ {
fmt.Println("Test() Hello World " + strconv.Itoa(i))
time.Sleep(time.Millisecond * 1000)
}
}
func main() {
go test() //开启一个协程
for i := 0; i < 10; i++ {
fmt.Println("Main() Hello Golang " + strconv.Itoa(i))
time.Sleep(time.Millisecond * 1000)
}
}
运行结果:
控制台打印:
Main() Hello Golang 0
Test() Hello World 0
Test() Hello World 1
Main() Hello Golang 1
Main() Hello Golang 2
Test() Hello World 2
Test() Hello World 3
Main() Hello Golang 3
Main() Hello Golang 4
Test() Hello World 4
Test() Hello World 5
Main() Hello Golang 5
Main() Hello Golang 6
Test() Hello World 6
Test() Hello World 7
Main() Hello Golang 7
Main() Hello Golang 8
Test() Hello World 8
Test() Hello World 9
Main() Hello Golang 9
执行流程图:
goroutine的调度模型
MPG模式基本介绍
M:(Machine)操作系统的主线程(是物理线程)
P:(Processor)协程执行所需要的上下文(协程运行时所需要的资源、环境)
G:(Goroutine)协程
文章1
文章2
查询CPU逻辑个数与设置可使用的Cpu个数
代码:
func CpuDemo() {
//搜索Cpu的逻辑个数
cpuNum := runtime.NumCPU()
fmt.Println("电脑CPU逻辑个数:", cpuNum)
//设置该程序最大可使用多少的cpu个数
runtime.GOMAXPROCS(cpuNum - 1)
fmt.Println("设置成功")
}
运行结果:
控制台打印:
电脑CPU逻辑个数: 4
设置成功
案例演示
使用goroutine计算从1到某个数的阶乘
代码演示:
package main
import (
"fmt"
"runtime"
"time"
)
var (
//定义一个全局map用来存放阶乘的计算结果
jiecheng = make(map[int]int,10)
)
func JieChengJisuan(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
jiecheng[n] = res
}
func main() {
//主线程休眠10秒(自己估计10秒钟内可以计算完成,当然这种方式是不可取的,后面会进行优化)
time.Sleep(time.Second * 10)
//计算从1到200的阶乘,方法为开启200个协程
for i := 1; i <= 200; i++ {
go JieChengJisuan(i)
}
//打印map中存入的值
for k, v := range jiecheng {
fmt.Printf("%d != %d n", k, v)
}
}
运行结果:
fatal error: concurrent map writes
fatal error: concurrent map writes
出现报错,主要是因为向map中写入数据时,出现争夺资源的问题
使用go build -race main.go命令,可以查看程序中的信息
之后运行main.exe
可以看到在最后一行打印,表示有3个数据竞争了资源
Found 3 data race(s)
为了解决这个问题,引出锁的概念
Lock
使用锁来对程序进行改进
代码如下:
package main
import (
"fmt"
"runtime"
"time"
//引入sync包
"sync"
)
var (
jiecheng = make(map[int]int, 10)
//Synchronized:同步
//Mutex:互斥
lock sync.Mutex
)
func JieChengJisuan(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
lock.Lock()
jiecheng[n] = res
lock.Unlock()
}
func main() {
time.Sleep(time.Second * 10)
for i := 1; i <= 200; i++ {
go JieChengJisuan(i)
}
lock.Lock()
for k, v := range jiecheng {
fmt.Printf("%d != %d n", k, v)
}
lock.Unlock()
}
运行结果:
184 != 0
92 != 0
145 != 0
151 != 0
181 != 0
33 != 3400198294675128320
77 != 0
132 != 0
141 != 0
154 != 0
8 != 40320
注意:有的数字的阶乘不正确,原因是阶乘的结果太大了,存不下,所以出现异常。
注:sync包提供了基本的同步基元,如互斥锁。除了Once和WaitGroup类型,大部分都是适用于低水平程序线程,高水平的同步使用channel通信更好一些。
本包的类型的值不应被拷贝。
WaitGroup的使用
前面的程序休眠10ms是自己估计的,很可能导致没有意义的等待。这里使用sync包下的WaitGroup来解决
下面的案例:计算100000以内的素数(开启100个协程)
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func Test(n int) {
for i := (n - 1) * 1000+1; i <= 1000*n; i++ {
if i>1 {
flag := true
for j := 2; j < i; j++ {
if i%j == 0 {
flag = false
}
}
if flag {
//fmt.Println(i,"是素数")
}
}
}
wg.Done()
}
func main() {
//记录程序开始时间
start := time.Now()
for i := 1; i <= 100; i++ {
wg.Add(1)
go Test(i)
}
wg.Wait()
//计算出程序运行时间
time := time.Since(start)
fmt.Println(time)
}
首先:定义全局变量wg
var wg sync.WaitGroup
第二步:每开启一个协程,令wg的计数加一
wg.Add(1)
第三步:在协程函数执行完毕,令wg的计数减一
wg.Done()
最后:主线程监视wg,Wait()阻塞主线程,直到接收到0时,才会继续执行
wg.Wait()
channel(管道)
前面使用全局变量加锁同步来解决goroutine的通讯,但是并不完美。
比如前面的程序执行完成的时间设置为10秒,这个时间完全是自己估计的,有可能实际运行时间比这个短,也有可能比这个长。
使用加全局锁来实现通讯,并不利于多个协程对全局变量的读写操作。
这里需要使用一个新的机制来实现通讯,需要使用到管道channel
- channel本质就是一个数据结构-队列
- 数据先进先出(FIFO)First in first out
- channel是线程是安全的,多个goroutine访问时,不需要加锁
- channel是有数据类型的,一个string类型的channel只能存放string类型的数据,必须存放指定的数据类型
channel简单的代码演示1
package main
import "fmt"
func main() {
//声明一个int类型的管道
var intChan chan int
//创建一个容量为3的int型管道
intChan = make(chan int, 3)
//查看管道是什么
fmt.Printf("管道是:%vn", intChan) //可以看到管道是一个地址
//向管道中存入一个整型数1
intChan <- 1
num1 := 2
//向管道中存入num1
intChan <- num1
//注意:向管道中存入数据时不能超过其容量
fmt.Printf("管道的长度是:%v,管道的容量是:%vn", len(intChan), cap(intChan))
//从管道中取出数据
num2 := <-intChan
num3 := <-intChan
fmt.Printf("取出的num2 = %v ,取出的num3 = %vn", num2, num3) //num2 = 1,num3 = 2
//注意:在没有使用协程的情况下,取完channel中的数据,再取数据的话就会报错dead lock
}
代码演示2:
func main() {
myChan := make(chan interface{}, 3)
var cat1 Cat
cat1 = Cat{
Name: "123",
Age: 13,
}
myChan <- 10
myChan <- "123"
myChan <- cat1
<-myChan
<-myChan
num3 := <-myChan
fmt.Printf("num3的数据类型是:%T", num3)
}
其中num3值类型的打印结果是
num3的数据类型是:main.Cat
如果我们在程序中想取得num3的Name值,直接用以下的代码是会提示错误的:
fmt.Printf("num3的数据类型是:%T,num3的Name值是:%v", num3, num3.Name)
原因是前面的管道定义的是interface{}类型的,而interface{}并不具有Name属性。所以想要拿到Name的值,就需要使用类型断言。但是前面的打印结果为什么是main.Cat类型的?原因为:程序在运行的时候才判断出来num3的类型,而程序在没有运行的时候是检测不出来的,所以会报错,不能编译。
使用类型断言来解决:
//断言
a := num3.(Cat)
fmt.Printf("num3的数据类型是:%T,num3的Name值是:%v", num3, a.Name)
channel的关闭
channel是可以进行关闭的
channel关闭后不可再向该管道中存入数据,但可以从该管道中取出数据
使用内置函数close()来进行管道的关闭
演示代码如下:
package main
import "fmt"
func main() {
myChan := make(chan int, 3)
myChan <- 1
myChan <- 2
//关闭管道
close(myChan)
num1 := <-myChan
fmt.Println("num1=", num1)
num2 := <-myChan
fmt.Println("num2=", num2)
myChan <- 3
}
输出结果:
num1= 1
num2= 2
panic: send on closed channel
goroutine 1 [running]:
main.main()
D:/GOProjects/VSCodePro/Study/Nove08/main/main.go:15 +0x215
exit status 2
前两个数据都取出来了
在第15行抛出一个panic,表示:向关闭的管道发送(存入)数据,这是不允许的
channel的遍历
注意:在进行channel遍历时需要使用for-range循环,使用for循环会出现失败的情况。
代码演示:
package main
import "fmt"
func main() {
myChan := make(chan int, 100)
for i := 0; i < 100; i++ {
myChan <- i * 2
}
for v := range myChan {
fmt.Println("v = ", v)
}
}
部分打印结果:
v = 192
v = 194
v = 196
v = 198
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
D:/GOProjects/VSCodePro/Study/Nove08/main/main.go:10 +0x147
exit status 2
出现这个错误的原因是:channel在遍历之前必须关闭,不然当channel中数据取完之后,还会接着从管道中取,导致错误。
修改代码:
package main
import "fmt"
func main() {
myChan := make(chan int, 100)
for i := 0; i < 100; i++ {
myChan <- i * 2
}
close(myChan)
for v := range myChan {
fmt.Println("v = ", v)
}
}
部分打印结果:
v = 190
v = 192
v = 194
v = 196
v = 198
最后
以上就是辛勤春天为你收集整理的Golang之goroutine(协程)与channel(管道)的全部内容,希望文章能够帮你解决Golang之goroutine(协程)与channel(管道)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复