Goroutine(协程)的理解


title: Goroutine(协程)的理解
tags: Go,Goroutine
author: Clown95


并发概念

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

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

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

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

线程和协程

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等做一些非局部的操作,它们一般需要先锁住调度器。

使用goroutine

只须在函数调用前添加go关键字即可创建并发任务,关键字go并非执行并发操作,而是创建一个并发任务单元。新建任务被放置在系统队列中、等待调度器安排合适系统线程去获取执行权。

比如说,第一种方案,商场雇佣1000个导购员进行一对一服务,这种肯定是很高效的,但是造成资源大量浪费,而且管理困难。
第二种方案,商场雇佣20个导购员,对需要咨询的顾客进行处理,处理完,在处理剩下的顾客。

我们用代码来简单的演示一遍:

func loop() {
    for i := 0; i <10 ; i++ {
        fmt.Printf("%d ", i)
    }
}
func main() {
   go loop() // 启动一个goroutine
    loop()
}

我们看下运行结果。

第一次:0 1 2 3 4 5 6 7 8 9 0 1 

第二次:0 1 2 3 4 5 6 7 8 9 0 

第三次:0 1 2 3 4 5 6 7 8 9 0 1 2 

我们发现每次运行的结果都不同,这是因为goroutine当前流程不会阻塞,不会等待该任务启动,且运行时也不保证并发任务的执行次序。

总结

许多人可能认为goroutine比线程运行的更快,这是一个误解goroutine并不会更快,它只是增加了更多的并发性。当一goroutine被阻塞(比如等待IO),golang的调度器会调度其它可以执行的goroutine运行。与线程相比,它有以下几个优点:

优点:

  • 内存消耗更少:Goroutine所需要的内存通常只有2kb,而线程则需要1Mb

  • 创建与销毁的开销更小:由于线程创建时需要向操作系统申请资源,并且在销毁时将资源归还,因此它的创建和销毁的开销比较大。相比之下,goroutine的创建和销毁是由go语言在运行时自己管理的,因此开销更低。

  • 切换开销更小线程的调度方式是抢占式的,如果一个线程的执行时间超过了分配给它的时间片,就会被其它可执行的线程抢占;而goroutine的调度是协同式的,它不会直接地与操作系统内核打交道。

缺点:

  • 协程调度机制无法实现公平调度:因为协程的调度是非入侵式的,系统不会为他分配资源。
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
当我们需要进行并发编程时,通常会使用线程或进程来实现。但是,线程和进程的开销比较大,而且容易发生死锁、竞态条件等问题。Golang 提供了协程Goroutine)和通道(Channel)等并发机制,可以轻松实现高效的并发编程。 协程Goroutine)是一种轻量级线程,由 Golang 运行时管理。与线程相比,协程的开销非常小,可以轻松创建数以千计的协程,并发执行任务。协程之间通过通道(Channel)进行通信,以实现数据共享和同步。 通道(Channel)是一种特殊的数据类型,用于协程之间的通信。通道可以用于发送和接收数据,通道的发送和接收操作是原子性的,因此可以保证数据的同步和安全。通道有两种类型:有缓冲通道和无缓冲通道。有缓冲通道可以缓存一定数量的数据,但发送和接收操作可能会发生阻塞;无缓冲通道必须有发送和接收操作同时进行,否则会发生阻塞。 以下是一个简单的示例,演示了如何使用协程和通道实现并发处理任务: ```go func worker(id int, jobs <-chan int, results chan<- int) { for j := range jobs { fmt.Printf("worker %d started job %d\n", id, j) time.Sleep(time.Second) fmt.Printf("worker %d finished job %d\n", id, j) results <- j * 2 } } func main() { jobs := make(chan int, 100) results := make(chan int, 100) for w := 1; w <= 3; w++ { go worker(w, jobs, results) } for j := 1; j <= 5; j++ { jobs <- j } close(jobs) for r := 1; r <= 5; r++ { <-results } } ``` 在这个示例中,我们创建了一个有缓冲通道 `jobs` 和一个无缓冲通道 `results`。然后创建了 3 个协程 `worker`,用于处理任务。在 `main` 函数中,我们向 `jobs` 通道中发送了 5 个任务,然后关闭了 `jobs` 通道。接下来,我们从 `results` 通道中接收了 5 个结果。在 `worker` 函数中,我们使用 `range` 循环从 `jobs` 通道中接收任务,然后处理任务并将结果发送到 `results` 通道中。 通过协程和通道的组合,我们可以轻松实现并发处理任务,并且避免了线程和进程的开销和安全问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值