Skip to content

Commit 1b1b5d5

Browse files
authored
Implement PaddedSpinLock, which avoids false sharing. (#55944)
1 parent 051be13 commit 1b1b5d5

File tree

4 files changed

+75
-10
lines changed

4 files changed

+75
-10
lines changed

NEWS.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ Command-line option changes
2424
Multi-threading changes
2525
-----------------------
2626

27+
* A new `AbstractSpinLock` is defined with `SpinLock <: AbstractSpinLock` ([#55944]).
28+
* A new `PaddedSpinLock <: AbstractSpinLock` is defined. It has extra padding to avoid false sharing ([#55944]).
29+
* New types are defined to handle the pattern of code that must run once per process, called
30+
a `OncePerProcess{T}` type, which allows defining a function that should be run exactly once
31+
the first time it is called, and then always return the same result value of type `T`
32+
every subsequent time afterwards. There are also `OncePerThread{T}` and `OncePerTask{T}` types for
33+
similar usage with threads or tasks. ([#TBD])
34+
2735
Build system changes
2836
--------------------
2937

base/locks-mt.jl

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import .Base: unsafe_convert, lock, trylock, unlock, islocked, wait, notify, AbstractLock
44

55
export SpinLock
6-
6+
public PaddedSpinLock
77
# Important Note: these low-level primitives defined here
88
# are typically not for general usage
99

@@ -12,33 +12,68 @@ export SpinLock
1212
##########################################
1313

1414
"""
15-
SpinLock()
15+
abstract type AbstractSpinLock <: AbstractLock end
1616
17-
Create a non-reentrant, test-and-test-and-set spin lock.
17+
A non-reentrant, test-and-test-and-set spin lock.
1818
Recursive use will result in a deadlock.
1919
This kind of lock should only be used around code that takes little time
2020
to execute and does not block (e.g. perform I/O).
2121
In general, [`ReentrantLock`](@ref) should be used instead.
2222
2323
Each [`lock`](@ref) must be matched with an [`unlock`](@ref).
24-
If [`!islocked(lck::SpinLock)`](@ref islocked) holds, [`trylock(lck)`](@ref trylock)
24+
If [`!islocked(lck::AbstractSpinLock)`](@ref islocked) holds, [`trylock(lck)`](@ref trylock)
2525
succeeds unless there are other tasks attempting to hold the lock "at the same time."
2626
2727
Test-and-test-and-set spin locks are quickest up to about 30ish
2828
contending threads. If you have more contention than that, different
2929
synchronization approaches should be considered.
3030
"""
31-
mutable struct SpinLock <: AbstractLock
31+
abstract type AbstractSpinLock <: AbstractLock end
32+
33+
"""
34+
SpinLock() <: AbstractSpinLock
35+
36+
Spinlocks are not padded, and so may suffer from false sharing.
37+
See also [`PaddedSpinLock`](@ref).
38+
39+
See the documentation for [`AbstractSpinLock`](@ref) regarding correct usage.
40+
"""
41+
mutable struct SpinLock <: AbstractSpinLock
3242
# we make this much larger than necessary to minimize false-sharing
3343
@atomic owned::Int
3444
SpinLock() = new(0)
3545
end
3646

47+
# TODO: Determine the cache line size using e.g., CPUID. Meanwhile, this is correct for most
48+
# processors.
49+
const CACHE_LINE_SIZE = 64
50+
51+
"""
52+
PaddedSpinLock() <: AbstractSpinLock
53+
54+
PaddedSpinLocks are padded so that each is guaranteed to be on its own cache line, to avoid
55+
false sharing.
56+
See also [`SpinLock`](@ref).
57+
58+
See the documentation for [`AbstractSpinLock`](@ref) regarding correct usage.
59+
"""
60+
mutable struct PaddedSpinLock <: AbstractSpinLock
61+
# we make this much larger than necessary to minimize false-sharing
62+
_padding_before::NTuple{max(0, CACHE_LINE_SIZE - sizeof(Int)), UInt8}
63+
@atomic owned::Int
64+
_padding_after::NTuple{max(0, CACHE_LINE_SIZE - sizeof(Int)), UInt8}
65+
function PaddedSpinLock()
66+
l = new()
67+
@atomic l.owned = 0
68+
return l
69+
end
70+
end
71+
3772
# Note: this cannot assert that the lock is held by the correct thread, because we do not
3873
# track which thread locked it. Users beware.
39-
Base.assert_havelock(l::SpinLock) = islocked(l) ? nothing : Base.concurrency_violation()
74+
Base.assert_havelock(l::AbstractSpinLock) = islocked(l) ? nothing : Base.concurrency_violation()
4075

41-
function lock(l::SpinLock)
76+
function lock(l::AbstractSpinLock)
4277
while true
4378
if @inline trylock(l)
4479
return
@@ -49,7 +84,7 @@ function lock(l::SpinLock)
4984
end
5085
end
5186

52-
function trylock(l::SpinLock)
87+
function trylock(l::AbstractSpinLock)
5388
if l.owned == 0
5489
GC.disable_finalizers()
5590
p = @atomicswap :acquire l.owned = 1
@@ -61,7 +96,7 @@ function trylock(l::SpinLock)
6196
return false
6297
end
6398

64-
function unlock(l::SpinLock)
99+
function unlock(l::AbstractSpinLock)
65100
if (@atomicswap :release l.owned = 0) == 0
66101
error("unlock count must match lock count")
67102
end
@@ -70,6 +105,6 @@ function unlock(l::SpinLock)
70105
return
71106
end
72107

73-
function islocked(l::SpinLock)
108+
function islocked(l::AbstractSpinLock)
74109
return (@atomic :monotonic l.owned) != 0
75110
end

doc/src/base/multi-threading.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ Base.@threadcall
6363
These building blocks are used to create the regular synchronization objects.
6464

6565
```@docs
66+
Base.Threads.AbstractSpinLock
6667
Base.Threads.SpinLock
68+
Base.Threads.PaddedSpinLock
6769
```
6870

6971
## Task metrics (Experimental)

test/threads_exec.jl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,26 @@ if threadpoolsize(:default) > 1
110110
end
111111
end
112112

113+
if threadpoolsize() > 1
114+
let lk = Base.Threads.PaddedSpinLock()
115+
c1 = Base.Event()
116+
c2 = Base.Event()
117+
@test trylock(lk)
118+
@test !trylock(lk)
119+
t1 = Threads.@spawn (notify(c1); lock(lk); unlock(lk); trylock(lk))
120+
t2 = Threads.@spawn (notify(c2); trylock(lk))
121+
Libc.systemsleep(0.1) # block our thread from scheduling for a bit
122+
wait(c1)
123+
wait(c2)
124+
@test !fetch(t2)
125+
@test istaskdone(t2)
126+
@test !istaskdone(t1)
127+
unlock(lk)
128+
@test fetch(t1)
129+
@test istaskdone(t1)
130+
end
131+
end
132+
113133
# threading constructs
114134

115135
@testset "@threads and @spawn threadpools" begin

0 commit comments

Comments
 (0)