Attempting to make my module code Threadsafe

I wrote my module PDFPs.jl to implement Precision Decimal Floating Point object. I was very very careful to make my code purely functional with pure functions. So I thought that my code is thread safe by default.

But alas, i reaised that it depends heavily on one “Global Variable” called DEFAULT_PRECISION

module PDFPs

    # Use the Ref hack to speed up DEFAULT_PRECISION
    # to get the value use DEFAULT_PRECISION[]
    # to set the value use DEFAULT_PRECISION[] = newvalue
    const DEFAULT_PRECISION = Ref(16)

    @inline function PDFP_getDefaultPrecision()
        DEFAULT_PRECISION[]
    end

    @inline function PDFP_setDefaultPrecision(prec::Int)
        global DEFAULT_PRECISION[] = prec < 0 ? 0 : prec
    end

end

It took me a while to realised that this is totally unsafe as when I have multiple threads running, each thread could be changing the DEFAULT_PRECISION and interferring with each other. What I need is a local copy of DEFAULT_PRECISION for each thread. So I came up with this.

module PDFPs

const DEFAULT_PRECISION = Ref(16)

# Thread id for the main thread is always 1, so put in the local copy
LocalCopy_DefaultPrecision = Dict{Int64,Int64}(1=>DEFAULT_PRECISION[])
Mutex_LocalCopy_DefaultPrecision = Base.Threads.SpinLock()

function PDFP_getDefaultPrecision()
    thread_id = Base.Threads.threadid()
    Base.Threads.lock(Mutex_LocalCopy_DefaultPrecision)
    if ! haskey(LocalCopy_DefaultPrecision,thread_id)
        LocalCopy_DefaultPrecision[thread_id] = DEFAULT_PRECISION[]
    end
    result = LocalCopy_DefaultPrecision[thread_id]
    Base.Threads.unlock(Mutex_LocalCopy_DefaultPrecision)
    return result
end

function PDFP_setDefaultPrecision(prec::Int)
    thread_id = Base.Threads.threadid()
    Base.Threads.lock(Mutex_LocalCopy_DefaultPrecision)
    LocalCopy_DefaultPrecision[thread_id] = prec < 0 ? 0 : prec
    Base.Threads.unlock(Mutex_LocalCopy_DefaultPrecision)
end

# Next we need to replace all DefaultPrecision[] with
# PDFP_getDefaultPrecision()
#
# And replace all DefaultPrecision[]=<number> with
# PDFP_setDefaultPrecision(<number>)

end

Would my new code make my module Thread safe for Julia 1.2 and 1.3?

Do you think it is possible to have a magical MACRO called @localtoeachthread so that I can simple do this instead?

module PDFPs

    # Use the Ref hack to speed up DEFAULT_PRECISION
    # to get the value use DEFAULT_PRECISION[]
    # to set the value use DEFAULT_PRECISION[] = newvalue
    @localtoeachthread  const DEFAULT_PRECISION = Ref(16)

    @inline function PDFP_getDefaultPrecision()
        DEFAULT_PRECISION[]
    end

    @inline function PDFP_setDefaultPrecision(prec::Int)
        global DEFAULT_PRECISION[] = prec < 0 ? 0 : prec
    end

end

It seems quite complicated. Why not something like:

const DEFAULT_PRECISION = Int[]

function __init__()
    resize!(DEFAULT_PRECISION, Threads.nthreads())
    fill!(DEFAULT_PRECISION, 16)
end

get_default_precision() = DEFAULT_PRECISION[Threads.threadid()]
set_default_precision(prec::Int) = DEFAULT_PRECISION[Threads.threadid()] = prec

__init__() runs when the module is loaded.

(Note that this will likely suffer from false sharing, https://en.wikipedia.org/wiki/False_sharing, but it might not be a problem in practice).

2 Likes

But what if the number of threads changes during the execution of the program? My solution would work even if the number of threads changes dynamically.

That doesn’t happen though. The number of threads is set at Julia startup.

1 Like

What about addprocs() doesn’t this increases the number of threads?

Nope, that increases the number of processes which are not using shared memory but distributed memory.

1 Like

Not sure what the usage model is, but I feel like you should create a “Context” object that has the default precision, then all your methods would take this Context as a parameter.

My worry is, function A sets the default precision does some operations, calls function B which changes the default precision, does some operations, returns, function A continues to run but the default precision has changed. It would not be obvious looking at function A that the precision HAS changed.

It’s a recipe for hidden bugs, especially if the first pass of function B doesn’t change the default precision, but at some later point code is updated to change the default precision. If you don’t restore the precision when B exits, function A is going to operate differently.

This all assumes the default precision gets changed often, by your question it sounds like it does. The other option is you require the client set it once and it doesn’t get changed…

Changing the constant value of a default defeats a bit its purpose in the first place i.e. to be a constant value with which some other local mutable gets initialized.

thanks pixel27

My worry is, function A sets the default precision does some operations, calls function B which changes the default precision, does some operations, returns, function A continues to run but the default precision has changed. It would not be obvious looking at function A that the precision HAS changed.

That is not an issue as only humans on the Julia REPL can call PDFP_setDefaultPrecision(). Functions within PDFPs.jl does not change the DEFAULT_PRECISION but rather uses it as a constant.

My real worry is that julia code from other people uses my module PDFPs.jl and calls PDFP_setDefaultPrecision() from multiple threads.

If you are referring to other people’s code, then there is nothing I can do about it as I cannot stop other coders from writing stupid codes when using PDFP_setDefaultPrecision().

Changing the constant value of a default defeats a bit its purpose in the first place i.e. to be a constant value with which some other local mutable gets initialized.

No it is pretty useful on the REPL

julia> using PDFPs

julia> PDFP(pi)
PDFP(0, 0, [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9, 3])

julia> PDFP_setDefaultPrecision(3); PDFP(pi)
PDFP(0, 0, [3, 1, 4])

Thank you to everyone. I got my module to be ThreadSafe

julia> Base.Threads.nthreads()
8

julia> using PDFPs

julia> PDFPs.DEFAULT_PRECISION[]
8-element Array{Int64,1}:
 16
 16
 16
 16
 16
 16
 16
 16

julia> PDFP_setDefaultPrecision(4)
4

julia> PDFPs.DEFAULT_PRECISION[]
8-element Array{Int64,1}:
  4
 16
 16
 16
 16
 16
 16
 16

julia> Base.Threads.threadid()
1

julia> PDFP_getDefaultPrecision()
4