Skip to content

Commit

Permalink
Fix precompilation issues arising from use of @sync macros.
Browse files Browse the repository at this point in the history
Macros and precompilation do not necessarily mix in Julia. If
macro evaluation is dependent on program state and the program
state changes later, the macro does not get reevaluated.

However, Julia does track function dependencies, so we can force
recompilation by altering any underlying regular functions.
  • Loading branch information
rbehrends committed Aug 7, 2020
1 parent 8dfd851 commit b4125ba
Showing 1 changed file with 67 additions and 48 deletions.
115 changes: 67 additions & 48 deletions src/sync.jl
Original file line number Diff line number Diff line change
@@ -1,73 +1,92 @@
module Sync
mutable struct LockStatus
nested :: Int
owner :: Union{Task, Nothing}
end

const mutex = ReentrantLock()
const lock_status = LockStatus(0, nothing)

@inline is_locked() = lock_status.owner == Base.current_task()

@inline function lock()
if is_locked()
lock_status.nested += 1
else
Base.lock(mutex)
lock_status.nested = 1
lock_status.owner = Base.current_task()
end
Base.lock(mutex)
end

@inline function unlock()
@assert is_locked()
lock_status.nested -= 1
if lock_status.nested == 0
lock_status.owner = nothing
Base.unlock(mutex)
end
Base.unlock(mutex)
end

@inline function check_lock()
@assert is_locked()
@assert mutex.locked_by === Base.current_task()
end
end

macro sync(expr)
if Threads.nthreads() > 1
quote
# To switch between multi-threaded and single-threaded mode, we
# define functions that install the appropriate handlers for sync()
# etc.
#
# This is necessary because otherwise precompilation would fix
# the mode at whatever state it was during precompilation. Thus,
# initially loading GAP.jl in single-threaded mode would also keep
# synchronization off in multi-threaded mode.
#
# However, Julia tracks function dependencies. If a function changes
# upon which another depends, both are being recompiled. Thus,
# by installing the proper version during __init__(), we force
# selective recompilation of the affected functions as needed.

function enable_sync()
Sync.eval(:(@inline function sync(f::Function)
try
Sync.lock()
$(esc(expr))
lock()
f()
finally
Sync.unlock()
unlock()
end
end))
Sync.eval(:(@inline function sync_noexcept(f::Function)
lock()
t = f()
unlock()
t
end))
Sync.eval(:(@inline function check_sync(f::Function)
check_lock()
f()
end))
end

function disable_sync()
Sync.eval(:(@inline function sync(f::Function)
f()
end))
Sync.eval(:(@inline function sync_noexcept(f::Function)
f()
end))
Sync.eval(:(@inline function check_sync(f::Function)
f()
end))
end

# Initialization is tricky. __init__() can be called from
# within the first sync() call if the module has already
# been precompiled. Thus, we default to enable_sync() for
# precompilation and then set the actual sync mode during
# __init__(). Dropping back from sync enabled to being
# disabled is safe, but not the other way round.

enable_sync()

function __init__()
if Threads.nthreads() > 1
enable_sync()
else
disable_sync()
end
else
:( $(esc(expr)) )
end
end

macro sync(expr)
:( Sync.sync(()->$(esc(expr))) )
end

macro sync_noexcept(expr)
if Threads.nthreads() > 1
quote
Sync.lock()
local t = $(esc(expr))
Sync.unlock()
t
end
else
:( $(esc(expr)) )
end
:( Sync.sync_noexcept(()->$(esc(expr))) )
end

macro check_sync(expr)
if Threads.nthreads() > 1
quote
Sync.check_lock()
$(esc(expr))
end
else
:( $(esc(expr)) )
end
:( Sync.check_sync(()->$(esc(expr))) )
end

0 comments on commit b4125ba

Please sign in to comment.