Java’s ThreadLocal is the dedicated storage Java provides for each thread, putting information on ThreadLocal that can be used to simplify API usage for upper-layer applications. One obvious application scenario is that with ThreadLocal, there is no need to add additional parameters to each function in the call stack to pass some information related to the call chain and log link tracing.

The Go Team made it clear in their proposal to add LocalStorage that they would prefer to explicitly use the Context parameter instead of LocalStorage for passing Context information. There are several implementations of GLS (Goroutine Local Storage) in the community, and our team has also used GLS in the system. There is no significant performance degradation after application, mainly because we do not want to add parameters on each function definition to pass TraceId for log link tracing. However, it is not recommended that business logic rely on the GLS libraries of these three parties.

There has been a lot of discussion about the need to add GLS and the performance and incompatibilities that GLS brings to the language. I just saw an article summarizing the discussion about whether OR not GLS should be introduced to Go.

Original author: LAN Lingzi

Original link: lanlingzi.cn/post/techni…

1 background

Recently, I designed the API of call chain and log tracking. I found that compared with Java and C++, Go language has no native Thread (coroutine) context, nor supports TLS (Thread Local Storage), nor does it expose the API to obtain the Id of Goroutine (GoId). This makes it impossible to put some information on TLS, as Java does, to simplify API usage for upper-layer applications: there is no need to pass parameters in the function of the call stack to pass some context information about the call chain and log trace.

In Java and C++, TLS is a mechanism that refers to a structure stored in a thread’s environment for storing data that is exclusive to that thread. Threads in the process cannot access TLS that are not their own, which ensures that data in TLS is globally shared within the thread but not visible outside the thread.

In Java, the JDK library provides thread.currentthread () to retrieve the CurrentThread object and ThreadLocal to store and retrieve thread-local variables. Since Java can get the CurrentThread through thread.currentthread (), the idea is simple, with a Map in the ThreadLocal class that stores variables for each Thread.

ThreadLocal’s API provides the following four methods:

public T get()
protected  T initialValue()
public void remove()
public void set(T value)
Copy the code
  • T get(): returns the value in the current thread copy of this thread-local variable, which is created and initialized if this is the first time the thread has called the method.
  • protected T initialValue(): returns the current thread’s initial value for this thread-local variable. This method is called at most once each time a thread is accessed to obtain each thread-local variable, the first time it is used by the threadget()When a method accesses a variable. If the thread precedesgetThe method callset(T)Method is not called again in the threadinitialValueMethods.
  • void remove(): Removes the value of this thread-local variable. This may help reduce storage requirements for thread-local variables. If this thread-local variable is accessed again, it will own it by defaultinitialValue.
  • void set(T value)Sets the value in the current thread copy of this thread-local variable to the specified value. Many applications don’t need this functionality, they just rely oninitialValue()Method to set the value of a thread-local variable.

In the Go language, the solution provided by Google is to use the golang.org/x/net/context package to pass GoRoutine context. For further insight into the Go Context, refer to my previous analysis: Understand the Go Context mechanism. Context can also store some Goroutine data for sharing, but it provides the interface WithValue function to create a new Context object.

func WithValue(parent Context, key interface{}, val interface{}) Context {
	return &valueCtx{parent, key, val}
}

type valueCtx struct {
	Context
	key, val interface{}
}

func (c *valueCtx) Value(key interface{}) interface{} {
	if c.key == key {
		return c.val
	}
	return c.Context.Value(key)
}
Copy the code

If the Value of the Context is set once, a Context object is generated. If the Value of the Context is set once, the Value of the Context object is retrieved. Getting a Value is safe because the interface is designed so that only one Goroutine sets the Key/Value at a time. The other goroutines can only read the Value of the Key.

2 Why is the GoId interface not obtained

This, among other reasons, to prevent programmers for simulating thread local storage using the goroutine id as a key.

In order to avoid using the Goroutine Id as the Key of Thread Local Storage.

Please don’t use goroutine local storage. It’s highly discouraged. In fact, IIRC, we used to expose Goid, but it is hidden since we don’t want people to do this.

Users often use GoId to implement Goroutine local storage, which the Go language does not want users to use.

When goroutine goes away, its goroutine local storage won’t be GCed. But you can’t get a list of all running goroutines)

The reason goroutine local Storage is not recommended is because it is not easy to GC, and while you can get the current GoId, you cannot get other running Goroutines.

what if handler spawns goroutine itself? the new goroutine suddenly loses access to your goroutine local storage. You can guarantee that your own code won’t spawn other goroutines, but in general you can’t make sure the standard library or any 3rd party code won’t do that.

Another important reason is that since it is so easy to create a Goroutine (and threads generally use thread pools), the new Goroutine will lose access to the Goroutine local storage. Upper-layer applications are required to ensure that no new Goroutine will be created, but it is hard to ensure that the standard or third libraries will not do so.

thread local storage is invented to help reuse bad/legacy code that assumes global state, Go doesn’t have legacy code like that, and you really should design your code so that state is passed explicitly and not as global (e.g. resort to goroutine local storage)

TLS is used to help reuse existing bad (legacy) code that uses global state. The Go language suggests redesigning the code to pass state explicitly rather than globally (for example, goroutine Local Storage).

3 Obtain the GoId by other means

While the Go language consciously hides the GoId, there are currently means to obtain it:

  • Modifying the source code exposes the GoId, but the Go language can modify the source code at any time, resulting in incompatibilities

    The newextram function in the library runtime/proc.go (go 1.6.3) produces a GoId:

    mp.lockedg = gp
    gp.lockedm = mp
    gp.goid = int64(atomic.Xadd64(&sched.goidgen, 1))
    Copy the code
  • Run runtime.Stack to analyze the Stack output to obtain the GoId.

    In the library runtime/mprof.go (go 1.6.3), runtime.Stack fetches the GP object (containing the GoId) and prints the entire Stack:

    func Stack(buf []byte, all bool) int {
        if all {
            stopTheWorld("stack trace")
        }
    
        n := 0
        if len(buf) > 0 {
            gp := getg()
            sp := getcallersp(unsafe.Pointer(&buf))
            pc := getcallerpc(unsafe.Pointer(&buf))
            systemstack(func() {
                g0 := getg()
                g0.m.traceback = 1
                g0.writebuf = buf[0:0:len(buf)]
                goroutineheader(gp)
                traceback(pc, sp, 0, gp)
                if all {
                    tracebackothers(gp)
                }
                g0.m.traceback = 0
                n = len(g0.writebuf)
                g0.writebuf = nil
            })
        }
    
        if all {
            startTheWorld()
        }
        return n
    }
    Copy the code

    The runtime/ mprofe. go runtime/ mProfe. go runtime/ mProfe. go runtime/ mProfe. go From the above code, if the second argument is specified as true, STW is also used, which is unacceptable to the business system in any case. If the Go language changes the output of the Stack, analyzing the Stack information will also cause that the GoId cannot be obtained.

  • Use runtime.Callers to label calls to the Stack

    Code reference: github.com/jtolds/gls/…

  • Inline C or inline assembly

    Go version 1.5, compiled under x86_64ARC, is estimated to be also not universal

    // func GoID() int64
    TEXT s3lib GoID(SB),NOSPLIT,$0-8
    MOVQ TLS, CX
    MOVQ 0(CX)(TLS*1), AX
    MOVQ AX, ret+0(FP)
    RET
    Copy the code

4 Open source Goroutine Local Storage implementation

As long as there is a mechanism to obtain the GoId, you can use the global map to implement goroutine local storage like Java.

  • tylerb/gls

    The GoId is obtained by analyzing the Stack output through runtime.Stack.

  • jtolds/gls

    The GoId is the generic runtime.Callers label calls to the Stack

The second one was tested in 2013 with the following data:

BenchmarkGetValue 500000 2953 ns/op BenchmarkSetValues 500000 4050 ns/op

The goroutine local Storage implementation is map+RWMutex and has some performance bottlenecks.

  • Unlike threads, goroutines can number up to 100,000 concurrent threads, and performance deteriorates dramatically when so many goroutines compete for the same lock.
  • The GoId is obtained by analyzing the information from the call to the Stack, and is also a costly call, in a word: slow.

However, without an official GLS, it’s not very convenient, and there are performance and incompatibility risks associated with third-party implementations. Even the jTOLds/GLS writer posted comments from others:

“Wow, that’s horrifying.”

“This is the most terrible thing I have seen in a very long time.”

“Where is it getting a context from? Is this serializing all the requests? What the heck is the client being bound to? What are these tags? Why does he need callers? Oh God no. No no no.”

5 subtotal

The Go language officially considers TLS to store global state to be a bad design, but to pass state explicitly. Google’s solution is golang.org/x/net/context.