From b4125baad88849519de81d4bda02655b92b297f5 Mon Sep 17 00:00:00 2001 From: Reimer Behrends Date: Fri, 7 Aug 2020 06:41:54 +0200 Subject: [PATCH] Fix precompilation issues arising from use of @sync macros. 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. --- src/sync.jl | 115 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 67 insertions(+), 48 deletions(-) diff --git a/src/sync.jl b/src/sync.jl index 03dd3b3fb..1c84c0f2d 100644 --- a/src/sync.jl +++ b/src/sync.jl @@ -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