1+ """
2+ TaskState
3+
4+ When a `Task` acquires the GIL, save the GIL state and the stickiness of the
5+ `Task` since we will force the `Task` to be sticky. We need to restore the GIL
6+ state on release of the GIL via `C.PyGILState_Release`.
7+ """
18struct TaskState
29 task:: Task
310 sticky:: Bool # original stickiness of the task
411 state:: C.PyGILState_STATE
512end
613
14+ """
15+ TaskStack
16+
17+ For each thread the `TaskStack` maintains a first-in-last-out list of tasks
18+ as well as the GIL state and their stickiness upon entering the stack. This
19+ forces tasks to unlock the GIL in the reverse order of which they locked it.
20+ """
721struct TaskStack
822 stack:: Vector{TaskState}
923 count:: IdDict{Task,Int}
@@ -43,7 +57,7 @@ function Base.pop!(task_stack::TaskStack)::Task
4357 # If 0, remove it from the key set
4458 pop! (task_stack. count, task)
4559 else
46- task_stack[task] = count
60+ task_stack. count [task] = count
4761 end
4862
4963 C. PyGILState_Release (gil_state)
@@ -57,42 +71,56 @@ function Base.pop!(task_stack::TaskStack)::Task
5771
5872 return task
5973end
74+ Base. in (task:: Task , task_stack:: TaskStack ) = haskey (task_stack. count)
6075Base. isempty (task_stack:: TaskStack ) = isempty (task_stack. stack)
6176
6277if ! isdefined (Base, :OncePerThread )
6378
79+ const PerThreadLock = Base. ThreadSynchronizer ()
80+
6481 # OncePerThread is implemented in full in Julia 1.12
65- # This implementation is meant for compatibility with Julia 1.10 and 1.11
66- # and only supports a static number of threads. Use Julia 1.12 for dynamic
67- # thread usage.
82+ # This implementation is meant for compatibility with Julia 1.10 and 1.11.
83+ # Using Julia 1.12 is recommended.
6884 mutable struct OncePerThread{T,F} <: Function
69- @atomic xs:: Vector{ T} # values
70- @atomic ss:: Vector{ UInt8} # states: 0=initial, 1=hasrun, 2=error, 3==concurrent
85+ @atomic xs:: Dict{Int, T} # values
86+ @atomic ss:: Dict{Int, UInt8} # states: 0=initial, 1=hasrun, 2=error, 3==concurrent
7187 const initializer:: F
7288 function OncePerThread {T,F} (initializer:: F ) where {T,F}
7389 nt = Threads. maxthreadid ()
74- return new {T,F} (Vector { T} (undef, nt ), zeros (UInt8, nt ), initializer)
90+ return new {T,F} (Dict {Int, T} (), Dict {Int,UInt8} ( ), initializer)
7591 end
7692 end
7793 OncePerThread {T} (initializer:: Type{U} ) where {T, U} = OncePerThread {T,Type{U}} (initializer)
7894 (once:: OncePerThread{T,F} )() where {T,F} = once[Threads. threadid ()]
7995 function Base. getindex (once:: OncePerThread , tid:: Integer )
80- tid = Threads . threadid ( )
96+ tid = Int (tid )
8197 ss = @atomic :acquire once. ss
8298 xs = @atomic :monotonic once. xs
83- if checkbounds (Bool, xs, tid)
84- if ss[tid] == 0
99+
100+ if haskey (ss, tid) && ss[tid] == 1
101+ return xs[tid]
102+ end
103+
104+ Base. lock (PerThreadLock)
105+ try
106+ state = get (ss, tid, 0 )
107+ if state == 0
85108 xs[tid] = once. initializer ()
86109 ss[tid] = 1
87110 end
88- return xs[tid]
89- else
90- throw (ErrorException (" Thread id $tid is out of bounds as initially allocated. Use Julia 1.12 for dynamic thread usage." ))
111+ finally
112+ Base. unlock (PerThreadLock)
91113 end
114+ return xs[tid]
92115 end
93-
94116end
95117
118+ """
119+ GlobalInterpreterLock
120+
121+ Provides a thread aware reentrant lock around Python's interpreter lock that
122+ ensures that `Task`s acquiring the lock stay on the same thread.
123+ """
96124struct GlobalInterpreterLock <: Base.AbstractLock
97125 lock_owners:: OncePerThread{TaskStack}
98126 function GlobalInterpreterLock ()
@@ -105,16 +133,44 @@ function Base.lock(gil::GlobalInterpreterLock)
105133end
106134function Base. unlock (gil:: GlobalInterpreterLock )
107135 lock_owner:: TaskStack = gil. lock_owners ()
108- while last (lock_owner) != current_task ()
109- wait (lock_owner. condvar)
136+ last_owner:: Task = if isempty (lock_owner)
137+ current_task ()
138+ else
139+ last (lock_owner)
140+ end
141+ while last_owner != current_task ()
142+ if istaskdone (last_owner) && ! isempty (lock_owner)
143+ # Last owner is done and unable to unlock the GIL
144+ pop! (lock_owner)
145+ error (" Unlock from the wrong task. The Task that owned the GIL is done and did not unlock the GIL: $(last_owner) " )
146+ else
147+ # This task does not own the GIL. Wait to unlock the GIL until
148+ # another task successfully unlocks the GIL.
149+ wait (lock_owner. condvar)
150+ end
151+ last_owner = if isempty (lock_owner)
152+ current_task ()
153+ else
154+ last (lock_owner)
155+ end
156+ end
157+ if isempty (lock_owner)
158+ error (" Unlock from wrong task: $(current_task) . No tasks on this thread own the lock." )
159+ else
160+ task = pop! (lock_owner)
110161 end
111- task = pop! (lock_owner)
112162 @assert task == current_task ()
113163 return nothing
114164end
115165function Base. islocked (gil:: GlobalInterpreterLock )
116- # TODO : handle Julia 1.10 and 1.11 case when have not allocated up to maxthreadid
117166 return any (! isempty (gil. lock_owners[thread_index]) for thread_index in 1 : Threads. maxthreadid ())
118167end
168+ function haslock (gil:: GlobalInterpreterLock , task:: Task )
169+ lock_owner:: TaskStack = gil. lock_owners ()
170+ if isempty (lock_owner)
171+ return false
172+ end
173+ return last (lock_owner):: Task == task
174+ end
119175
120176const _GIL = GlobalInterpreterLock ()
0 commit comments