十、Go协程的调度,互斥锁,计数器和线程池
发布日期:2021-07-01 02:08:07 浏览次数:2 分类:技术文章

本文共 3704 字,大约阅读时间需要 12 分钟。

@Author:Runsen

在字节面试中,我见过:GO语言中的协程与Python中的协程的区别?其实就是要我讲解Go中GMP机制。我表示很多都用过,但是底层不了解。

那时我只知道与传统的系统级线程和进程相比,协程的优势在于其“轻量级”,可以轻松创建上百万个而不会导致系统资源枯竭,而线程和进程通常不能超过1万个。所以协程也经常被称为轻量级线程。

在前面说过,Go编写一个并发编程程序很简单,只需要在函数之前使用一个Go关键字就可以实现并发编程。

func main() {
go func(){
fmt.Println("Hello,World!") }()}

Go语言使用一个Go关键字即可实现并发编程,但是Goroutine被调度到后端之后,具体的实现比较复杂。这里我也不知道很清楚吗,先看看调度器有哪几部分组成。Go的调度器通常被称为G-M-P模型。

G: Goroutine, 表示go协程M: Manager, 表示操作系统的线程P: Processor, 表示逻辑处理器

关于GMP底层原理,推荐文章和

因为这里涉及很复杂的东西,我真的写不出来这种水平,但这个真的是重点。

数据同步

之前说过,channle 是协程间通信主要方式。我们可以利用 channel 的阻塞特性来实现协程的数据同步。下面我们利用 channel 来实现典型的生产者和消费者模型,代码如下:

package mainimport (    "fmt")func Producer(ch chan int) {
for i := 1; i <= 5; i++ {
fmt.Println("Runsen搬砖挣了", i, "块钱") ch <- i } close(ch)}func Consumer(ch chan int) {
for {
value, ok := <-ch if ok {
fmt.Println("Runsen今天去嫖,花了", value, "块钱") } else {
fmt.Println("我去,竟然竟然没钱了!") break } }}func main() {
ch := make(chan int) go Producer(ch) Consumer(ch)}

具体输出如下,主要介绍的是多个 goroutine 间通过 channel 能很好地实现数据同步

Runsen搬砖挣了 1 块钱Runsen搬砖挣了 2 块钱Runsen今天去嫖,花了 1 块钱Runsen今天去嫖,花了 2 块钱Runsen搬砖挣了 3 块钱Runsen搬砖挣了 4 块钱Runsen今天去嫖,花了 3 块钱Runsen今天去嫖,花了 4 块钱Runsen搬砖挣了 5 块钱Runsen今天去嫖,花了 5 块钱我去,竟然竟然没钱了!

互斥锁

在协程中,有一个互斥锁Mutex。互斥锁,如果对一个已经上锁的对象再次上锁,那么就会导致该锁定操作被阻塞,直到该互斥锁回到被解锁状态。

假如现在有多个协程对同一个变量进行操作,如何确保每个协程都能拿到当前这个变量的最新结果是多线程并发应该要考虑到的问题

针对这种场景,我们可以使用互斥锁,利用加锁和解锁的特点来实现数据同步,代码如下:

package mainimport (    "fmt"    "sync")var counter int = 0func Count(lock *sync.Mutex) {
lock.Lock() counter++ fmt.Println(counter) lock.Unlock()}func main() {
lock := &sync.Mutex{
} for i := 0; i < 5; i++ {
go Count(lock) } for {
lock.Lock() c := counter lock.Unlock() if c >= 5 {
break } } fmt.Printf("counter is : %v", counter)}

具体输出如下,主要介绍的是&sync.Mutex{}利用互斥锁来实现数据同步

12345counter is :  5

计数器

WaitGroup 内部实现了一个计数器,用来记录未完成的操作个数,它提供了三个方法:

  • Add() 用来添加计数
  • Done() 用来在操作结束时调用,使计数减一,翻看源码可以看到,该方法的实现实际上就是调用 wg.Add(-1)
  • Wait() 用来等待所有的操作结束,即计数变为 0,该函数会在计数不为 0 时等待,在计数为 0 时立即返回

下面看一下使用 sync.WaitGroup 是如何实现协程同步的:

package mainimport (    "fmt"    "sync")func main()  {
var wg sync.WaitGroup wg.Add(2) // 因为有两个goroutine,所以增加2个计数 go func() {
fmt.Println("Goroutine 1") wg.Done() // 操作完成,减少一个计数 }() go func() {
fmt.Println("Goroutine 2") wg.Done() // 操作完成,减少一个计数 }() wg.Wait() // 等待,直到计数为0}Goroutine 1Goroutine 2

关于WaitGroup暂时介绍这么多。

常见的面试题

下面这是比较 常见的面试题。开启十个协程,实现0到9这十个数据自己和自己相加。(这十个几乎是同时执行的)

问题就是输出多少。

package mainimport (    "fmt"    "time")func Add(x, y int) {
z := x + y fmt.Print(z, "\t")}func main() {
for i := 0; i < 10; i++ {
go Add(i, i) } time.Sleep(2)}

运行结果每一次都是不一样的。

4	0	10	2	8	14	12	6	16	18	4	12	18	10	6	8	14	0	16	2

上面的代码中,我们使用go关键字声明一个Golang的协程Add,通过函数的值传递打印参数,运行程序会发现打印结果并不是按照顺序进行相加的,这是因为产生的协程并不是按照产生协程的顺序被调度的,这和协程、内核对象之间的竞争关系相关,每次打印的结果顺序是随机的。

线程池

在Java中有ThreadPoolExecutor线程池,来解决并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、还有死锁。

那么Golang语言的线程就是其goroutines,也肯定有goroutines线程池

首先定义工作goroutine池,内部定义了两个变量,一个是任务队列,一个是需启动goroutine的数量

type WorkerPool struct {
tasks <-chan *string //任务队列长度 poolSize int //启动goroutine的数目}

也可以&sync.Pool创建线程池,Go 在1.3版本 的sync包中加入一个新特性:Pool。

我们来看一个具体的示例,简单的数据存储。如果没有数据,返回线程池指定的数据0。

package main import (    "fmt"    "sync")func main() {
p:=&sync.Pool{
New: func() interface{
}{
return 0 }, } p.Put("Runsen") p.Put(123456) fmt.Println(p.Get()) //Runsen fmt.Println(p.Get()) //123456 fmt.Println(p.Get()) //0}

参考:http://topgoer.com

转载地址:https://maoli.blog.csdn.net/article/details/108060279 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:一、Git 多人协作模拟实战
下一篇:九、Golang并发和线程模型

发表评论

最新留言

网站不错 人气很旺了 加油
[***.192.178.218]2024年04月27日 18时18分44秒