diff --git a/src/TimeZones.jl b/src/TimeZones.jl index 9e83cc45..fa10cf75 100644 --- a/src/TimeZones.jl +++ b/src/TimeZones.jl @@ -52,6 +52,7 @@ end include("compat.jl") include("utils.jl") include("indexable_generator.jl") +include("readers_writer_lock.jl") include("class.jl") include("utcoffset.jl") diff --git a/src/readers_writer_lock.jl b/src/readers_writer_lock.jl new file mode 100644 index 00000000..a87c6526 --- /dev/null +++ b/src/readers_writer_lock.jl @@ -0,0 +1,61 @@ +using Base.Threads: AbstractLock, Atomic + +struct StateLock{L} <: AbstractLock + state::Atomic{UInt8} +end + +const ReadersLock = StateLock{:Readers} +const WriterLock = StateLock{:Writer} + +function Base.lock(r::ReadersLock) + while true + x = r.state[] + if x != 0xff + y = x + 0x01 + if Threads.atomic_cas!(r.state, x, y) == x + break + end + end + end +end + +function Base.unlock(r::ReadersLock) + Threads.atomic_sub!(r.state, 0x01) +end + +function Base.lock(w::WriterLock) + while true + x = w.state[] + if x == 0x00 + if Threads.atomic_cas!(w.state, x, 0xff) == x + break + end + end + end +end + +function Base.unlock(w::WriterLock) + Threads.atomic_xchg!(w.state, 0x00) +end + +# https://en.wikipedia.org/wiki/Readers–writer_lock +# https://yizhang82.dev/lock-free-rw-lock + +""" + ReadersWriterLock + +Allow for concurrent read-only operations, while providing exclusive access for write +operations. +""" +struct ReadersWriterLock + readers::ReadersLock + writer::WriterLock + + function ReadersWriterLock() + state = Threads.Atomic{UInt8}(0) + readers = ReadersLock(state) + writer = WriterLock(state) + + return new(readers, writer) + end +end diff --git a/src/types/timezone.jl b/src/types/timezone.jl index 7e8289b1..a3be695d 100644 --- a/src/types/timezone.jl +++ b/src/types/timezone.jl @@ -1,4 +1,6 @@ const TIME_ZONE_CACHE = Dict{String,Tuple{TimeZone,Class}}() +const TZ_CACHE_LOCK = ReadersWriterLock() + """ TimeZone(str::AbstractString) -> TimeZone @@ -41,24 +43,36 @@ US/Pacific (UTC-8/UTC-7) TimeZone(::AbstractString, ::Class) function TimeZone(str::AbstractString, mask::Class=Class(:DEFAULT)) + lock(TZ_CACHE_LOCK.readers) + # Note: If the class `mask` does not match the time zone we'll still load the # information into the cache to ensure the result is consistent. - tz, class = get!(TIME_ZONE_CACHE, str) do - tz_path = joinpath(TZData.COMPILED_DIR, split(str, "/")...) - - if isfile(tz_path) - open(deserialize, tz_path, "r") - elseif occursin(FIXED_TIME_ZONE_REGEX, str) - FixedTimeZone(str), Class(:FIXED) - elseif !isdir(TZData.COMPILED_DIR) || isempty(readdir(TZData.COMPILED_DIR)) - # Note: Julia 1.0 supresses the build logs which can hide issues in time zone - # compliation which result in no tzdata time zones being available. - throw(ArgumentError( - "Unable to find time zone \"$str\". Try running `TimeZones.build()`." - )) - else - throw(ArgumentError("Unknown time zone \"$str\"")) + if haskey(TIME_ZONE_CACHE, str) + tz, class = TIME_ZONE_CACHE[str] + unlock(TZ_CACHE_LOCK.readers) + else + unlock(TZ_CACHE_LOCK.readers) + lock(TZ_CACHE_LOCK.writer) + + tz, class = get!(TIME_ZONE_CACHE, str) do + tz_path = joinpath(TZData.COMPILED_DIR, split(str, "/")...) + + if isfile(tz_path) + open(deserialize, tz_path, "r") + elseif occursin(FIXED_TIME_ZONE_REGEX, str) + FixedTimeZone(str), Class(:FIXED) + elseif !isdir(TZData.COMPILED_DIR) || isempty(readdir(TZData.COMPILED_DIR)) + # Note: Julia 1.0 supresses the build logs which can hide issues in time zone + # compliation which result in no tzdata time zones being available. + throw(ArgumentError( + "Unable to find time zone \"$str\". Try running `TimeZones.build()`." + )) + else + throw(ArgumentError("Unknown time zone \"$str\"")) + end end + + unlock(TZ_CACHE_LOCK.writer) end if mask & class == Class(:NONE)