我是靠谱客的博主 辛勤春天,最近开发中收集的这篇文章主要介绍Golang之goroutine(协程)与channel(管道),觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

初识协程

    • 协程
    • 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(管道)所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(55)

评论列表共有 0 条评论

立即
投稿
返回
顶部