From ea0a68e050f9a53fba0c1796ec4e218d6e449f14 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Mon, 25 Apr 2022 08:11:10 -0700 Subject: [PATCH] Remove various lock implementations (#40) --- .../src/ConcurrentUtilsBenchmarks.jl | 4 - .../src/bench_acquire_release_coop_locks.jl | 14 -- .../src/bench_acquire_release_read_locks.jl | 14 +- .../bench_acquire_release_read_write_locks.jl | 18 +- .../src/bench_acquire_release_spin_locks.jl | 91 ---------- .../src/raynal_read_write_lock.jl | 35 ---- docs/src/index.md | 20 --- src/ConcurrentUtils.jl | 23 --- src/backoff_lock.jl | 90 +--------- src/clh_lock.jl | 155 ------------------ src/docs/NonreentrantBackoffSpinLock.md | 32 ---- src/docs/NonreentrantCLHLock.md | 24 --- src/docs/ReentrantBackoffSpinLock.md | 24 --- src/docs/ReentrantCLHLock.md | 24 --- src/docs/TaskObliviousLock.md | 31 ---- src/docs/acquire.md | 28 ---- src/docs/acquire_then.md | 19 --- src/docs/race_acquire.md | 18 -- src/docs/read_write_lock.md | 13 +- src/docs/release.md | 5 - src/docs/try_race_acquire.md | 19 --- src/lock_interface.jl | 82 +-------- src/naming_convention.md | 4 +- src/read_write_lock.jl | 12 +- .../src/ConcurrentUtilsTests.jl | 1 - test/ConcurrentUtilsTests/src/test_locks.jl | 154 ----------------- .../src/test_read_write_lock.jl | 60 ++++--- 27 files changed, 71 insertions(+), 943 deletions(-) delete mode 100644 benchmark/ConcurrentUtilsBenchmarks/src/bench_acquire_release_coop_locks.jl delete mode 100644 benchmark/ConcurrentUtilsBenchmarks/src/bench_acquire_release_spin_locks.jl delete mode 100644 benchmark/ConcurrentUtilsBenchmarks/src/raynal_read_write_lock.jl delete mode 100644 src/clh_lock.jl delete mode 100644 src/docs/NonreentrantBackoffSpinLock.md delete mode 100644 src/docs/NonreentrantCLHLock.md delete mode 100644 src/docs/ReentrantBackoffSpinLock.md delete mode 100644 src/docs/ReentrantCLHLock.md delete mode 100644 src/docs/TaskObliviousLock.md delete mode 100644 src/docs/acquire.md delete mode 100644 src/docs/acquire_then.md delete mode 100644 src/docs/race_acquire.md delete mode 100644 src/docs/release.md delete mode 100644 src/docs/try_race_acquire.md delete mode 100644 test/ConcurrentUtilsTests/src/test_locks.jl diff --git a/benchmark/ConcurrentUtilsBenchmarks/src/ConcurrentUtilsBenchmarks.jl b/benchmark/ConcurrentUtilsBenchmarks/src/ConcurrentUtilsBenchmarks.jl index 5cf29f1..ea5ff05 100644 --- a/benchmark/ConcurrentUtilsBenchmarks/src/ConcurrentUtilsBenchmarks.jl +++ b/benchmark/ConcurrentUtilsBenchmarks/src/ConcurrentUtilsBenchmarks.jl @@ -2,14 +2,10 @@ module ConcurrentUtilsBenchmarks using BenchmarkTools: Benchmark, BenchmarkGroup -include("bench_acquire_release_spin_locks.jl") -include("bench_acquire_release_coop_locks.jl") include("bench_acquire_release_read_write_locks.jl") include("bench_acquire_release_read_locks.jl") const MODULES = [ - BenchAcquireReleaseSpinLocks, - BenchAcquireReleaseCoOpLocks, BenchAcquireReleaseReadWriteLocks, BenchAcquireReleaseReadLocks, # ... diff --git a/benchmark/ConcurrentUtilsBenchmarks/src/bench_acquire_release_coop_locks.jl b/benchmark/ConcurrentUtilsBenchmarks/src/bench_acquire_release_coop_locks.jl deleted file mode 100644 index 86136cc..0000000 --- a/benchmark/ConcurrentUtilsBenchmarks/src/bench_acquire_release_coop_locks.jl +++ /dev/null @@ -1,14 +0,0 @@ -module BenchAcquireReleaseCoOpLocks - -using ..BenchAcquireReleaseSpinLocks - -setup(; options...) = BenchAcquireReleaseSpinLocks.setup(; - nspins_list = [0], - ntasks_list = [16 * Threads.nthreads()], - ntries = 2, - options..., -) - -function clear() end - -end # module diff --git a/benchmark/ConcurrentUtilsBenchmarks/src/bench_acquire_release_read_locks.jl b/benchmark/ConcurrentUtilsBenchmarks/src/bench_acquire_release_read_locks.jl index e8f7533..a6a5a77 100644 --- a/benchmark/ConcurrentUtilsBenchmarks/src/bench_acquire_release_read_locks.jl +++ b/benchmark/ConcurrentUtilsBenchmarks/src/bench_acquire_release_read_locks.jl @@ -4,10 +4,10 @@ using BenchmarkTools using ConcurrentUtils using SyncBarriers -using ..BenchAcquireReleaseReadWriteLocks: raynal_read_write_lock, single_reentrantlock +using ..BenchAcquireReleaseReadWriteLocks: single_reentrantlock function setup_repeat_acquire_release( - lock; + lck; ntries = 2^2, nrlocks = 2^8, ntasks = Threads.nthreads(), @@ -17,14 +17,14 @@ function setup_repeat_acquire_release( barrier = CentralizedBarrier(ntasks) workers = map(1:ntasks) do i Threads.@spawn begin - acquire(lock) - release(lock) + lock(lck) + unlock(lck) cycle!(init[i]) cycle!(init[i]) for _ in 1:ntries for _ in 1:nrlocks - acquire(lock) - release(lock) + lock(lck) + unlock(lck) end cycle!(barrier[i], nspins_barrier) end @@ -50,7 +50,7 @@ function setup(; nrlocks = smoke ? 3 : 2^8, ntasks_list = default_ntasks_list(), nspins_barrier = 1_000_000, - locks = [read_write_lock, raynal_read_write_lock, single_reentrantlock], + locks = [read_write_lock, single_reentrantlock], ) suite = BenchmarkGroup() for ntasks in ntasks_list diff --git a/benchmark/ConcurrentUtilsBenchmarks/src/bench_acquire_release_read_write_locks.jl b/benchmark/ConcurrentUtilsBenchmarks/src/bench_acquire_release_read_write_locks.jl index ea72781..ffc1afe 100644 --- a/benchmark/ConcurrentUtilsBenchmarks/src/bench_acquire_release_read_write_locks.jl +++ b/benchmark/ConcurrentUtilsBenchmarks/src/bench_acquire_release_read_write_locks.jl @@ -4,10 +4,6 @@ using BenchmarkTools using ConcurrentUtils using SyncBarriers -include("raynal_read_write_lock.jl") - -raynal_read_write_lock() = read_write_lock(RaynalReadWriteLock()) - function single_reentrantlock() lock = ReentrantLock() return (lock, lock) @@ -25,16 +21,16 @@ function setup_repeat_acquire_release( barrier = CentralizedBarrier(ntasks) workers = map(1:ntasks) do i Threads.@spawn begin - acquire(rlock) - release(rlock) + lock(rlock) + unlock(rlock) cycle!(init[i]) cycle!(init[i]) for _ in 1:ntries - acquire(wlock) - release(wlock) + lock(wlock) + unlock(wlock) for _ in 1:nrlocks - acquire(rlock) - release(rlock) + lock(rlock) + unlock(rlock) end cycle!(barrier[i], nspins_barrier) end @@ -60,7 +56,7 @@ function setup(; nrlocks = smoke ? 3 : 2^8, ntasks_list = default_ntasks_list(), nspins_barrier = 1_000_000, - locks = [read_write_lock, raynal_read_write_lock, single_reentrantlock], + locks = [read_write_lock, single_reentrantlock], ) suite = BenchmarkGroup() for ntasks in ntasks_list diff --git a/benchmark/ConcurrentUtilsBenchmarks/src/bench_acquire_release_spin_locks.jl b/benchmark/ConcurrentUtilsBenchmarks/src/bench_acquire_release_spin_locks.jl deleted file mode 100644 index 558932d..0000000 --- a/benchmark/ConcurrentUtilsBenchmarks/src/bench_acquire_release_spin_locks.jl +++ /dev/null @@ -1,91 +0,0 @@ -module BenchAcquireReleaseSpinLocks - -using BenchmarkTools -using ConcurrentUtils -using SyncBarriers - -function setup_repeat_acquire_release( - lock; - ntries = 2^10, - ntasks = Threads.nthreads(), - nspins = 1_000_000, - nspins_barrier = nspins, -) - init = CentralizedBarrier(ntasks + 1) - barrier = CentralizedBarrier(ntasks) - workers = map(1:ntasks) do i - Threads.@spawn begin - acquire(lock) - release(lock) - cycle!(init[i]) - cycle!(init[i]) - local n = 0 - while true - if lock_supports_nspins(lock) - acquire(lock; nspins = nspins) - else - acquire(lock) - end - release(lock) - (n += 1) < ntries || break - cycle!(barrier[i], nspins_barrier) - end - end - end - cycle!(init[ntasks+1]) - - return function benchmark() - cycle!(init[ntasks+1]) - foreach(wait, workers) - end -end - -default_ntasks_list() = [ - Threads.nthreads(), - # 8 * Threads.nthreads(), - # 64 * Threads.nthreads(), -] - -function setup(; - smoke = false, - ntries = smoke ? 10 : 2^10, - ntasks_list = default_ntasks_list(), - nspins_list = [100, 1_000, 10_000], - locks = [ - ReentrantLock, - Threads.SpinLock, - ReentrantCLHLock, - NonreentrantCLHLock, - ReentrantBackoffSpinLock, - NonreentrantBackoffSpinLock, - ], -) - suite = BenchmarkGroup() - for T in locks - s1 = suite["impl=:$(nameof(T))"] = BenchmarkGroup() - for nspins in (lock_supports_nspins(T) ? nspins_list : [0]) - s2 = s1["nspins=$nspins"] = BenchmarkGroup() - for ntasks in ntasks_list - nspins_barrier = ntasks > Threads.nthreads() ? nothing : 1_000_000 - s2["ntasks=$ntasks"] = @benchmarkable( - benchmark(), - setup = begin - benchmark = setup_repeat_acquire_release( - $T(); - ntries = $ntries, - ntasks = $ntasks, - nspins = $(nspins == 0 ? nothing : nspins), - nspins_barrier = $nspins_barrier, - ) - end, - evals = 1, - ) - end - end - end - return suite -end - -function clear() end - -end # module diff --git a/benchmark/ConcurrentUtilsBenchmarks/src/raynal_read_write_lock.jl b/benchmark/ConcurrentUtilsBenchmarks/src/raynal_read_write_lock.jl deleted file mode 100644 index c1e4538..0000000 --- a/benchmark/ConcurrentUtilsBenchmarks/src/raynal_read_write_lock.jl +++ /dev/null @@ -1,35 +0,0 @@ -using ConcurrentUtils -using ConcurrentUtils.Internal: AbstractReadWriteLock # TODO: export? - -# https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock#Using_two_mutexes -mutable struct RaynalReadWriteLock{ReadLock,GlobalLock} <: AbstractReadWriteLock - readlock::ReadLock - globallock::GlobalLock # must be task-oblivious - _pad::NTuple{7,Int} - nreaders::Int -end - -RaynalReadWriteLock(readlock = ReentrantLock(), globallock = TaskObliviousLock()) = - RaynalReadWriteLock(readlock, globallock, ntuple(_ -> 0, Val(7)), 0) - -function ConcurrentUtils.acquire_read(lock::RaynalReadWriteLock) - acquire_then(lock.readlock) do - n = lock.nreaders - lock.nreaders = n + 1 - if n == 0 - acquire(lock.globallock) - end - end -end - -function ConcurrentUtils.release_read(lock::RaynalReadWriteLock) - acquire_then(lock.readlock) do - n = lock.nreaders -= 1 - if n == 0 - release(lock.globallock) - end - end -end - -ConcurrentUtils.acquire(lock::RaynalReadWriteLock) = acquire(lock.globallock) -ConcurrentUtils.release(lock::RaynalReadWriteLock) = release(lock.globallock) diff --git a/docs/src/index.md b/docs/src/index.md index b17cd02..94d6f6e 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -53,17 +53,7 @@ using DocumentationOverview using ConcurrentUtils DocumentationOverview.table_md( :[ - ReentrantCLHLock, - NonreentrantCLHLock, - ReentrantBackoffSpinLock, - NonreentrantBackoffSpinLock, - TaskObliviousLock, read_write_lock, - acquire, - release, - try_race_acquire, - race_acquire, - acquire_then, ], namespace = ConcurrentUtils, signature = :name, @@ -71,17 +61,7 @@ DocumentationOverview.table_md( ``` ```@docs -ReentrantCLHLock -NonreentrantCLHLock -ReentrantBackoffSpinLock -NonreentrantBackoffSpinLock -TaskObliviousLock read_write_lock -acquire -release -try_race_acquire -race_acquire -acquire_then ``` ## Guards diff --git a/src/ConcurrentUtils.jl b/src/ConcurrentUtils.jl index c82b4b0..f5db634 100644 --- a/src/ConcurrentUtils.jl +++ b/src/ConcurrentUtils.jl @@ -6,16 +6,11 @@ export @tasklet, # Constructors Guard, - NonreentrantBackoffSpinLock, - NonreentrantCLHLock, NotAcquirableError, NotSetError, OccupiedError, Promise, ReadWriteGuard, - ReentrantBackoffSpinLock, - ReentrantCLHLock, - TaskObliviousLock, ThreadLocalStorage, TooManyTries @@ -45,14 +40,6 @@ struct TooManyTries <: InternalPrelude.Exception ntries::Int end -InternalPrelude.@exported_function acquire -InternalPrelude.@exported_function release -InternalPrelude.@exported_function race_acquire -InternalPrelude.@exported_function try_race_acquire -# function try_race_acquire_then end -InternalPrelude.@exported_function acquire_then -InternalPrelude.@exported_function lock_supports_nspins - #= InternalPrelude.@exported_function isacquirable InternalPrelude.@exported_function isacquirable_read @@ -122,16 +109,12 @@ using ..ConcurrentUtils: NotSetError, OccupiedError, TooManyTries, - acquire, acquire_read, acquire_read_then, - race_acquire, race_fetch_or!, - release, release_read, spinfor, spinloop, - try_race_acquire, try_race_acquire_read, try_race_fetch, try_race_fetch_or!, @@ -153,7 +136,6 @@ include("thread_local_storage.jl") # Locks include("lock_interface.jl") include("backoff_lock.jl") -include("clh_lock.jl") include("read_write_lock.jl") include("guards.jl") @@ -161,11 +143,6 @@ end # module Internal const Promise = Internal.Promise const ThreadLocalStorage = Internal.ThreadLocalStorage -const ReentrantBackoffSpinLock = Internal.ReentrantBackoffSpinLock -const NonreentrantBackoffSpinLock = Internal.NonreentrantBackoffSpinLock -const ReentrantCLHLock = Internal.ReentrantCLHLock -const NonreentrantCLHLock = Internal.NonreentrantCLHLock -const TaskObliviousLock = NonreentrantCLHLock const Guard = Internal.Guard const ReadWriteGuard = Internal.ReadWriteGuard diff --git a/src/backoff_lock.jl b/src/backoff_lock.jl index b062fc3..d752cd3 100644 --- a/src/backoff_lock.jl +++ b/src/backoff_lock.jl @@ -1,3 +1,4 @@ +# TODO: export this mutable struct Backoff limit::Int @const maxdelay::Int @@ -11,92 +12,3 @@ end spinfor(delay) return delay end - -abstract type BackoffSpinLock <: Lockable end - -@enum BackoffSpinLockState LCK_AVAILABLE LCK_HELD - -mutable struct NonreentrantBackoffSpinLock <: BackoffSpinLock - @atomic state::BackoffSpinLockState - @const _pad::NTuple{7,Int} - mindelay::Int - maxdelay::Int -end - -NonreentrantBackoffSpinLock(; mindelay = 1, maxdelay = 1000) = - NonreentrantBackoffSpinLock(LCK_AVAILABLE, pad7(), mindelay, maxdelay) - -mutable struct ReentrantBackoffSpinLock <: BackoffSpinLock - @atomic state::BackoffSpinLockState - @const _pad1::NTuple{7,Int} - mindelay::Int - maxdelay::Int - @const _pad2::NTuple{7,Int} - @atomic owner::Union{Nothing,Task} - count::Int -end - -ReentrantBackoffSpinLock(; mindelay = 1, maxdelay = 1000) = - ReentrantBackoffSpinLock(LCK_AVAILABLE, pad7(), mindelay, maxdelay, pad7(), nothing, 0) - -isreentrant(::ReentrantBackoffSpinLock) = true - -Base.islocked(lock::BackoffSpinLock) = (@atomic :monotonic lock.state) !== LCK_AVAILABLE - -function ConcurrentUtils.try_race_acquire( - lock::BackoffSpinLock; - mindelay = lock.mindelay, - maxdelay = lock.maxdelay, - nspins = -∞, - ntries = -∞, -) - handle_reentrant_acquire(lock) && return Ok(nothing) - - if !islocked(lock) && (@atomicswap :acquire lock.state = LCK_HELD) === LCK_AVAILABLE - @goto locked - end - - local nt::Int = 0 - while true - # Check this first so that no loop is executed if `nspins == -∞` - nt < nspins || return Err(TooManyTries(nt, 0)) - islocked(lock) || break - spinloop() - nt += 1 - end - - local nb::Int = 0 - backoff = Backoff(max(1, mindelay), max(1, maxdelay)) - while true - if (@atomicswap :acquire lock.state = LCK_HELD) === LCK_AVAILABLE - @goto locked - end - nb < ntries || return Err(TooManyTries(nt, nb)) - nt += backoff() - nb += 1 - while islocked(lock) - nt < nspins || return Err(TooManyTries(nt, nb)) - spinloop() - nt += 1 - end - end - - @label locked - start_reentrant_acquire(lock) - return Ok(nothing) -end - -function Base.lock( - lock::BackoffSpinLock; - mindelay = lock.mindelay, - maxdelay = lock.maxdelay, -) - try_race_acquire(lock; mindelay, maxdelay, nspins = ∞, ntries = ∞)::Ok - return -end - -function Base.unlock(lock::BackoffSpinLock) - handle_reentrant_release(lock) && return - @atomic :release lock.state = LCK_AVAILABLE - return -end diff --git a/src/clh_lock.jl b/src/clh_lock.jl deleted file mode 100644 index 27dafd9..0000000 --- a/src/clh_lock.jl +++ /dev/null @@ -1,155 +0,0 @@ -struct IsLocked end - -mutable struct LockQueueNode - @atomic state::Union{IsLocked,Task,Nothing} - _pad::NTuple{7,Int} - - LockQueueNode(state) = new(state) -end - -@inline _islocked(node::LockQueueNode) = (@atomic :monotonic node.state) isa IsLocked - -# TODO: Compare it with MCSLock -abstract type CLHLock <: Lockable end - -mutable struct NonreentrantCLHLock <: CLHLock - # TODO: node caching - @atomic tail::LockQueueNode - @const _pad::NTuple{7,Int} - current::LockQueueNode -end - -const DUMMY_NODE = LockQueueNode(nothing) - -NonreentrantCLHLock() = - NonreentrantCLHLock(LockQueueNode(nothing), ntuple(_ -> 0, Val(7)), DUMMY_NODE) - -mutable struct ReentrantCLHLock <: CLHLock - # TODO: node caching - @atomic tail::LockQueueNode - @const _pad::NTuple{7,Int} - current::LockQueueNode - @atomic owner::Union{Nothing,Task} - count::Int -end - -ReentrantCLHLock() = - ReentrantCLHLock(LockQueueNode(nothing), ntuple(_ -> 0, Val(7)), DUMMY_NODE, nothing, 0) - -isreentrant(::Lockable) = false -isreentrant(::ReentrantCLHLock) = true - -function handle_reentrant_acquire(lock) - isreentrant(lock) || return false - if (@atomic :monotonic lock.owner) === current_task() - lock.count += 1 - return true - end - return false -end - -function start_reentrant_acquire(lock) - isreentrant(lock) || return - @atomic :monotonic lock.owner = current_task() - lock.count = 1 - return -end - -function handle_reentrant_release(lock) - isreentrant(lock) || return false - @assert (@atomic :monotonic lock.owner) === current_task() - if (lock.count -= 1) > 0 - return true - end - @atomic :monotonic lock.owner = nothing - return false -end - -function ConcurrentUtils.try_race_acquire(lock::CLHLock; nspins = -∞, ntries = -∞) - handle_reentrant_acquire(lock) && return Ok(nothing) - pred = @atomic :monotonic lock.tail - local ns::Int = 0 - while _islocked(pred) - # Check this first so that no loop is executed if `nspins == -∞` - ns < nspins || return Err(TooManyTries(ns, 0)) - spinloop() - ns += 1 - end - - local nt::Int = 0 - node = LockQueueNode(IsLocked()) - while true - _, ok = @atomicreplace(:acquire_release, :acquire, lock.tail, pred => node) - if ok - start_reentrant_acquire(lock) - lock.current = node - return Ok(nothing) - end - nt += 1 - nt < ntries || return Err(TooManyTries(ns, nt)) - - pred = @atomic :monotonic lock.tail - while _islocked(pred) - ns < nspins || return Err(TooManyTries(ns, nt)) - spinloop() - ns += 1 - end - end -end - -ConcurrentUtils.lock_supports_nspins(::Type{<:CLHLock}) = true - -function Base.lock(lock::CLHLock; nspins = nothing) - handle_reentrant_acquire(lock) && return - - node = LockQueueNode(IsLocked()) - # TODO: do we need acquire for `lock.tail`? - pred = @atomicswap :acquire_release lock.tail = node - if !_islocked(pred) - atomic_fence(:acquire) - # Main.@tlc notlocked - @goto locked - end - for _ in oneto(nspins) - if !_islocked(pred) - atomic_fence(:acquire) - # Main.@tlc spinlock - @goto locked - end - spinloop() - end - - task = current_task() - # The acquire ordering is for `acquire(lock)` semantics. The release ordering is for - # task's fields: - state = @atomicswap :acquire_release pred.state = task - if state isa IsLocked - wait() - @assert pred.state === nothing - # @atomic :monotonic pred.state = nothing # if reusing - else - @assert state === nothing - end - # Main.@tlc waited - - @label locked - start_reentrant_acquire(lock) - lock.current = node - return -end - -function Base.unlock(lock::CLHLock) - handle_reentrant_release(lock) && return - node = lock.current - # The release ordering is for `release(lock)` semantics. The acquire ordering is for - # task's fields: - state = @atomicswap :acquire_release node.state = nothing - if state isa Task - # The waiter is already sleeping. Wake it up. - task = state::Task - schedule(task) - else - @assert state isa IsLocked # i.e., not `nothing` - end - return -end diff --git a/src/docs/NonreentrantBackoffSpinLock.md b/src/docs/NonreentrantBackoffSpinLock.md deleted file mode 100644 index c2eb626..0000000 --- a/src/docs/NonreentrantBackoffSpinLock.md +++ /dev/null @@ -1,32 +0,0 @@ - NonreentrantBackoffSpinLock - -A non-reentrant exponential backoff spin lock. - -See also [`ReentrantBackoffSpinLock`](@ref) that provides a reentrant version. - -# Extended help - -`NonreentrantBackoffSpinLock` performs better than `Base.Threads.SpinLock` with high -contention. [`NonreentrantCLHLock`](@ref) is better than `NonreentrantBackoffSpinLock` with -high contention with many worker threads (20 to 80; it depends on the machine). - -Since `NonreentrantBackoffSpinLock` does not have a fallback "cooperative" waiting -mechanism, [`NonreentrantCLHLock`](@ref) is in general recommended. - -## Memory ordering - -`NonreentrantBackoffSpinLock` has the same semantics as [`ReentrantBackoffSpinLock`](@ref) -provided that each hand-off of the lock between tasks (if any) establishes a happened-before -edge. - -## Supported operations - -* `NonreentrantBackoffSpinLock(; [mindelay], [maxdelay]) -> lock`: Create a `lock`. - `mindelay` (default: 1) specifies the number of [`spinloop`](@ref) called in the initial - backoff. `mindelay` (default: 1000) specifies the maximum backoff. -* [`acquire(lock::NonreentrantBackoffSpinLock; [mindelay], [maxdelay])`](@ref acquire) - (`lock`): Acquire the `lock`. Keyword arguments `mindelay` and `maxdelay` can be passed - to override the values specified by the constructor. -* [`try_race_acquire(lock::NonreentrantBackoffSpinLock)`](@ref try_race_acquire) - (`trylock`): -* [`release(lock::NonreentrantBackoffSpinLock)`](@ref) (`unlock`) diff --git a/src/docs/NonreentrantCLHLock.md b/src/docs/NonreentrantCLHLock.md deleted file mode 100644 index ebe1ad9..0000000 --- a/src/docs/NonreentrantCLHLock.md +++ /dev/null @@ -1,24 +0,0 @@ - NonreentrantCLHLock - -A (non-reentrant) CLH "spinnable" lock that provides first-come-first-served fairness. -Keyword argument `nspins::Integer` can be passed to [`acquire`](@ref) to specify a number of -spins tried before falling back to "cooperative" waiting in the Julia scheduler. - -# Extended help - -`NonreentrantCLHLock` implements the spin lock by Craig (1993) and Magnussen, Landin, and -Hagersten (1994) with a fallback to "cooperatively" wait in the scheduler instead of -spinning (hence "spinnable"). See [`ReentrantCLHLock`](@ref) that provides a reentrant -version. - -## Memory ordering - -`NonreentrantCLHLock` has the same semantics as [`ReentrantCLHLock`](@ref) provided that -each hand-off of the lock between tasks (if any) establishes a happened-before edge. - -## Supported operations - -* [`acquire(lock::NonreentrantCLHLock; [nspins::Integer])`](@ref acquire) (`lock`) -* [`try_race_acquire(lock::NonreentrantCLHLock)`](@ref try_race_acquire) (`trylock`): Not very - efficient but lock-free. Fail with `AcquiredByWriterError`. -* [`release(lock::NonreentrantCLHLock)`](@ref) (`unlock`) diff --git a/src/docs/ReentrantBackoffSpinLock.md b/src/docs/ReentrantBackoffSpinLock.md deleted file mode 100644 index 6e6f75e..0000000 --- a/src/docs/ReentrantBackoffSpinLock.md +++ /dev/null @@ -1,24 +0,0 @@ - ReentrantBackoffSpinLock - -A reentrant exponential backoff spin lock. - -See also [`NonreentrantBackoffSpinLock`](@ref) that provides a non-reentrant version. - -# Extended help - -## Memory ordering - -A `release` invocation on a `lock` establishes happened-before edges to subsequent -invocations of `acquire` and `try_race_acquire` that returns an `Ok` on the same `lock`. - -## Supported operations - -* `ReentrantBackoffSpinLock(; [mindelay], [maxdelay]) -> lock`: Create a `lock`. - `mindelay` (default: 1) specifies the number of [`spinloop`](@ref) called in the initial - backoff. `mindelay` (default: 1000) specifies the maximum backoff. -* [`acquire(lock::ReentrantBackoffSpinLock; [mindelay], [maxdelay])`](@ref acquire) - (`lock`): Acquire the `lock`. Keyword arguments `mindelay` and `maxdelay` can be passed - to override the values specified by the constructor. -* [`try_race_acquire(lock::ReentrantBackoffSpinLock)`](@ref try_race_acquire) - (`trylock`): -* [`release(lock::ReentrantBackoffSpinLock)`](@ref) (`unlock`) diff --git a/src/docs/ReentrantCLHLock.md b/src/docs/ReentrantCLHLock.md deleted file mode 100644 index b26a05a..0000000 --- a/src/docs/ReentrantCLHLock.md +++ /dev/null @@ -1,24 +0,0 @@ - ReentrantCLHLock - -A reentrant CLH "spinnable" lock that provides first-come-first-served fairness. Keyword -argument `nspins::Integer` can be passed to [`acquire`](@ref) to specify a number of spins -tried before falling back to "cooperative" waiting in the Julia scheduler. - -# Extended help - -`ReentrantCLHLock` implements the spin lock by Craig (1993) and Magnussen, Landin, and -Hagersten (1994) with a fallback to "cooperatively" wait in the scheduler instead of -spinning (hence "spinnable"). See [`NonreentrantCLHLock`](@ref) that provides a -non-reentrant version. - -## Memory ordering - -A `release` invocation on a `lock` establishes happened-before edges to subsequent -invocations of `acquire` and `try_race_acquire` that returns an `Ok` on the same `lock`. - -## Supported operations - -* [`acquire(lock::ReentrantCLHLock; [nspins::Integer])`](@ref acquire) (`lock`) -* [`try_race_acquire(lock::ReentrantCLHLock)`](@ref try_race_acquire) (`trylock`): Not very efficient - but lock-free. Fail with `AcquiredByWriterError`. -* [`release(lock::ReentrantCLHLock)`](@ref) (`unlock`) diff --git a/src/docs/TaskObliviousLock.md b/src/docs/TaskObliviousLock.md deleted file mode 100644 index e0aa599..0000000 --- a/src/docs/TaskObliviousLock.md +++ /dev/null @@ -1,31 +0,0 @@ - TaskObliviousLock - -A lock that can be released in a task that did not acquire the lock. It does not support -reentrancy. - -# Extended help -## Examples - -```julia -julia> using ConcurrentUtils - -julia> lock = TaskObliviousLock(); - -julia> acquire(lock); - -julia> wait(Threads.@spawn release(lock)); # completes -``` - -## Supported operations - -* [`acquire(lock::TaskObliviousLock)`](@ref acquire) (`lock`) -* [`release(lock::TaskObliviousLock)`](@ref) (`unlock`) - -## Implementation detail - -`TaskObliviousLock` is an alias to unspecified implementation of lock. Currently, it is: - -```julia -julia> TaskObliviousLock === NonreentrantCLHLock -true -``` diff --git a/src/docs/acquire.md b/src/docs/acquire.md deleted file mode 100644 index d4dc025..0000000 --- a/src/docs/acquire.md +++ /dev/null @@ -1,28 +0,0 @@ - acquire(lock) - -Acquire a `lock`. It is equivalent to `Base.lock(lock)` but it may support additional -keyword arguments. - -See also [`release`](@ref) and [`try_race_acquire`](@ref). - -# Extended help - -## Examples -```julia -julia> using ConcurrentUtils - -julia> lock = ReentrantCLHLock(); - -julia> acquire(lock); - -julia> release(lock); -``` - -## On naming - -ConcurrentUtils.jl uses `acquire`/`release` instead of `lock`/`unlock` so that: - -1. Variable `lock` can be used. -2. Make it clear that `ConcurrentUtils.try_race_acquire(lock) -> result::Union{Ok,Err}` and - `Base.trylock(lock) -> locked::Bool` have different return types. In particular, - `try_race_acquire` can report the reason why certain attempt have failed. diff --git a/src/docs/acquire_then.md b/src/docs/acquire_then.md deleted file mode 100644 index cc3a48e..0000000 --- a/src/docs/acquire_then.md +++ /dev/null @@ -1,19 +0,0 @@ - acquire_then(f, lock; acquire_options...) -> y - -Execute a thunk `f` in a critical section protected by `lock` and return the value `y` -returned from `f`. Keyword arguments are passed to [`acquire`](@ref). - -# Extended help - -## Examples - -```julia -julia> using ConcurrentUtils - -julia> lock = ReentrantCLHLock(); - -julia> acquire_then(lock; nspins = 10) do - 123 + 456 - end -579 -``` diff --git a/src/docs/race_acquire.md b/src/docs/race_acquire.md deleted file mode 100644 index 8a8bb96..0000000 --- a/src/docs/race_acquire.md +++ /dev/null @@ -1,18 +0,0 @@ - race_acquire(lock) -> isacquired::Bool - -Try to acquire `lock` and return `true` on success and `false` on failure. - -See also [`try_race_acquire`](@ref). - -## Examples -```julia -julia> using ConcurrentUtils - -julia> lock = NonreentrantCLHLock(); - -julia> race_acquire(lock) -true - -julia> race_acquire(lock) -false -``` diff --git a/src/docs/read_write_lock.md b/src/docs/read_write_lock.md index a1a935a..93e7bb5 100644 --- a/src/docs/read_write_lock.md +++ b/src/docs/read_write_lock.md @@ -6,10 +6,9 @@ Return the read handle `rlock` and the write handle `wlock` of a read-write lock Supported operations: -* [`acquire(rlock)`](@ref acquire) (`lock`) -* [`try_race_acquire(rlock; [nspins], [ntries])`](@ref try_race_acquire) (`trylock`): Not - very efficient but lock-free. Fail with `TooManyTries`. -* [`release(rlock)`](@ref) (`unlock`) -* [`acquire(wlock)`](@ref acquire) (`lock`) -* [`try_race_acquire(wlock)`](@ref try_race_acquire) (`trylock`): Fail with `NotAcquirableError`. -* [`release(wlock)`](@ref) (`unlock`) +* `lock(rlock)` +* `trylock(rlock)` (not very efficient but lock-free) +* `unlock(rlock)` +* `lock(wlock)` +* `trylock(wlock)` +* `unlock(wlock)` diff --git a/src/docs/release.md b/src/docs/release.md deleted file mode 100644 index 72735a1..0000000 --- a/src/docs/release.md +++ /dev/null @@ -1,5 +0,0 @@ - release(lock) - -Release a `lock`. It is equivalent to `Base.unlock(lock)`. - -See also [`acquire`](@ref) and [`try_race_acquire`](@ref). diff --git a/src/docs/try_race_acquire.md b/src/docs/try_race_acquire.md deleted file mode 100644 index 5606b94..0000000 --- a/src/docs/try_race_acquire.md +++ /dev/null @@ -1,19 +0,0 @@ - try_race_acquire(lock) -> Ok(nothing) or Err(reason) - -Try to acquire `lock` and return `Ok(nothing)` on success. Return an `Err` wrapping a value -explaining a `reason` of failure. - -See the documentation of `typeof(lock)` for possible error types. - -## Examples -```julia -julia> using ConcurrentUtils - -julia> lock = NonreentrantCLHLock(); - -julia> try_race_acquire(lock) -Try.Ok: nothing - -julia> try_race_acquire(lock) -Try.Err: TooManyTries(0, 0) -``` diff --git a/src/lock_interface.jl b/src/lock_interface.jl index 4567332..f8b643d 100644 --- a/src/lock_interface.jl +++ b/src/lock_interface.jl @@ -1,74 +1,8 @@ -### -### Base.AbstractLock adapters -### - -ConcurrentUtils.acquire(lck::Base.AbstractLock; options...) = lock(lck; options...) -ConcurrentUtils.release(lck::Base.AbstractLock) = unlock(lck) - -ConcurrentUtils.acquire(x) = Base.acquire(x) -ConcurrentUtils.release(x) = Base.release(x) - -ConcurrentUtils.race_acquire(lck) = trylock(lck) - -function ConcurrentUtils.try_race_acquire(lck::Base.AbstractLock) - if trylock(lck) - return Ok(nothing) - else - return Err(NotAcquirableError()) - end -end - -function ConcurrentUtils.acquire_then(f, lock; acquire_options...) - acquire(lock; acquire_options...) - try - return f() - finally - release(lock) - end -end - -ConcurrentUtils.lock_supports_nspins(::Type{<:Base.AbstractLock}) = false - -ConcurrentUtils.lock_supports_nspins(lock) = - ConcurrentUtils.lock_supports_nspins(typeof(lock)) - -need_lock_object() = error("need lock type or object") -ConcurrentUtils.lock_supports_nspins(::Type{Union{}}) = need_lock_object() -ConcurrentUtils.lock_supports_nspins(::Type) = need_lock_object() - -### -### Main ConcurrentUtils' lock interface -### - -abstract type Lockable <: Base.AbstractLock end - -Base.trylock(lck::Lockable) = Try.isok(try_race_acquire(lck)) - -function Base.lock(f, lck::Lockable; options...) - lock(lck; options...) - try - return f() - finally - unlock(lck) - end -end - -#= -function ConcurrentUtils.try_race_acquire_then(f, lock::Lockable) - @? try_race_acquire(lock) - try - return f() - finally - release(lock) - end -end -=# - ### ### Reader-writer lock interface ### -abstract type AbstractReadWriteLock <: Lockable end +abstract type AbstractReadWriteLock <: Base.AbstractLock end function ConcurrentUtils.acquire_read_then(f, lock::AbstractReadWriteLock) acquire_read(lock) @@ -79,21 +13,13 @@ function ConcurrentUtils.acquire_read_then(f, lock::AbstractReadWriteLock) end end -struct WriteLockHandle{RWLock} <: Lockable +struct ReadLockHandle{RWLock} <: Base.AbstractLock rwlock::RWLock end -struct ReadLockHandle{RWLock} <: Lockable - rwlock::RWLock -end - -ConcurrentUtils.try_race_acquire(lock::WriteLockHandle) = try_race_acquire(lock.rwlock) -Base.lock(lck::WriteLockHandle) = acquire(lck.rwlock) -Base.unlock(lck::WriteLockHandle) = release(lck.rwlock) - -ConcurrentUtils.try_race_acquire(lock::ReadLockHandle) = try_race_acquire_read(lock.rwlock) +Base.trylock(lck::ReadLockHandle) = Try.iok(try_race_acquire_read(lck.rwlock)) Base.lock(lck::ReadLockHandle) = acquire_read(lck.rwlock) Base.unlock(lck::ReadLockHandle) = release_read(lck.rwlock) ConcurrentUtils.read_write_lock(lock::AbstractReadWriteLock = ReadWriteLock()) = - (ReadLockHandle(lock), WriteLockHandle(lock)) + (ReadLockHandle(lock), lock) diff --git a/src/naming_convention.md b/src/naming_convention.md index ec05b0c..6682092 100644 --- a/src/naming_convention.md +++ b/src/naming_convention.md @@ -13,5 +13,5 @@ Common verbs that appear as a primitive concept: * `put` * `fetch` -* `acquire` -* `release` +* `lock` +* `unlock` diff --git a/src/read_write_lock.jl b/src/read_write_lock.jl index dc9f709..4988da3 100644 --- a/src/read_write_lock.jl +++ b/src/read_write_lock.jl @@ -96,27 +96,23 @@ function ConcurrentUtils.release_read(rwlock::ReadWriteLock) return end -function ConcurrentUtils.try_race_acquire(rwlock::ReadWriteLock) +function Base.trylock(rwlock::ReadWriteLock) _, success = @atomicreplace( :acquire_release, :monotonic, rwlock.nreaders_and_writelock, NOTLOCKED => WRITELOCK_MASK, ) - if success - return Ok(nothing) - else - return Err(NotAcquirableError()) - end + return success::Bool end function Base.lock(rwlock::ReadWriteLock) - if Try.isok(try_race_acquire(rwlock)) + if trylock(rwlock) return end lock(rwlock.lock) do while true - if Try.isok(try_race_acquire(rwlock)) + if trylock(rwlock) return end wait(rwlock.cond_write) diff --git a/test/ConcurrentUtilsTests/src/ConcurrentUtilsTests.jl b/test/ConcurrentUtilsTests/src/ConcurrentUtilsTests.jl index 485ee26..5852197 100644 --- a/test/ConcurrentUtilsTests/src/ConcurrentUtilsTests.jl +++ b/test/ConcurrentUtilsTests/src/ConcurrentUtilsTests.jl @@ -7,7 +7,6 @@ include("test_tasklet.jl") include("test_thread_local_storage.jl") # Locks -include("test_locks.jl") include("test_read_write_lock.jl") include("test_guards.jl") diff --git a/test/ConcurrentUtilsTests/src/test_locks.jl b/test/ConcurrentUtilsTests/src/test_locks.jl deleted file mode 100644 index 7da5635..0000000 --- a/test/ConcurrentUtilsTests/src/test_locks.jl +++ /dev/null @@ -1,154 +0,0 @@ -module TestLocks - -using ConcurrentUtils -using Test - -using ..Utils: poll_until, unfair_sleep - -function check_minimal_lock_interface(lock) - phase = Threads.Atomic{Int}(0) - acquire(lock) - @sync begin - Threads.@spawn begin - phase[] = 1 - acquire(lock) - release(lock) - phase[] = 2 - end - - @test poll_until(() -> phase[] != 0) - @test phase[] == 1 - sleep(0.01) - @test phase[] == 1 - - release(lock) - end - @test phase[] == 2 - # @test fetch(Threads.@spawn try_race_acquire(lock)) == Err(NotAcquirableError()) - # @test fetch(Threads.@spawn try_race_acquire(lock)) == Ok(nothing) -end - -function test_minimal_lock_interface() - @testset "$(nameof(T))" for T in [ReentrantLock, ReentrantCLHLock, NonreentrantCLHLock] - check_minimal_lock_interface(T()) - end -end - -function check_concurrent_mutex(lock, ntasks, ntries) - ref = Ref(0) - @sync for _ in 1:ntasks - Threads.@spawn for _ in 1:ntries - acquire_then(lock) do - x = ref[] - - # sleep about 3 μs - unfair_sleep(100) - - ref[] = x + 1 - end - end - end - return ref[] -end - -function test_concurrent_mutex() - @testset "$(nameof(T))" for T in [ - ReentrantLock, - ReentrantCLHLock, - NonreentrantCLHLock, - ReentrantBackoffSpinLock, - NonreentrantBackoffSpinLock, - ], - ntasks in [Threads.nthreads(), 64 * Threads.nthreads()] - - ntries = 1000 - actual = check_concurrent_mutex(T(), ntasks, ntries) - @test actual == ntasks * ntries - end -end - -function check_try_race_acquire(lock) - acquire(lock) - @test fetch(Threads.@spawn try_race_acquire(lock)) isa Err - @test !fetch(Threads.@spawn race_acquire(lock)) - @test !fetch(Threads.@spawn trylock(lock)) - release(lock) - @test try_race_acquire(lock) == Ok(nothing) - release(lock) - @test race_acquire(lock) - release(lock) - @test trylock(lock) - release(lock) -end - -function test_try_race_acquire() - @testset "$(nameof(T))" for T in [ - ReentrantLock, - ReentrantCLHLock, - NonreentrantCLHLock, - ReentrantBackoffSpinLock, - NonreentrantBackoffSpinLock, - ] - check_try_race_acquire(T()) - end -end - -function check_concurrent_try_race_acquire(tryacq, lock, ntasks, ntries) - errs = ThreadLocalStorage(Vector{Any}) - atomic = Threads.Atomic{Int}(0) - ref = Ref(0) - @sync for _ in 1:ntasks - Threads.@spawn for _ in 1:ntries - result = tryacq(lock) - if Try.isok(result) - ref[] += 1 - release(lock) - Threads.atomic_add!(atomic, 1) - else - push!(errs[], Try.unwrap_err(result)) - end - end - end - return ref[], atomic[], foldl(append!, unsafe_takestorages!(errs)) -end - -function get_ntries(@nospecialize(e)) - if e isa TooManyTries - e.ntries - else - typemax(Int) - end -end - -function test_concurrent_try_race_acquire() - @testset for ntasks in [Threads.nthreads(), 64 * Threads.nthreads()] - ntries = 1000 - - backofflocks = [ReentrantBackoffSpinLock, NonreentrantBackoffSpinLock] - @testset "$(nameof(T))" for T in backofflocks - @testset "no backoffs" begin - actual, desired, errs = - check_concurrent_try_race_acquire(T(), ntasks, ntries) do lock - try_race_acquire(lock; nspins = 10) - end - @test actual == desired - - @test filter(e -> !(e isa TooManyTries), errs) == [] - @test all(<=(0), map(get_ntries, errs)) - end - - @testset "#backoffs = 3" begin - actual, desired, errs = - check_concurrent_try_race_acquire(T(), ntasks, ntries) do lock - try_race_acquire(lock; nspins = 10000, ntries = 3) - end - @test actual == desired - - @test filter(e -> !(e isa TooManyTries), errs) == [] - @test all(<=(3), map(get_ntries, errs)) - end - end - end -end - -end # module diff --git a/test/ConcurrentUtilsTests/src/test_read_write_lock.jl b/test/ConcurrentUtilsTests/src/test_read_write_lock.jl index 14a1f04..c262247 100644 --- a/test/ConcurrentUtilsTests/src/test_read_write_lock.jl +++ b/test/ConcurrentUtilsTests/src/test_read_write_lock.jl @@ -3,25 +3,45 @@ module TestReadWriteLock using ConcurrentUtils using Test -using ..TestLocks: check_minimal_lock_interface using ..Utils: poll_until, unfair_sleep function test_no_blocks() rlock, wlock = read_write_lock() @sync begin - acquire(rlock) - acquire(rlock) + lock(rlock) + lock(rlock) Threads.@spawn begin - acquire(rlock) - release(rlock) + lock(rlock) + unlock(rlock) end - release(rlock) - release(rlock) + unlock(rlock) + unlock(rlock) end - acquire(wlock) - release(wlock) + lock(wlock) + unlock(wlock) +end + +function check_minimal_lock_interface(lck) + phase = Threads.Atomic{Int}(0) + lock(lck) + @sync begin + Threads.@spawn begin + phase[] = 1 + lock(lck) + unlock(lck) + phase[] = 2 + end + + @test poll_until(() -> phase[] != 0) + @test phase[] == 1 + sleep(0.01) + @test phase[] == 1 + + unlock(lck) + end + @test phase[] == 2 end function test_wlock() @@ -32,11 +52,11 @@ end function test_a_writer_blocks_a_reader() rlock, wlock = read_write_lock() locked = Threads.Atomic{Bool}(false) - @sync acquire_then(wlock) do + @sync lock(wlock) do Threads.@spawn begin - acquire(rlock) + lock(rlock) locked[] = true - release(rlock) + unlock(rlock) end sleep(0.01) @@ -49,11 +69,11 @@ function test_a_writer_blocks_a_writer() _rlock, wlock = read_write_lock() locked = Threads.Atomic{Bool}(false) - @sync acquire_then(wlock) do + @sync lock(wlock) do Threads.@spawn begin - acquire(wlock) + lock(wlock) locked[] = true - release(wlock) + unlock(wlock) end sleep(0.01) @@ -66,11 +86,11 @@ function test_a_reader_blocks_a_writer() rlock, wlock = read_write_lock() locked = Threads.Atomic{Bool}(false) - @sync acquire_then(rlock) do + @sync lock(rlock) do Threads.@spawn begin - acquire(wlock) + lock(wlock) locked[] = true - release(wlock) + unlock(wlock) end sleep(0.01) @@ -88,7 +108,7 @@ function check_concurrent_mutex(nreaders, nwriters, ntries) @sync begin for _ in 1:nreaders Threads.@spawn while true - acquire_then(rlock) do + lock(rlock) do # Threads.atomic_add!(nreads, 1) ref[] < limit end || break @@ -99,7 +119,7 @@ function check_concurrent_mutex(nreaders, nwriters, ntries) for _ in 1:nwriters Threads.@spawn for _ in 1:ntries - acquire_then(wlock) do + lock(wlock) do local x = ref[] # sleep about 3 μs