Skip to content

Commit 00f846e

Browse files
authored
Implement _thread.RLock (#1922)
* Implement _thread.RLock * Enable some test_threading tests * Get tests passing * Disable test on Mono * Fix repr
1 parent 21007e9 commit 00f846e

File tree

5 files changed

+187
-13
lines changed

5 files changed

+187
-13
lines changed

src/core/IronPython.Modules/_thread.cs

Lines changed: 139 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public static void PerformModuleReload(PythonContext/*!*/ context, PythonDiction
3535

3636
#region Public API Surface
3737

38-
public static double TIMEOUT_MAX = 0; // TODO: fill this with a proper value
38+
public static double TIMEOUT_MAX = Math.Floor(TimeSpan.MaxValue.TotalSeconds);
3939

4040
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes")]
4141
public static readonly PythonType LockType = DynamicHelpers.GetPythonTypeFromType(typeof(@lock));
@@ -138,23 +138,33 @@ public static object _set_sentinel(CodeContext context) {
138138

139139
#endregion
140140

141+
#nullable enable
142+
141143
[PythonType, PythonHidden]
142144
public sealed class @lock {
143-
private AutoResetEvent blockEvent;
144-
private Thread curHolder;
145+
private AutoResetEvent? blockEvent;
146+
private Thread? curHolder;
145147

146148
public object __enter__() {
147-
acquire(true, -1);
149+
acquire();
148150
return this;
149151
}
150152

151153
public void __exit__(CodeContext/*!*/ context, [NotNone] params object[] args) {
152154
release(context);
153155
}
154156

155-
public bool acquire(bool blocking = true, float timeout = -1) {
157+
public bool acquire(bool blocking = true, double timeout = -1) {
158+
var timespan = Timeout.InfiniteTimeSpan;
159+
160+
if (timeout != -1) {
161+
if (!blocking) throw PythonOps.ValueError("can't specify a timeout for a non-blocking call");
162+
if (timeout < 0) throw PythonOps.ValueError("timeout value must be a non-negative number");
163+
timespan = TimeSpan.FromSeconds(timeout);
164+
}
165+
156166
for (; ; ) {
157-
if (Interlocked.CompareExchange<Thread>(ref curHolder, Thread.CurrentThread, null) == null) {
167+
if (Interlocked.CompareExchange(ref curHolder, Thread.CurrentThread, null) is null) {
158168
return true;
159169
}
160170
if (!blocking) {
@@ -166,16 +176,16 @@ public bool acquire(bool blocking = true, float timeout = -1) {
166176
CreateBlockEvent();
167177
continue;
168178
}
169-
if (!blockEvent.WaitOne(timeout < 0 ? Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(timeout))) {
179+
if (!blockEvent.WaitOne(timespan)) {
170180
return false;
171181
}
172182
GC.KeepAlive(this);
173183
}
174184
}
175185

176186
public void release(CodeContext/*!*/ context) {
177-
if (Interlocked.Exchange<Thread>(ref curHolder, null) == null) {
178-
throw PythonExceptions.CreateThrowable((PythonType)context.LanguageContext.GetModuleState("threaderror"), "lock isn't held", null);
187+
if (Interlocked.Exchange(ref curHolder, null) is null) {
188+
throw PythonOps.RuntimeError("release unlocked lock");
179189
}
180190
if (blockEvent != null) {
181191
// if this isn't set yet we race, it's handled in Acquire()
@@ -184,18 +194,135 @@ public void release(CodeContext/*!*/ context) {
184194
}
185195
}
186196

187-
public bool locked() {
188-
return curHolder != null;
197+
public bool locked()
198+
=> curHolder is not null;
199+
200+
public string __repr__() {
201+
if (curHolder is null) {
202+
return $"<unlocked _thread.lock object at 0x{IdDispenser.GetId(this):X16}>";
203+
}
204+
return $"<locked _thread.lock object at 0x{IdDispenser.GetId(this):X16}>";
189205
}
190206

191207
private void CreateBlockEvent() {
192208
AutoResetEvent are = new AutoResetEvent(false);
193-
if (Interlocked.CompareExchange<AutoResetEvent>(ref blockEvent, are, null) != null) {
209+
if (Interlocked.CompareExchange(ref blockEvent, are, null) is not null) {
194210
are.Close();
195211
}
196212
}
197213
}
198214

215+
[PythonType]
216+
public sealed class RLock {
217+
private AutoResetEvent? blockEvent;
218+
private Thread? curHolder;
219+
private int count;
220+
221+
public object __enter__() {
222+
acquire();
223+
return this;
224+
}
225+
226+
public void __exit__(CodeContext/*!*/ context, [NotNone] params object[] args) {
227+
release();
228+
}
229+
230+
public bool acquire(bool blocking = true, double timeout = -1) {
231+
var timespan = Timeout.InfiniteTimeSpan;
232+
233+
if (timeout != -1) {
234+
if (!blocking) throw PythonOps.ValueError("can't specify a timeout for a non-blocking call");
235+
if (timeout < 0) throw PythonOps.ValueError("timeout value must be a non-negative number");
236+
timespan = TimeSpan.FromSeconds(timeout);
237+
}
238+
239+
var currentThread = Thread.CurrentThread;
240+
241+
for (; ; ) {
242+
var previousThread = Interlocked.CompareExchange(ref curHolder, currentThread, null);
243+
if (previousThread == currentThread) {
244+
count++;
245+
return true;
246+
}
247+
if (previousThread is null) {
248+
count = 1;
249+
return true;
250+
}
251+
if (!blocking) {
252+
return false;
253+
}
254+
if (blockEvent is null) {
255+
// try again in case someone released us, checked the block
256+
// event and discovered it was null so they didn't set it.
257+
CreateBlockEvent();
258+
continue;
259+
}
260+
if (!blockEvent.WaitOne(timespan)) {
261+
return false;
262+
}
263+
GC.KeepAlive(this);
264+
}
265+
}
266+
267+
public void release() {
268+
var currentThread = Thread.CurrentThread;
269+
270+
if (curHolder != currentThread) {
271+
throw PythonOps.RuntimeError("cannot release un-acquired lock");
272+
}
273+
if (--count > 0) {
274+
return;
275+
}
276+
277+
if (Interlocked.Exchange(ref curHolder, null) is null) {
278+
throw PythonOps.RuntimeError("release unlocked lock");
279+
}
280+
if (blockEvent is not null) {
281+
// if this isn't set yet we race, it's handled in acquire()
282+
blockEvent.Set();
283+
GC.KeepAlive(this);
284+
}
285+
}
286+
287+
public string __repr__() {
288+
if (curHolder is null) {
289+
return $"<unlocked _thread.RLock object owner=0 count=0 at 0x{IdDispenser.GetId(this):X16}>";
290+
}
291+
return $"<locked _thread.RLock object owner={curHolder?.ManagedThreadId} count={count} at 0x{IdDispenser.GetId(this):X16}>";
292+
}
293+
294+
public void _acquire_restore([NotNone] PythonTuple state) {
295+
acquire();
296+
count = (int)state[0]!;
297+
curHolder = (Thread?)state[1];
298+
}
299+
300+
public PythonTuple _release_save() {
301+
var count = Interlocked.Exchange(ref this.count, 0);
302+
if (count == 0) {
303+
throw PythonOps.RuntimeError("cannot release un-acquired lock");
304+
}
305+
306+
// release
307+
var owner = Interlocked.Exchange(ref curHolder, null);
308+
blockEvent?.Set();
309+
310+
return PythonTuple.MakeTuple(count, owner);
311+
}
312+
313+
public bool _is_owned()
314+
=> curHolder == Thread.CurrentThread;
315+
316+
private void CreateBlockEvent() {
317+
AutoResetEvent are = new AutoResetEvent(false);
318+
if (Interlocked.CompareExchange(ref blockEvent, are, null) != null) {
319+
are.Close();
320+
}
321+
}
322+
}
323+
324+
#nullable restore
325+
199326
#region Internal Implementation details
200327

201328
private static Thread CreateThread(CodeContext/*!*/ context, ThreadStart start) {

src/core/IronPython.StdLib/lib/test/script_helper.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ def _interpreter_requires_environment():
3939
"""
4040
global __cached_interp_requires_environment
4141
if __cached_interp_requires_environment is None:
42+
# https://github.com/IronLanguages/ironpython3/issues/1440
43+
if sys.implementation.name == "ironpython":
44+
__cached_interp_requires_environment = True
45+
return True
46+
4247
# Try running an interpreter with -E to see if it works or not.
4348
try:
4449
subprocess.check_call([sys.executable, '-E',

tests/IronPython.Tests/Cases/CPythonCasesManifest.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -924,7 +924,7 @@ Ignore=true
924924
[CPython.test_threadedtempfile]
925925
RetryCount=2 # https://github.com/IronLanguages/ironpython3/issues/1063
926926

927-
[CPython.test_threading]
927+
[CPython.test_threading] # IronPython.test_threading_stdlib
928928
Ignore=true
929929

930930
[CPython.test_threading_local]

tests/IronPython.Tests/Cases/IronPythonCasesManifest.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ Reason=Unstable - https://github.com/IronLanguages/ironpython3/issues/1037
131131
Ignore=true
132132
Reason=StackOverflowException - https://github.com/IronLanguages/ironpython2/issues/182
133133

134+
[IronPython.test_threading_stdlib]
135+
IsolationLevel=PROCESS
136+
134137
[IronPython.test_threadsafety]
135138
Ignore=true
136139

tests/suite/test_threading_stdlib.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Licensed to the .NET Foundation under one or more agreements.
2+
# The .NET Foundation licenses this file to you under the Apache 2.0 License.
3+
# See the LICENSE file in the project root for more information.
4+
5+
##
6+
## Run selected tests from test_threading from StdLib
7+
##
8+
9+
from iptest import is_ironpython, is_mono, generate_suite, run_test
10+
11+
import test.test_threading
12+
13+
def load_tests(loader, standard_tests, pattern):
14+
tests = loader.loadTestsFromModule(test.test_threading)
15+
16+
if is_ironpython:
17+
failing_tests = []
18+
19+
skip_tests = [
20+
test.test_threading.SubinterpThreadingTests('test_threads_join'), # ImportError: No module named '_testcapi'
21+
test.test_threading.SubinterpThreadingTests('test_threads_join_2'), # ImportError: No module named '_testcapi'
22+
test.test_threading.ThreadTests('test_PyThreadState_SetAsyncExc'), # AttributeError: function PyThreadState_SetAsyncExc is not defined
23+
test.test_threading.ThreadTests('test_enumerate_after_join'), # AttributeError: 'module' object has no attribute 'getswitchinterval'
24+
test.test_threading.ThreadTests('test_finalize_runnning_thread'), # AssertionError: 1 != 42
25+
test.test_threading.ThreadTests('test_finalize_with_trace'), # AssertionError
26+
test.test_threading.ThreadTests('test_no_refcycle_through_target'), # AttributeError: 'module' object has no attribute 'getrefcount'
27+
]
28+
29+
if is_mono:
30+
skip_tests += [
31+
test.test_threading.ThreadJoinOnShutdown('test_4_daemon_threads'), # SystemError: Thread was being aborted
32+
]
33+
34+
return generate_suite(tests, failing_tests, skip_tests)
35+
36+
else:
37+
return tests
38+
39+
run_test(__name__)

0 commit comments

Comments
 (0)