From 3a0b06b66679adcf174d2f178000671d9d4740a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lozier?= Date: Tue, 25 Feb 2025 17:05:49 -0500 Subject: [PATCH 1/5] Implement _thread.RLock --- src/core/IronPython.Modules/_thread.cs | 151 +++++++++++++++++++++++-- 1 file changed, 139 insertions(+), 12 deletions(-) diff --git a/src/core/IronPython.Modules/_thread.cs b/src/core/IronPython.Modules/_thread.cs index 7515b9e0d..963c8083b 100644 --- a/src/core/IronPython.Modules/_thread.cs +++ b/src/core/IronPython.Modules/_thread.cs @@ -35,7 +35,7 @@ public static void PerformModuleReload(PythonContext/*!*/ context, PythonDiction #region Public API Surface - public static double TIMEOUT_MAX = 0; // TODO: fill this with a proper value + public static double TIMEOUT_MAX = Math.Floor(TimeSpan.MaxValue.TotalSeconds); [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes")] public static readonly PythonType LockType = DynamicHelpers.GetPythonTypeFromType(typeof(@lock)); @@ -138,13 +138,15 @@ public static object _set_sentinel(CodeContext context) { #endregion +#nullable enable + [PythonType, PythonHidden] public sealed class @lock { - private AutoResetEvent blockEvent; - private Thread curHolder; + private AutoResetEvent? blockEvent; + private Thread? curHolder; public object __enter__() { - acquire(true, -1); + acquire(); return this; } @@ -152,9 +154,17 @@ public void __exit__(CodeContext/*!*/ context, [NotNone] params object[] args) { release(context); } - public bool acquire(bool blocking = true, float timeout = -1) { + public bool acquire(bool blocking = true, double timeout = -1) { + var timespan = Timeout.InfiniteTimeSpan; + + if (timeout != -1) { + if (!blocking) throw PythonOps.ValueError("can't specify a timeout for a non-blocking call"); + if (timeout < 0) throw PythonOps.ValueError("timeout value must be a non-negative number"); + timespan = TimeSpan.FromSeconds(timeout); + } + for (; ; ) { - if (Interlocked.CompareExchange(ref curHolder, Thread.CurrentThread, null) == null) { + if (Interlocked.CompareExchange(ref curHolder, Thread.CurrentThread, null) is null) { return true; } if (!blocking) { @@ -166,7 +176,7 @@ public bool acquire(bool blocking = true, float timeout = -1) { CreateBlockEvent(); continue; } - if (!blockEvent.WaitOne(timeout < 0 ? Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(timeout))) { + if (!blockEvent.WaitOne(timespan)) { return false; } GC.KeepAlive(this); @@ -174,8 +184,8 @@ public bool acquire(bool blocking = true, float timeout = -1) { } public void release(CodeContext/*!*/ context) { - if (Interlocked.Exchange(ref curHolder, null) == null) { - throw PythonExceptions.CreateThrowable((PythonType)context.LanguageContext.GetModuleState("threaderror"), "lock isn't held", null); + if (Interlocked.Exchange(ref curHolder, null) is null) { + throw PythonOps.RuntimeError("release unlocked lock"); } if (blockEvent != null) { // if this isn't set yet we race, it's handled in Acquire() @@ -184,18 +194,135 @@ public void release(CodeContext/*!*/ context) { } } - public bool locked() { - return curHolder != null; + public bool locked() + => curHolder is not null; + + public string __repr__() { + if (curHolder is null) { + return $"(ref blockEvent, are, null) != null) { + if (Interlocked.CompareExchange(ref blockEvent, are, null) is not null) { are.Close(); } } } + [PythonType] + public sealed class RLock { + private AutoResetEvent? blockEvent; + private Thread? curHolder; + private int count; + + public object __enter__() { + acquire(); + return this; + } + + public void __exit__(CodeContext/*!*/ context, [NotNone] params object[] args) { + release(); + } + + public bool acquire(bool blocking = true, double timeout = -1) { + var timespan = Timeout.InfiniteTimeSpan; + + if (timeout != -1) { + if (!blocking) throw PythonOps.ValueError("can't specify a timeout for a non-blocking call"); + if (timeout < 0) throw PythonOps.ValueError("timeout value must be a non-negative number"); + timespan = TimeSpan.FromSeconds(timeout); + } + + var currentThread = Thread.CurrentThread; + + for (; ; ) { + var previousThread = Interlocked.CompareExchange(ref curHolder, currentThread, null); + if (previousThread == currentThread) { + count++; + return true; + } + if (previousThread is null) { + count = 1; + return true; + } + if (!blocking) { + return false; + } + if (blockEvent is null) { + // try again in case someone released us, checked the block + // event and discovered it was null so they didn't set it. + CreateBlockEvent(); + continue; + } + if (!blockEvent.WaitOne(timespan)) { + return false; + } + GC.KeepAlive(this); + } + } + + public void release() { + var currentThread = Thread.CurrentThread; + + if (curHolder != currentThread) { + throw PythonOps.RuntimeError("cannot release un-acquired lock"); + } + if (--count > 0) { + return; + } + + if (Interlocked.Exchange(ref curHolder, null) is null) { + throw PythonOps.RuntimeError("release unlocked lock"); + } + if (blockEvent is not null) { + // if this isn't set yet we race, it's handled in acquire() + blockEvent.Set(); + GC.KeepAlive(this); + } + } + + public string __repr__() { + if (curHolder is null) { + return $" curHolder == Thread.CurrentThread; + + private void CreateBlockEvent() { + AutoResetEvent are = new AutoResetEvent(false); + if (Interlocked.CompareExchange(ref blockEvent, are, null) != null) { + are.Close(); + } + } + } + +#nullable restore + #region Internal Implementation details private static Thread CreateThread(CodeContext/*!*/ context, ThreadStart start) { From 4851fc3e223e186710956272dab10ade4eb3fbfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lozier?= Date: Tue, 25 Feb 2025 17:06:18 -0500 Subject: [PATCH 2/5] Enable some test_threading tests --- .../Cases/CPythonCasesManifest.ini | 2 +- tests/suite/test_threading_stdlib.py | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 tests/suite/test_threading_stdlib.py diff --git a/tests/IronPython.Tests/Cases/CPythonCasesManifest.ini b/tests/IronPython.Tests/Cases/CPythonCasesManifest.ini index 7641cef7d..98688b1b1 100644 --- a/tests/IronPython.Tests/Cases/CPythonCasesManifest.ini +++ b/tests/IronPython.Tests/Cases/CPythonCasesManifest.ini @@ -924,7 +924,7 @@ Ignore=true [CPython.test_threadedtempfile] RetryCount=2 # https://github.com/IronLanguages/ironpython3/issues/1063 -[CPython.test_threading] +[CPython.test_threading] # IronPython.test_threading_stdlib Ignore=true [CPython.test_threading_local] diff --git a/tests/suite/test_threading_stdlib.py b/tests/suite/test_threading_stdlib.py new file mode 100644 index 000000000..4b4052fd0 --- /dev/null +++ b/tests/suite/test_threading_stdlib.py @@ -0,0 +1,34 @@ +# Licensed to the .NET Foundation under one or more agreements. +# The .NET Foundation licenses this file to you under the Apache 2.0 License. +# See the LICENSE file in the project root for more information. + +## +## Run selected tests from test_threading from StdLib +## + +from iptest import is_ironpython, generate_suite, run_test + +import test.test_threading + +def load_tests(loader, standard_tests, pattern): + tests = loader.loadTestsFromModule(test.test_threading) + + if is_ironpython: + failing_tests = [] + + skip_tests = [ + test.test_threading.SubinterpThreadingTests('test_threads_join'), # ImportError: No module named '_testcapi' + test.test_threading.SubinterpThreadingTests('test_threads_join_2'), # ImportError: No module named '_testcapi' + test.test_threading.ThreadTests('test_PyThreadState_SetAsyncExc'), # AttributeError: function PyThreadState_SetAsyncExc is not defined + test.test_threading.ThreadTests('test_enumerate_after_join'), # AttributeError: 'module' object has no attribute 'getswitchinterval' + test.test_threading.ThreadTests('test_finalize_runnning_thread'), # AssertionError: 1 != 42 + test.test_threading.ThreadTests('test_finalize_with_trace'), # AssertionError + test.test_threading.ThreadTests('test_no_refcycle_through_target'), # AttributeError: 'module' object has no attribute 'getrefcount' + ] + + return generate_suite(tests, failing_tests, skip_tests) + + else: + return tests + +run_test(__name__) From caf5ff84af8c890dc83524b23115f7e19489ffd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lozier?= Date: Tue, 25 Feb 2025 19:36:24 -0500 Subject: [PATCH 3/5] Get tests passing --- src/core/IronPython.StdLib/lib/test/script_helper.py | 5 +++++ tests/IronPython.Tests/Cases/IronPythonCasesManifest.ini | 3 +++ 2 files changed, 8 insertions(+) diff --git a/src/core/IronPython.StdLib/lib/test/script_helper.py b/src/core/IronPython.StdLib/lib/test/script_helper.py index 807de5249..66bf3d9a9 100644 --- a/src/core/IronPython.StdLib/lib/test/script_helper.py +++ b/src/core/IronPython.StdLib/lib/test/script_helper.py @@ -39,6 +39,11 @@ def _interpreter_requires_environment(): """ global __cached_interp_requires_environment if __cached_interp_requires_environment is None: + # https://github.com/IronLanguages/ironpython3/issues/1440 + if sys.implementation.name == "ironpython": + __cached_interp_requires_environment = True + return True + # Try running an interpreter with -E to see if it works or not. try: subprocess.check_call([sys.executable, '-E', diff --git a/tests/IronPython.Tests/Cases/IronPythonCasesManifest.ini b/tests/IronPython.Tests/Cases/IronPythonCasesManifest.ini index bc4cce06b..07fe270f8 100644 --- a/tests/IronPython.Tests/Cases/IronPythonCasesManifest.ini +++ b/tests/IronPython.Tests/Cases/IronPythonCasesManifest.ini @@ -131,6 +131,9 @@ Reason=Unstable - https://github.com/IronLanguages/ironpython3/issues/1037 Ignore=true Reason=StackOverflowException - https://github.com/IronLanguages/ironpython2/issues/182 +[IronPython.test_threading_stdlib] +IsolationLevel=PROCESS + [IronPython.test_threadsafety] Ignore=true From c5a858e887240a99a9de777214968114e599582c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lozier?= Date: Tue, 25 Feb 2025 20:18:25 -0500 Subject: [PATCH 4/5] Disable test on Mono --- tests/suite/test_threading_stdlib.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/suite/test_threading_stdlib.py b/tests/suite/test_threading_stdlib.py index 4b4052fd0..459c219d1 100644 --- a/tests/suite/test_threading_stdlib.py +++ b/tests/suite/test_threading_stdlib.py @@ -6,7 +6,7 @@ ## Run selected tests from test_threading from StdLib ## -from iptest import is_ironpython, generate_suite, run_test +from iptest import is_ironpython, is_mono, generate_suite, run_test import test.test_threading @@ -26,6 +26,11 @@ def load_tests(loader, standard_tests, pattern): test.test_threading.ThreadTests('test_no_refcycle_through_target'), # AttributeError: 'module' object has no attribute 'getrefcount' ] + if is_mono: + skip_tests += [ + test.test_threading.ThreadJoinOnShutdown('test_4_daemon_threads'), # SystemError: Thread was being aborted + ] + return generate_suite(tests, failing_tests, skip_tests) else: From 2bc61c7da19e923d29d33f6d5c7f02fdf5e9e821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lozier?= Date: Wed, 26 Feb 2025 13:32:36 -0500 Subject: [PATCH 5/5] Fix repr --- src/core/IronPython.Modules/_thread.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/IronPython.Modules/_thread.cs b/src/core/IronPython.Modules/_thread.cs index 963c8083b..469da057c 100644 --- a/src/core/IronPython.Modules/_thread.cs +++ b/src/core/IronPython.Modules/_thread.cs @@ -199,9 +199,9 @@ public bool locked() public string __repr__() { if (curHolder is null) { - return $""; } - return $""; } private void CreateBlockEvent() { @@ -286,9 +286,9 @@ public void release() { public string __repr__() { if (curHolder is null) { - return $""; } - return $""; } public void _acquire_restore([NotNone] PythonTuple state) {