本文已参与「新人创作礼」活动,一起开启掘金创作之路。
单进程或者单线程工作的程序都会受限串行执行的先后顺序问题,特别是由于只有一个执行对象,就会有程序卡顿的现象。 现在流行的开发语言,都是支持多线程编程的。
线程(Thread)的定义:
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
协程(Goroutine)的定义
协程不是进程或线程,其执行过程更类似于子例程,或者说不带返回值的函数调用。goroutine 是 Go语言中的轻量级线程实现,由 Go 运行时(runtime)管理。Go 程序会智能地将 goroutine 中的任务合理地分配给每个 CPU。
两者的关系
在Goroutine的定义中已经说的很清楚了:‘Goroutine 是 go语言实现的轻量级线程实现’,也就是说Goroutine 其实也是通过线程实现的。那google这个费心搞出一个Goroutine有什么好处呢?
嗯,这么说吧:Go 程序的高性能来自对 CPU cores 的充分利用,其最关键的优势就是 goroutine。 也就是说Go runtime 里的 Go scheduler 负责把 goroutine 调度到 thread 上执行。 OS scheduler 负责把 thread 调度到 CPU core 上执行。 区别就是 Go scheduler 做了一次调度,而Thread是直接让cpu调度。
让我们回想一下多线程编程最大的问题是什么? 是**线程切换 **
线程切换
线程切换(context switch,OS scheduler 让一个 core 从执行一个 thread 变为执行另一个 thread 的过程)
线程切换是有代价的,而且代价很大,每次切换耗时大约 1,000 ~ 1,500 纳秒,这些时间本可以用来执行 12,000~18,000 条 CPU 指令! 为什么么要切换?(cpu需要分出时间给每个线程时间来执行任务啊,不然你8核的电脑就只能8个线程工作了,计算机原理将的很清楚了,我就不复述了)。
线程切换开销大,不切换CPU的使用又不能达到最大效率 —— 尴尬。 所以Google搞出了协程(Goroutine)协程的调度方式是 non-preemptive 的 —— 也就是说一个 goroutine 干一段时间后要主动告诉 Go scheduler “我可以了,换别人吧”。Go scheduler 因此让执行这个 goroutine 的 thread 转而执行另一个 goroutine。正因为 goroutines 有着全局考虑的“协作精神”(collaborative),所以才会被叫做coroutine。 看出啥了么?线程没切换,但是任务换了,换掉的是Goroutine,Thread依然是原来的,这样就减少了线程切换的开销。
Go线程实现模型MPG
协程(Goroutine)为啥可以不换线程就实现执行任务的切换呢?来看一下Go线程实现模型MPG
- M指的是Machine,一个M直接关联了一个内核线程。由操作系统管理。
- P指的是”processor”,代表了M所需的上下文环境,也是处理用户级代码逻辑的处理器。它负责衔接M和G的调度上下文,将等待执行的G与M对接。
- G指的是Goroutine,其实本质上也是一种轻量级的线程。包括了调用栈,重要的调度信息,例如channel等。
P的数量由环境变量中的GOMAXPROCS决定,通常来说它是和核心数对应, 例如在4Core的服务器上回启动4个线程。 G会有很多个,每个P会将Goroutine从一个就绪的队列中做Pop操作,为了减小锁的竞争, 通常情况下每个P会负责一个(Goroutine)队列。
实验证明
- 验证java多线程先的线程生成数量
public static void main(String[] args) throws IOException { System.out.println("pid = "+getCurrentPid()); for(int i = 0;i<10;i++){ Thread t = new Thread(()->{ try { System.out.println("Thread:"+Thread.currentThread().getName()+" pid = "+getCurrentPid()); Thread.sleep(100*1000); } catch (InterruptedException e) { e.printStackTrace(); } }); t.start(); } System.in.read(); } public static String getCurrentPid() { String OS_NAME = System.getProperty("os.name"); if (!OS_NAME.startsWith("Windows")) return ""; // 获取pid String name = ManagementFactory.getRuntimeMXBean().getName(); // get pid String pid = name.split("@")[0]; return pid; }
执行结果
开启了10个线程
实际执行的线程有18个,当然这面也包含了外部等待的线程
- 验证golang多协程下的线程生成数量
package main import ( "log" "os" "runtime" "time" ) func main() { runtime.GOMAXPROCS(1) log.Printf("start pid:%d\n", os.Getpid()) for i := 0; i < 10; i++ { go func() { log.Printf("start %d pid:%d\n",i, os.Getpid()) time.Sleep(100 * time.Second) }() } time.Sleep(200 * time.Second) }
执行结果
开启了10个协程(Goroutine)
实际只有5个线程在执行,为啥不是一个线程?
golang的协程(Goroutine)是有MPG线程模型管理的,执行需要线程,管理需要线程,程序外部需要线程,所以就不会只有一个线程。 执行线程数少于执行的协程数就可以证明golang使用协程(Goroutine)减少了了线程切换的开销。 你可以增加并发的Goroutine数量效果应该会跟明显