本文主要介绍Go语言中goroutine。文中如有描述不对或则不合理的地方,请各位大佬积极留言,我会每日及时查看并核查纠正。
并发&并行
日常开发过程中沟通的
并发,是指基于并行机制下的并发。
-
并发: 任务数多余CPU核数,通过操作系统的各种任务调度算法实现多个任务一起执行;实际上任务之间切换执行;比如:单核CPU上的多任务系统。 -
并行: 任务数小于等于CPU核数,即任务真的一起执行的;前提条件必须是多核CPU。
进程&线程&协程
-
进程: 一个程序在一个数据集中的一次动态执行过程,可以简单理解为“正在执行的程序”,它是CPU资源分配和调度的独立单位。- 进程一般由
程序、数据集、进程控制块三部分组成程序用来描述进程要完成哪些功能以及如何完成;数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。
- 进程的局限是创建、撤销和切换的开销比较大。
- 进程一般由
-
线程: 操作系统能够进行运算调度的最小单位,它被包含在进程中,是进程的实际运作单位。-
它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。一个进程可以包含多个线程。
-
线程的优点是减小了程序并发执行时的开销,提高了操作系统的并发性能,缺点是线程没有自己的系统资源,只拥有在运行时必不可少的资源,但同一进程的各线程可以共享进程所拥有的系统资源,如果把进程比作一个车间,那么线程就好比是车间里面的工人。不过对于某些独占性资源存在锁机制,处理不当可能会产生“死锁”。
-
线程解决了问题-
最早的
通用网关接口(CGI)程序很简单,通过脚本将原来单机版的程序包装在一个进程里,来一个用户就启动一个进程。但明显承载不了多少用户,并且如果进程间需要共享资源还得通过进程间通信机制。 -
(图形用户界面)GUI出现后急切需要并发机制来保证用户界面的响应
-
-
线程增加了复杂度问题-
竞态条件(race conditions): 线程之间的任务总有一些资源需要共享。 -
依赖关系以及执行顺序: 线程之间的任务有依赖关系,需要等待以及通知机制来进行协调,保证顺序。
-
-
解决所带来的复杂度问题-
线程锁(互斥锁,Mutex): -
信号量(semaphore):
-
-
线程的成本-
内存: 线程的栈空间 -
调度成本(context-switch): 切换成本和线程的栈空间使用大小有直接关系
-
-
如何控制系统的线程个数-
线程池: -
设置和CPU核数相等的线程数,保证线程一直处于运行状态:-
异步回调方案: -
GreenThread/Coroutine/Fiber方案:
-
-
-
-
协程: 是一种用户态的轻量级线程,又称微线程,英文名Coroutine,协程的调度完全由用户控制。人们通常将协程和子程序(函数)比较着理解。- 子程序调用总是一个入口,一次返回,一旦退出即完成了子程序的执行。
CSP
与主流语言通过共享内存来进行并发控制方式不同,Go 语言采用了 CSP 模式。这是一种用于描述两个独立的并发实体通过共享的通讯 Channel(管道)进行通信的并发模型。
goroutine
调度器不能保证多个goroutine执行顺序,且进程退出后不会等待它们结束。
func test0001() {
for i := 0; i < 10; i++ {
// 创建10个子goroutine
go func(i int) {
// 打印结果是: 乱序打印0,1,2,3,4,5,6,7,8,9
fmt.Println(i)
}(i)
}
// 延迟主goroutine执行时间,等待子goroutine执行完成
time.Sleep(time.Millisecond)
}
channel
Do not communicate by sharing memory; instead, share memory by communicating. 不要通过共享内存来通信,而要通过通信来实现内存共享
-
goroutine之间的通道,它可以让goroutine之间相互通信。 -
Go从语言层面保证同一个时间只有一个goroutine能够访问channel里面的数据,为开发者提供了一种优雅简单的工具,所以Go的做法就是使用channel来通信,通过通信来传递内存数据,使得内存数据在不同的goroutine中传递,而不是使用共享内存来通信。
-
信道
-
无缓冲信道: 同步模式,没有能力保存任何值。这种类型的信道只有发送和接收方同时准备好,才能进行下次信道的操作,否则阻塞状态。// 接收方 <说明> 参数ch: 仅仅只能用于接收数据 func receive(ch <-chan int) { for { // 顺序打印 1, 2, 3 fmt.Printf("ch received data of %d \n", <-ch) } } func test00015() { ch := make(chan int) go receive(ch) ch <- 1 ch <- 2 ch <- 3 time.Sleep(time.Millisecond) }引发死锁问题
// 接收方 func receive(ch <-chan int) { for { fmt.Printf("ch received data of %d \n", <-ch) } } func test00016() { ch := make(chan int) // 向ch发送3个数据: 准备发送第一个数据,阻塞状态,检测到为无缓冲信道,并且没有其他goroutine运行接收方,故直接deadlock ch <- 1 ch <- 2 ch <- 3 go receive(ch) } -
有缓冲信道: 异步模式,被创建时就开辟能存储n个值的信道。这种类型并不要求发送与接收方同时进行,只要缓冲区有未使用空间用于发送数据,或还包含可以接收的数据,那么其通信就会无阻塞地进行。
-
-
创建channel:
ch := make(chan int)
申明方式
chan T // 声明一个双向通道
chan<- T // 声明一个只能用于发送的通道
<-chan T // 声明一个只能用于接收的通道
比较channel:
func test0004() {
ch01 := make(chan int)
ch02 := make(chan int)
fmt.Println(ch01 == ch02) // false
var ch03 chan int
fmt.Println(ch03 == nil) // true
}
接收数据
func worker3(ch chan int) {
for data := range ch {
fmt.Printf("receive %c\n", data)
}
}
传递
package main
import (
"fmt"
)
func main() {
ch1 := make(chan int)
fmt.Printf("%T,%p\n",ch1,ch1)
test1(ch1)
}
func test1(ch chan int){
fmt.Printf("%T,%p\n",ch,ch)
}
关键源码路径
分析
- 现象一: 睡1秒,资源交给
receq goroutine先执行 <-ch,没有sendq,阻塞,资源交出,给main goroutine-
ch <- 100后确定有receq,所以唤醒recvq,但是不一定是让receq goroutine其执行;存在资源竞争,下一个资源交给main还是recvq不一定 -
有可能是
receq goroutine拿到资源,打印receive data -
有可能是
main goroutine拿到资源,打印 main goroutine
-
- 现象二: 先执行
ch <- 10,没有receq,阻塞,资源交出,给其他goroutine,但是这个案例只有一个goroutine,即receq goroutine,所以交给它,睡眠1秒;资源交给main goroutine吗?不行,因为main goroutine已经阻塞,不满足唤醒条件;只能在当前goroutine继续等待,直到1秒结束- 执行
<-ch,确定有sendq,唤醒main goroutine;只是一个唤醒操作,下一步还是存在资源竞争,即当前资源交出,开始竞争 - 可能
main goroutine拿到,打印 main goroutine - 可能
继续它拿到,收到数据,打印receive data
- 执行
-
deadlock,因为ch <- 100,调用chansend,没有receq,资源交出,但是由于当前仅仅只有一个main goroutine,交出不了;直接deadlock。