goroutine

并发与并行

Go语言相对于其他语言的最大一个特色就是支持高并发编程模式。Goroutine(协程)是Go中最基本的执行单元。事实上每一个Go程序至少有一个Goroutine:主Goroutine。当程序启动时,它会自动创建。

为了更好理解Goroutine,我们需要了解并发和并行的区别

  • 并发:逻辑上具备同时处理多个任务的能力。
  • 并行:物理上在同一时刻执行多个并发任务。

简单来说,并发是在同一时间处理多件事情。并行是在同一时间做多件事情。并发的目的在于把当个 CPU 的利用率使用到最高。并行则需要多核 CPU 的支持。

如图1:


goru.jpeg

线程与协程

Go 语言在语言层面上支持了并发,goroutine是Go语言提供的一种用户态线程,有时我们也称之为协程。下面我们了解下协程和线程。

  • 进程:拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度。
  • 线程:拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度。
  • 协程 :和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度。

在操作系统的OS Thread和编程语言的User Thread之间,实际上存在3种线程对应模型,也就是:1:1,1:N,M:N。

  • 1:1:一个用户线程就只在一个内核线程上跑,这时可以利用多核,但是上下文切换很慢,切换效率很低。
  • N:1:多个(N)用户线程始终在一个内核线程上跑,context上下文切换很快,但是无法真正的利用多核。
  • M:N:多个goroutine在多个内核线程上跑,这个可以集齐上面两者的优势,既能快速切换上下文,也能利用多核的优势,而Go正是选择这种实现方式。

简单将 goroutine归纳为协程并不合适。运行时会创建多个线程来执行并发任务,且任务单元可被调度到其他线程并行执行。这更像是多线程和协程的综合体,能最大限度提升执行效率,发挥多核处理能力。

MPG模型

我们可以创建很多的goroutine,并且它们跑在同一个内核线程之上的时候,就需要一个调度器来维护这些goroutine,确保所有的goroutine都能使用cpu,并且是尽可能公平地使用cpu资源。

Go语言中调度器的主要有4个重要部分,分别是M、G、P、前三个定义在runtime.h中,Sched定义在proc.c中。

  • M (work thread) 代表了系统线程OS Thread,由操作系统管理。

  • P (processor) 衔接M和G的调度上下文,它负责将等待执行的G与M对接。P的数量可以通过GOMAXPROCS()来设置,它其实也就代表了真正的并发度,即有多少个goroutine可以同时运行。

  • G (goroutine) goroutine的实体,包括了调用栈,重要的调度信息,例如channel等。

Sched是调度实现中使用的数据结构,大多数需要的信息都已放在了结构体M、G和P中,Sched结构体只是一个壳。Sched结构体中的Lock是非常必须的,如果M或P等做一些非局部的操作,它们一般需要先锁住调度器。
如图2:


mpg.jpeg

优点:

  • 内存消耗更少:Goroutine所需要的内存通常只有2kb,而线程则需要1Mb
  • 创建与销毁的开销更小:由于线程创建时需要向操作系统申请资源,并且在销毁时将资源归还,因此它的创建和销毁的开销比较大。相比之下,goroutine的创建和销毁是由go语言在运行时自己管理的,因此开销更低。
  • 切换开销更小线程的调度方式是抢占式的,如果一个线程的执行时间超过了分配给它的时间片,就会被其它可执行的线程抢占;而goroutine的调度是协同式的,它不会直接地与操作系统内核打交道。

缺点:

  • 协程调度机制无法实现公平调度:因为协程的调度是非入侵式的,系统不会为他分配资源。

使用

package main

import (
    "fmt"
    "sync"
    "time"
)

var ln = 100000

func main()  {
    goRou()
    goLoo()
}

func goRou()  {
    var wg = &sync.WaitGroup{}
    rn := 2
    ix := ln / rn

    startT := time.Now()
    for i := 0; i < rn ; i++ {
        wg.Add(1)
        go func(idx int) {
            defer wg.Done()
            for g := 0; g < ix; g++ {

            }
        }(i)
    }

    wg.Wait()

    tc := time.Since(startT)    //计算耗时
    fmt.Printf("time cost = %v\n", tc)
}

func goLoo(){
    startT := time.Now()

    for g := 0; g < ln; g++ {

    }

    tc := time.Since(startT)    //计算耗时
    fmt.Printf("time cost = %v\n", tc)
}
package main

import (
    "fmt"
    "time"
)

func newTask() {
    i := 0
    for {
        i++
        fmt.Printf("new goroutine: i = %d\n", i)
        time.Sleep(1 * time.Second) //延时1s
    }
}

func main() {
    //创建一个 goroutine,启动另外一个任务
    go newTask()

    i := 0
    //main goroutine 循环打印
    for {
        i++
        fmt.Printf("main goroutine: i = %d\n", i)
        time.Sleep(1 * time.Second) //延时1s
    }
}
package main

import (
    "fmt"
    "runtime"
)

func main() {
    //创建一个goroutine
    go func(s string) {
        for i := 0; i < 2; i++ {
            fmt.Println(s)
        }
    }("world")

    for i := 0; i < 2; i++ {
        runtime.Gosched() //import "runtime"
        /*
           屏蔽runtime.Gosched()运行结果如下:
               hello
               hello

           没有runtime.Gosched()运行结果如下:
               world
               world
               hello
               hello
        */
        fmt.Println("hello")
    }
}
package main

import (
    "fmt"
    "runtime"
)

func main() {
    go func() {
        defer fmt.Println("A.defer")

        func() {
            defer fmt.Println("B.defer")
            runtime.Goexit() // 终止当前 goroutine, import "runtime"
            fmt.Println("B") // 不会执行
        }()

        fmt.Println("A") // 不会执行
    }() //别忘了()

    //死循环,目的不让主goroutine结束
    for {
    }
}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容