DAY 11:Thread-Specific Storage Pattern,高并发的多重宇宙空间

什麽是 Thread-Specific Storage Pattern?

为每个 goroutine 拥有自己的储存空间,供不同的情境识别与使用

举例来说,如果正在设计一个 logger 物件,我们希望不同 goroutine 透过此物件纪录 log 时也能说明此动作是哪个 goroutine 做的,那此 logger 物件就需要「能辨识不同的 goroutine」。

java 的方式

以 java 来说,设计的方式是一个 logger 物件拥有 ThreadLocal 类别,ThreadLocal 简单来说即是一个 hashmap,但他在.get().set()时都可以以 thread id 来当作 key,并存入任意 value。

不同的 thread 从此空间拿取资料只能依照自己的 thread id 拿取,所以不会产生 race condition,也不需要 lock 保护,如图:

golang 的方式

golang 的标准库不提供取得 goroutine id 的方法,gopher Andrew Gerrand 认为:

We wouldn't even be having this discussion if thread local storage wasn't useful. But every feature comes at a cost, and in my opinion the cost of threadlocals far outweighs their benefits. They're just not a good fit for Go.

即取得 id 的开销比实际的应用来的大,所以不采用此方式。虽然有gls这样的 library,但是利用 stack 的特性来实作,并不是很稳定的作法。

所以 golang 是采用 context 的做法来达到「让 goroutine 拥有自己的储存空间」,context 可以利用With等方法,把 context 夹带某些值,带到不同的 goroutine,

  • context.WithValue(ctx, key, val): 可以将原有 context 新增某 key,并存放着某 value

除此之外也有其他With方法,让 context 可以夹带某些功能

  • context.WithCancel(ctx, cancelFunc): 让 context 夹带取消功能,当呼叫取消後,context.Done()就会发出 close 讯号
  • context.WithDeadline(ctx, deadlineTime): 让 context 夹带特定时间结束功能,当到特定时间後,context.Done()就会发出 close 讯号
  • context.WithTimeout(ctx, timeout): 让 context 夹带倒数计时结束功能,当倒数计时完毕後,context.Done()就会发出 close 讯号

每个 goroutine 都可以用的context.With来复制一份 context 并添加事物,藉此达到独立的储存空间,如图:

上图每个地鼠都是一个 goroutine,他们都拥有自己透过With复制的 context

问题情境

设计一个 server 的 logger 系统,在不同 goroutine request 进入系统後,log 时都会显示 request id

解决方式

相关的 code 在Github - go-design-patterns

透过context.WithValue(),将不同 goroutine request 的 context 夹带唯一的 uuid,在RequestHandler()运行时就可以用ctx.Value(RequestID{})取出 request id 来 log,如下:

package main

import (
	"context"
	"fmt"
	"time"

	uuid "github.com/satori/go.uuid"
)

type RequestID struct{}

func RequestHandler(ctx context.Context) {
	fmt.Printf("request ID is %s\n", ctx.Value(RequestID{}))

	// do something
}

func main() {
	ctx := context.Background()
	go RequestHandler(context.WithValue(ctx, RequestID{}, uuid.NewV4().String()))
	go RequestHandler(context.WithValue(ctx, RequestID{}, uuid.NewV4().String()))
	go RequestHandler(context.WithValue(ctx, RequestID{}, uuid.NewV4().String()))

	time.Sleep(10 * time.Second) //等待goroutine执行完毕
}

事实上这就是很多框架如gin, iris对 request id 添加方式,会在 router 设定处添加RequestIDMiddleware()这类 middleware,并会在里头处理与context.WithValue()概念相同的实作,将 request id 添加到此 request 的 context 上,供後续 log 使用,gin 的范例如下:

func RequestIdMiddleware() gin.HandlerFunc {
  return func(c *gin.Context) {
    c.Writer.Header().Set("X-Request-Id", uuid.NewV4().String())
    c.Next()
  }
}

func main() {
  router := gin.Default()
  router.Use(RequestIdMiddleware)

  // 其他router设定
}

<<:  python 练习

>>:  [DAY 09] ASG (Auto Scaling Group)

Day 01:程序设计师的一天

前言 这是一个七年 Android 工程师专为麻瓜写的。 麻瓜是指: 不会魔法、选错科系入错行、被老...

Day 15 讯息伫列的储存、接收及传送

关於讯息伫列怎麽去储存呢?大致分成下列两种: 1.系统池(system pool):如果能确定讯息伫...

[ Day 29 ] 实作一个 React.js 网站 5/5

今天是实作 React.js 网站的最後一篇介绍了,这篇会大量的使用到 React Hooks 的...

Pima Indians diabetes dataset 考古溯源 & model prediction

Machine learning常用到的一个diabetes 糖尿病资料集,Pima印第安人数据起源...

[Pizza吃到饱-2]【乔e欧爸爸 手工披萨吃到饱-台中新时代店】GIOIA PAPA handmade pizza lover

温馨小提醒:吃多少,拿多少(避免造成浪费啊~) 疫情前,我总是会在影片下方注明「吃多少,拿多少(避免...