乐趣区

关于golang:Go语言如何通过Go来更好的开发并发程序

并行、并发

并行和并发的区别:

  • 并行:两个或多个程序在 同一时刻 执行。
  • 并发:两个或多个程序在 同一个时间段内 执行。

并行执行的程序 ,在同一时刻,是真真正正的有多个程序在 CPU 上执行,这也就须要 CPU 提供多核计算的能力。而 并发执行的程序,只是在宏观的角度观察到有多个程序在 CPU 上执行,宏观上是它们在 CPU 上被疾速轮换执行。

对于过程、线程、协程,并发、并行,在我之前的文章中讲 并发把握 时也有介绍过,感兴趣的能够过来瞅一眼。传送门在此 Go 通关 09:并发把握,goroutine 和 channel 申明与应用!

Go 的 MPG 线程模型

之所以 Go 被认为是高性能开发语言,在于它在原生态反对 协程并发。协程是一种用户线程,是轻量级线程。协程的调度齐全取决于用户空间的代码管制。

协程领有本人的寄存器上下文和栈,并存储在用户空间,协程在切换时无需切换到内核态来拜访内核空间,切换速度极快。开发人员须要在用户空间解决协程切换时候的上下文信息的保留和复原、栈空间大小的治理等技术问题。

Go 语言采纳了一种非凡的两级线程模型,即 MPG 线程模型:

  • M,即 machine,相当于内核线程在 Go 过程中的映射,它与内核线程一一对应,代表真正执行计算的资源。M 的生命周期内,只会与一个内核线程相关联。
  • P,即 processor,代表 Go 代码片段执行所需的上下文环境。M 和 P 的联合能够为 G 提供无效的运行环境。它们之间的联合关系不是固定的。P 的最大数量决定了 Go 程序的并发规模,由 runtime.GOMAXPROCS 变量来决定。
  • G,即 goroutine,是一种轻量级的用户线程,是对代码片段的封装,领有执行时的栈、状态和代码片段等信息。

在理论执行过程中,多个可执行的 G 会程序挂载在 P 的可执行 G 队列上面,期待调度和指向。当 G 中存在一些 I/O 零碎调用阻塞了 M 时,P 会断开 M 的分割,从调度器闲暇 M 队列中获取一个 M 或创立一个新的 M 进行组合执行,从而保障 P 中可执行 G 队列中其余 G 失去执行,因为程序中并行执行的 M 数量没有变,所以程序的 CPU 有很高的利用率。

1. select 多路复用

select 能够实现多路复用,即同时监听多个 channel。

  • 发现哪个 channel 有数据产生,就执行相应的 case 分支
  • 如果同时有多个 case 分支能够执行,则会随机抉择一个
  • 如果一个 case 分支都不可执行,则 select 会始终期待

示例:

package main

import ("fmt")

func main() {ch := make(chan int, 1)
    for i := 0; i < 10; i++ {
        select {
        case x := <-ch:
            fmt.Println(x)
        case ch <- i:
            fmt.Println("--", i)
        }
    }
}

运行后果:

-- 0
0
-- 2
2
-- 4
4
-- 6
6
-- 8
8

2. Context 上下文

当须要在多个 goroutine 中传递上下文信息时,能够应用 Context 实现。Context 除了用来传递上下文信息,还能够用于传递终结执行子工作的相干信号,停止多个执行子工作的 goroutine。Context 中提供以下接口:

type Context interface {
    // 返回 Context 被勾销的工夫,即实现工作的截止日期
    Deadline() (deadline time.Time, ok bool)
    
    //1. 返回一个 channel,这个 channel 会在当前工作实现或者上下文被勾销之后敞开
    //2. 屡次调用 Done 办法会返回同一个 channel;Done() <-chan struct{}
    
    //1. 返回 Context 完结的起因,只会在 Done 返回的 channel 被敞开时才会返回非空的值
    //2. 如果 Context 被勾销,会返回 Canceled 谬误
    //3. 如果 Context 超时,会返回 DeadlineExceeded 谬误
    Err() error
    
    // 用于从 Context 中获取传递的键值信息
    Value(key interface{}) interface{}}

在理论工作中,一个 Web 申请可能须要启动多个 goroutine 协同工作,goroutine 之间可能须要共享申请的信息,且当申请被勾销或者执行超时时,该申请启动的所有 goroutine 都须要完结,开释资源,这时就须要应用 Context 来解决这些问题。

示例:

package main

import (
    "context"
    "fmt"
    "time"
)

const DB_ADDRESS  = "db_address"
const CALCULATE_VALUE  = "calculate_value"

func readDB(ctx context.Context, cost time.Duration)  {fmt.Println("db address is", ctx.Value(DB_ADDRESS))
    select {case <- time.After(cost): //  模仿数据库读取
        fmt.Println("read data from db")
    case <-ctx.Done():
        fmt.Println(ctx.Err()) // 工作勾销的起因

        // 一些清理工作

    }

}

func calculate(ctx context.Context, cost time.Duration)  {fmt.Println("calculate value is", ctx.Value(CALCULATE_VALUE))
    select {case <- time.After(cost): //  模仿数据计算
        fmt.Println("calculate finish")
    case <-ctx.Done():
        fmt.Println(ctx.Err()) // 工作勾销的起因
    
        // 一些清理工作

    }

}

func main()  {ctx := context.Background(); // 创立一个空的上下文
    // 增加上下文信息
    ctx = context.WithValue(ctx, DB_ADDRESS, "localhost:10086")
    ctx = context.WithValue(ctx, CALCULATE_VALUE, 1234)

    // 设定子 Context 2s 后执行超时返回
    ctx, cancel := context.WithTimeout(ctx, time.Second * 2)

    defer cancel()

    // 设定执行工夫为 4 s
    go readDB(ctx, time.Second * 4)
    go calculate(ctx, time.Second * 4)

    // 充沛执行
    time.Sleep(time.Second * 5)
}

运行后果:

calculate value is 1234
db address is localhost:10086
context deadline exceeded
context deadline exceeded

在例子中,咱们模仿了一个申请中同时进行数据库拜访和逻辑计算的操作,在申请执行超时时,敞开尚未执行完结 goroutine。

  1. 首先通过 context.WithValue 办法为 context 增加上下文信息,Context 在多个 goroutine 中是并发平安的。
  2. 接着应用 context.WithTimeout 办法设定了 Context 的超时工夫为 2s,并传递给 readDB 和 calculate 两个 goroutine 执行子工作。
  3. 在 readDB 和 calculate 办法中,应用 select 语句对 Context 的 Done 通道进行监控。因为咱们设定了子 Context 将在 2s 之后超时,所以它将在 2s 之后敞开 Done 通道;然而预设的子工作执行工夫为 4s,对应的 case 语句尚未返回,执行被勾销,进入到清理工作的 case 语句中,完结掉以后的 goroutine 所执行的工作。
退出移动版