@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底层原理,推荐文章go为什么这么快?(再探GMP模型)
和[典藏版]Golang调度器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 := <-chif 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 := counterlock.Unlock()if c >= 5 {break}}fmt.Printf("counter is :  %v", counter)
}

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

1
2
3
4
5
counter is :  5

计数器

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

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

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

package mainimport ("fmt""sync"
)func main()  {var wg sync.WaitGroupwg.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

关于WaitGroup暂时介绍这么多。

常见的面试题

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

问题就是输出多少。

package mainimport ("fmt""time"
)func Add(x, y int) {z := x + yfmt.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 mainimport ("fmt""sync"
)
func main() {p:=&sync.Pool{New: func() interface{}{return 0},}p.Put("Runsen")p.Put(123456)fmt.Println(p.Get()) //Runsenfmt.Println(p.Get())  //123456fmt.Println(p.Get())  //0
}

参考:http://topgoer.com

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

  1. linux 线程切换开销,协程 用户级(内核级)线程 切换开销 协程与异步回调的差异...

    今天先是看到多线程级别的内容,然后又看到协程的内容. 基本的领会是,协程是对异步回调方式的一种变换,同样是在一个线程内,协程通过主动放弃时间片交由其他协程执行来协作,故名协程. 而协程很早就有了,那时 ...

  2. 【并发编程二十】协程(coroutine)_协程库

    [并发编程二十]协程(coroutine) 一.线程的缺点 二.协程 三.优点 四.个人理解 五.协程库 1.window系统 2.unix系统(包括linux的各个版本) 2.1.makeconte ...

  3. 浅谈go协程及其调度模型

    每当我看着大海的时候,我总想找人谈谈.但当我和人交谈时,我又总想去看看大海.                                                              ...

  4. python多线程调度_python并发编程之进程、线程、协程的调度原理(六)

    进程.线程和协程的调度和运行原理总结. 系列文章 进程.线程的调度策略介绍 linux中的进程主要有三种调度策略: 优先级调度:将进程分为普通进程和实时进程: 先进先出(队列)调度:实时进程先创建的先 ...

  5. python协程是什么_在python中线程和协程的区别是什么

    在python中线程和协程的区别:1.一个线程可以拥有多个协程,这样在python中就能使用多核CPU:2.线程是同步机制,而协程是异步:3. 协程能保留上一次调用时的状态,每次过程重入时,就相当于进 ...

  6. python线程协程进程的区别_进程和线程、协程的区别

    现在多进程多线程已经是老生常谈了,协程也在最近几年流行起来.python中有协程库gevent,py web框架tornado中也用了gevent封装好的协程.本文主要介绍进程.线程和协程三者之间的区 ...

  7. 等待队列中为什么需要互斥锁?一个线程在等待时被唤醒后会做什么?安全队列的代码实现

    多线程2 同步 作用 条件变量及其接口 初始化 静态初始化 动态初始化 等待接口 唤醒接口 销毁接口 代码实现 参数为什么需要互斥锁 在调用该接口时,pthread_cond_wait函数的实现逻辑是 ...

  8. java 资源锁_concurrent包 线程池、资源封锁和队列、ReentrantReadWriteLock介绍

    jdk1.5后,提供了java.util.concurrent包,它可以实现线程池,你把线程当成普通对象就可以了,它来负责调度和执行 包括两类线程池 固定线程池 可变线程池 延迟线程池 固定线程池 p ...

  9. 蚂蚁三面题目(java开发岗):Java锁机制+JVM+线程池+事务+中间件

    一面 1.HashMap底层原理?HashTable和ConcurrentHashMap他们之间的相同点和不同点? 2.由上题提到锁的问题 3.MySQL的表锁&行锁&乐观锁& ...

最新文章

  1. 疯狂kotlin讲义连载之Kotlin的基础类型--null安全
  2. android sdk引入 微信分享_微信分享sdk接入总结
  3. RabbitMQ(三) ——发布订阅
  4. NYOJ-86 找球号(一)
  5. P2P打洞原理(二十二)
  6. springMVC常见问题
  7. matlab系统稳定性仿真实验,基于Matlab的电力系统暂态稳定仿真实验与分析
  8. 定制自己的Unity场景编辑工具界面(一)
  9. c语言题目详解——打印3的倍数的数
  10. 传说中的世界500强面试题-数学能力
  11. Win10正式版激活方法有哪些?如何激活Win10?
  12. MySQL启动常见错误:ERROR 2002 (HY000): Can‘t connect to local MySQL server through socket ‘/tmp/mysql.sock‘
  13. C语言模块化程序设计概念理解
  14. XPDL与WS-BPEL的比较之二:二者内容的大致概述
  15. 在ubuntu中查看摄像头
  16. python爬虫代理的使用_从零开始写Python爬虫 --- 2.4 爬虫实践:代理的爬取和验证...
  17. CentOS 8配置静态IP地址
  18. Java实现动态切换数据源
  19. 智安网络丨浅析如何加强个人信息安全防护
  20. “信息安全科普系列”——病毒与恶意代码

热门文章

  1. C语言操作符(又称运算符)(1)
  2. 判断字符串格式_Blind_pwn之格式化字符串
  3. mysql一对多前端实现_MySQL实现一对多查询的代码示例
  4. win7怎么桌面能不能设置html,win7系统怎么格式化
  5. COGS——T 8. 备用交换机
  6. 2015-2016 ACM-ICPC Northeastern European Regional Contest (NEERC 15)
  7. MariaDB exists 学习
  8. [Oracle] CPU/PSU补丁安装教程
  9. 为 pom.xml 添加组织,法律和开发人员信息
  10. 将SQL中数据输出到Excel中