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 functiong
that, when called multiple times, will only executef
once. Iff
panics during execution, subsequent calls tog
will not executef
, but each call will still panic.OnceValue
: Returns a functiong
that, when called multiple times, will only executef
once. The return type ofg
isT
, which is an additional return value compared to the previous function. The panic behavior is the same asOnceFunc
.OnceValues
: Returns a functiong
that, when called multiple times, will only executef
once. The return type ofg
is(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
c
and the pointer ofc
itself. If they are not equal, it means thatc
has been copied. This is the fastest check path. - The second step ensures that
c
is initialized. It initializesc
using CAS (CompareAndSwap). If the CAS succeeds, we’re done. If it fails, it means thatc
was either initialized concurrently and we simply lost the race, orc
has been copied. - The third step repeats the first step’s check. Since we know that
c
is definitely initialized at this point, if the check fails, it means thatc
was 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
.