线程(Thread)和协程(Goroutine)

4,404 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

单进程或者单线程工作的程序都会受限串行执行的先后顺序问题,特别是由于只有一个执行对象,就会有程序卡顿的现象。 现在流行的开发语言,都是支持多线程编程的。

线程(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

  1. M指的是Machine,一个M直接关联了一个内核线程。由操作系统管理。
  2. P指的是”processor”,代表了M所需的上下文环境,也是处理用户级代码逻辑的处理器。它负责衔接M和G的调度上下文,将等待执行的G与M对接。
  3. G指的是Goroutine,其实本质上也是一种轻量级的线程。包括了调用栈,重要的调度信息,例如channel等。

P的数量由环境变量中的GOMAXPROCS决定,通常来说它是和核心数对应, 例如在4Core的服务器上回启动4个线程。 G会有很多个,每个P会将Goroutine从一个就绪的队列中做Pop操作,为了减小锁的竞争, 通常情况下每个P会负责一个(Goroutine)队列。

实验证明

  1. 验证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;
    }
    

执行结果

thread1.jpg 开启了10个线程

thread2.jpg 实际执行的线程有18个,当然这面也包含了外部等待的线程

  1. 验证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)
    }
    

执行结果

mpg2.jpg 开启了10个协程(Goroutine)

mpg1.jpg 实际只有5个线程在执行,为啥不是一个线程?

golang的协程(Goroutine)是有MPG线程模型管理的,执行需要线程,管理需要线程,程序外部需要线程,所以就不会只有一个线程。 执行线程数少于执行的协程数就可以证明golang使用协程(Goroutine)减少了了线程切换的开销。 你可以增加并发的Goroutine数量效果应该会跟明显