In 2023, there have been some changes to Go’s concurrency library, and this article provides an overview of these changes. Minor details such as typos and documentation changes will not be covered.
This article is first published in the medium MPP plan. If you are a medium user, please follow me in medium. Thank you very much.
sync.Map
In Go 1.21.0, three functions related to Once were added to the sync package to facilitate the usage of Once:
|
|
The functionality of these three functions is as follows:
OnceFunc: Returns a functiongthat, when called multiple times, will only executefonce. Iffpanics during execution, subsequent calls togwill not executef, but each call will still panic.OnceValue: Returns a functiongthat, when called multiple times, will only executefonce. The return type ofgisT, which is an additional return value compared to the previous function. The panic behavior is the same asOnceFunc.OnceValues: Returns a functiongthat, when called multiple times, will only executefonce. The return type ofgis(T1, T2), which is an additional return value compared to the previous function. The panic behavior is the same asOnceFunc.
In theory, you can add more functions and return more values. However, since Go does not have a tuple type, the return values of function g cannot be simplified to a tuple type. In any case, Go 1.21.0 only added these three functions.
What are the benefits of these functions? Previously, when using sync.Once, such as initializing a thread pool, we needed to define a variable for the thread pool and call sync.Once.Do every time we accessed the thread pool variable:
|
|
With OnceValue, the code can be simplified:
|
|
The code is slightly simplified, and you only need to call the returned function g to obtain the singleton.
In summary, these three functions are just encapsulations of sync.Once to make it more convenient to use.
Understanding copyChecker
We know that sync.Cond has two fields, noCopy and checker. noCopy can be statically checked using the go vet tool, but checker is checked at runtime:
|
|
Previously, the conditions for copyChecker were as follows, although it is just three simple lines, it is not easy to understand:
|
|
Now, with added comments, the meaning of these three lines is explained:
|
|
The main logic is as follows:
- The first step is a fast check, directly comparing the pointer of
cand the pointer ofcitself. If they are not equal, it means thatchas been copied. This is the fastest check path. - The second step ensures that
cis initialized. It initializescusing CAS (CompareAndSwap). If the CAS succeeds, we’re done. If it fails, it means thatcwas either initialized concurrently and we simply lost the race, orchas been copied. - The third step repeats the first step’s check. Since we know that
cis definitely initialized at this point, if the check fails, it means thatcwas copied.
The entire logic uses CAS combined with two pointer checks to ensure the correctness of the judgment.
In summary, the first step is a performance optimization. The second step uses CAS to ensure initialization. The third step rechecks to ensure the judgment.
Optimization in sync.Map
Previously, the implementation of the Range function in sync.Map was as follows:
|
|
There was a line of code: m.read.Store(&read), which caused read to escape to the heap. To avoid the escape of read, a small trick was employed by introducing a new variable:
|
|
Issue #62404 analyzed this problem.
Replacing Done in sync.Once Implementation with atomic.Uint32
Previously, the implementation of sync.Once was as follows:
|
|
The done field was of type uint32 to indicate whether Once has been executed. The reason for using uint32 instead of bool is that uint32 can be used with atomic operations from the atomic package, while bool cannot.
Now, the implementation of sync.Once is as follows:
|
|
Since Go 1.19, the standard library has provided atomic wrappers for basic types, and a large amount of code in the Go standard library has been replaced with atomic.XXX types.
In my opinion, this modification may result in a performance decrease compared to the previous implementation in certain cases. I will write an article specifically to explore this.
Besides sync.Once, there are other types that have been replaced with atomic.XXX types in their usage. Is it necessary to replace them?
Optimization in Initial Implementation of sync.OnceFunc
The initial implementation of sync.OnceFunc was as follows:
|
|
If you look closely at this code, you will notice that the function f passed to OnceFunc/OnceValue/OnceValues remains alive even after it has been executed once, as long as the returned function g is not garbage collected. This is unnecessary because f only needs to be executed once and can be garbage collected afterwards. Therefore, an optimization can be made to set f to nil after it is executed, allowing it to be garbage collected.
|
|
Context
As we know, in Go 1.20, a new method WithCancelCause was added ( func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc)), which allows us to pass the cancellation cause to the Context generated by WithCancelCause. This allows us to retrieve the cancellation cause using the context.Cause method.
|
|
Of course, this implementation is only halfway done, as timeout-related Context also needs this functionality. Therefore, in Go 1.21.0, two additional functions were added:
|
|
These two functions, unlike WithCancelCause, directly pass the cause as a parameter instead of using the returned cancel function.
Go 1.21.0 also introduced a function AfterFunc, which is similar to time.AfterFunc, but it returns a Context that is automatically canceled after the timeout. The implementation of this function is as follows:
|
|
The specified Context triggers the invocation of f immediately when done (either due to timeout or cancellation). The returned stop function is used to stop the invocation of f. If stop is called and returns true, f will not be invoked.
This is a helper function, but it may be difficult to understand, and it is unlikely to be widely used.
Other minor performance optimizations, such as replacing type emptyCtx int with type emptyCtx struct{}, are not mentioned here.
An additional function func WithoutCancel(parent Context) Context was added, which creates a Context that is not affected when the parent is canceled.
golang.org/x/sync No Significant Changes in Sync
errgroup now supports setting the cause using withCancelCause. singleflight added an Unwrap method to panicError.