Go context 原理(channel广播机制 + mutex线程安全)

简介: Go context 原理(channel广播机制 + mutex线程安全)

Go context 原理简述

context包构建了树型关系的Context。go Context底层实际上是通过使用 channel + mutex 来实现的。channel负责在父级节点cancel()后的相关子协程之间广播通信,而mutex则保证了ctx在多个 goroutine 之间传递时的线程安全。

使用context时,首先要创建一个顶级的context,也就是context.Background()

每次用户请求到来时,向一组具有上下文关系的 goroutine 中分别传入ctx 参数,并分别监听ctx.Done()方法。

Done()方法返回一个只读的channel,所有相关函数监听此channel。一旦channel关闭,所有负责监听的 goroutine 通过Go channel被关闭时的广播机制,都能够收到通知。

子goroutine可以通过 select-case 的方式检查自身是否被父级节点cancel(),一旦上层环境(父节点)撤销了本 goroutine 的执行,应当终止对当前请求信息的处理,释放资源并return。

正因为上述方式,一个request范围内所有 goroutine 运行时的取消能得到有效控制。

Go context 介绍

  • 概念:

Go 1.7 标准库引入 context,中文译作“上下文”,准确说它是 goroutine 的上下文,包含 goroutine 的运行状态、环境、现场等信息。

  • 场景:

后端接收请求时,有时要将获取到的数据交由多个协程处理。例如登录验证时,将权限验证、密码验证、有效期验证分到三个不同的协程里处理,如果此时有一个协程处理失败了,其他协程也应该立即关闭,避免持续占用系统资源。而在Go中就可以用context来进行控制操作。

  • 作用:

context 主要用来在一组 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等。

  • 比较:

在Go里,我们不能直接杀死协程,协程的关闭一般会用 channel + select 方式来控制。但在某些场景下,例如处理一个请求衍生了很多协程,这些协程之间是相互关联的:需要共享一些全局变量、有共同的 deadline 等,而且可以同时被关闭。再用 channel + select 就会比较麻烦,这时就可以通过 context 来实现。

  • 使用:

    • 顶层Context:Background()

此方法返回一个空的Context,它作为所有由此继承Context的根节点。
要创建Context树,首先就是要创建根节点。该Context通常由接收request的第一个goroutine创建。根节点不能被取消、没有值、也没有过期时间,常作为处理request的顶层context存在。

- context库中,有4个关键方法:

WithCancel()返回一个子context对象和一个cancel()函数,可以主动停止goroutine。
WithDeadline()设置一个时间点,到点后执行cancel()方法。
WithTimeout()设置一个time.Duration,到时间则会cancel这个context。
WithValue()可以设置一个 k/v 的键值对,可在下游任何一个嵌套的context中通过key获取value,但是不建议使用这种来做goroutine之间的通信。这个值一般是线程安全的.

- 再配合Context提供的```Done()```方法,***子goroutine可以通过 select-case 的方式检查自身是否被父级节点Cancel***,一旦上层环境(父)撤销了本goroutine的执行,应当终止对当前请求信息的处理,释放资源并return:
// (1) 顶层Context:Background():返回一个空的Context,它作为所有由此继承Context的根节点
func Background() Context {
}

// (2) context库中,有4个关键方法
// 带cancel返回值的Context,一旦cancel被调用,即取消该创建的context
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
}

// 带有效期cancel返回值的Context,即必须到达指定时间点调用的cancel方法才会被执行
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
}

// 带超时时间cancel返回值的Context,类似Deadline,前者是时间点,后者为时间间隔
// 相当于WithDeadline(parent, time.Now().Add(timeout)).
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
}

// (3) Context提供的 Done()方法
select { 
    case <-ctx.Done(): 
        // do some clean… 
        // 主动终止对当前请求信息的处理,释放资源并返回
}

注意:父节点Context可以主动通过调用cancel方法取消子节点Context,而 子节点Context只能被动等待。同时父节点Context自身一旦被取消(如其上级节点Cancel),其下的所有子节点Context均会自动被取消。

  • 代码示例:

通过引入Context包,一个request范围内所有goroutine运行时的取消能得到有效控制

package main

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

func someHandler() {
    // 创建继承Background的子节点Context
    ctx, cancel := context.WithCancel(context.Background())
    // ctx, cancel := context.WithTimeout(context.Background(), 3 * time.Second) // 3秒后提前结束doSth()
    go doSth(ctx)

    //模拟程序运行 - Sleep 5秒
    time.Sleep(5 * time.Second) // 避免main()函数会提前结束
    cancel()
}

//每1秒work一下,同时会判断ctx是否被取消,如果是就退出
func doSth(ctx context.Context) {
    var i = 1
    for {
        time.Sleep(1 * time.Second)
        select {
        case <-ctx.Done():
            fmt.Println("done")
            return
        default:
            fmt.Printf("work %d seconds. \n", i)
        }
        i++
    }
}

func main() {
    fmt.Println("start...")
    someHandler()
    fmt.Println("end.")
}
  • 使用原则:

Context使用原则:

- 不要把Context放在结构体中,要以参数的方式传递 
- 以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位
- 给一个函数方法传递Context时,不要传递nil,如果不知道传递什么,就使用```context.TODO()```
- Context的Value相关方法应该传递必须的参数,不要什么数据都使用这个传递
- **Context是线程安全的,可以放心的在多个goroutine中传递**
  • 缺点:

一旦代码中某处用到了Context,传递Context变量(通常作为函数的第一个参数)会像病毒一样蔓延在各处调用它的地方。即每一个相关函数都必须增加一个context.Context类型的参数,且作为第一个参数,这对无关代码完全是侵入式的。

目录
相关文章
|
22天前
|
存储 安全 算法
Go语言是如何支持多线程的
【10月更文挑战第21】Go语言是如何支持多线程的
108 72
|
6天前
|
Go 调度 开发者
探索Go语言中的并发模式:goroutine与channel
在本文中,我们将深入探讨Go语言中的核心并发特性——goroutine和channel。不同于传统的并发模型,Go语言的并发机制以其简洁性和高效性著称。本文将通过实际代码示例,展示如何利用goroutine实现轻量级的并发执行,以及如何通过channel安全地在goroutine之间传递数据。摘要部分将概述这些概念,并提示读者本文将提供哪些具体的技术洞见。
|
18天前
|
存储 算法 Java
Go语言的内存管理机制
【10月更文挑战第25天】Go语言的内存管理机制
21 2
|
19天前
|
Java
线程池内部机制:线程的保活与回收策略
【10月更文挑战第24天】 线程池是现代并发编程中管理线程资源的一种高效机制。它不仅能够复用线程,减少创建和销毁线程的开销,还能有效控制并发线程的数量,提高系统资源的利用率。本文将深入探讨线程池中线程的保活和回收机制,帮助你更好地理解和使用线程池。
44 2
|
22天前
|
Go 调度 开发者
Go语言多线程的优势
【10月更文挑战第15天】
15 4
|
23天前
|
Java
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
24 1
|
28天前
|
安全 Go 调度
探索Go语言的并发之美:goroutine与channel
在这个快节奏的技术时代,Go语言以其简洁的语法和强大的并发能力脱颖而出。本文将带你深入Go语言的并发机制,探索goroutine的轻量级特性和channel的同步通信能力,让你在高并发场景下也能游刃有余。
|
1月前
|
存储 安全 Go
探索Go语言的并发模型:Goroutine与Channel
在Go语言的多核处理器时代,传统并发模型已无法满足高效、低延迟的需求。本文深入探讨Go语言的并发处理机制,包括Goroutine的轻量级线程模型和Channel的通信机制,揭示它们如何共同构建出高效、简洁的并发程序。
|
1月前
|
安全 Java 开发者
在多线程编程中,确保数据一致性与防止竞态条件至关重要。Java提供了多种线程同步机制
【10月更文挑战第3天】在多线程编程中,确保数据一致性与防止竞态条件至关重要。Java提供了多种线程同步机制,如`synchronized`关键字、`Lock`接口及其实现类(如`ReentrantLock`),还有原子变量(如`AtomicInteger`)。这些工具可以帮助开发者避免数据不一致、死锁和活锁等问题。通过合理选择和使用这些机制,可以有效管理并发,确保程序稳定运行。例如,`synchronized`可确保同一时间只有一个线程访问共享资源;`Lock`提供更灵活的锁定方式;原子变量则利用硬件指令实现无锁操作。
20 2
|
23天前
|
存储 Go 调度
深入理解Go语言的并发模型:goroutine与channel
在这个快速变化的技术世界中,Go语言以其简洁的并发模型脱颖而出。本文将带你穿越Go语言的并发世界,探索goroutine的轻量级特性和channel的同步机制。摘要部分,我们将用一段对话来揭示Go并发模型的魔力,而不是传统的介绍性文字。