十、Go协程的调度,互斥锁,计数器和线程池

举报
毛利 发表于 2021/07/15 01:49:45 2021/07/15
【摘要】 @Author:Runsen 在字节面试中,我见过:GO语言中的协程与Python中的协程的区别?其实就是要我讲解Go中GMP机制。我表示很多都用过,但是底层不了解。 那时我只知道与传统的系统级线程和进程相比,协程的优势在于其“轻量级”,可以轻松创建上百万个而不会导致系统资源枯竭,而线程和进程通常不能超过1万个。所以协程也经常被称为轻量级线程。 在前面说过,Go编...

@Author:Runsen

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

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

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

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

  
 
  • 1
  • 2
  • 3
  • 4
  • 5

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

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

  
 
  • 1
  • 2
  • 3

关于GMP底层原理,推荐文章go为什么这么快?(再探GMP模型)
[典藏版]Golang调度器GMP原理与调度全分析

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

数据同步

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

package main

import ( "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)
}


  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

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

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

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

互斥锁

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

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

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

package main

import ( "fmt" "sync"
)

var counter int = 0

func 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)
}


  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

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

1
2
3
4
5
counter is :  5

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

计数器

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

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

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

package main

import ( "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 1
Goroutine 2

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

关于WaitGroup暂时介绍这么多。

常见的面试题

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

问题就是输出多少。

package main

import ( "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)
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

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

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

  
 
  • 1
  • 2

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

线程池

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

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

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

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


  
 
  • 1
  • 2
  • 3
  • 4
  • 5

也可以&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
}


  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

参考:http://topgoer.com

文章来源: maoli.blog.csdn.net,作者:刘润森!,版权归原作者所有,如需转载,请联系作者。

原文链接:maoli.blog.csdn.net/article/details/108060279

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。