diff --git a/src/core/IronPython.Modules/_thread.cs b/src/core/IronPython.Modules/_thread.cs index 7515b9e0d..469da057c 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 $""; + } + return $""; } private void CreateBlockEvent() { AutoResetEvent are = new AutoResetEvent(false); - if (Interlocked.CompareExchange(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 $""; + } + return $""; + } + + public void _acquire_restore([NotNone] PythonTuple state) { + acquire(); + count = (int)state[0]!; + curHolder = (Thread?)state[1]; + } + + public PythonTuple _release_save() { + var count = Interlocked.Exchange(ref this.count, 0); + if (count == 0) { + throw PythonOps.RuntimeError("cannot release un-acquired lock"); + } + + // release + var owner = Interlocked.Exchange(ref curHolder, null); + blockEvent?.Set(); + + return PythonTuple.MakeTuple(count, owner); + } + + public bool _is_owned() + => 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) { 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/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/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 diff --git a/tests/suite/test_threading_stdlib.py b/tests/suite/test_threading_stdlib.py new file mode 100644 index 000000000..459c219d1 --- /dev/null +++ b/tests/suite/test_threading_stdlib.py @@ -0,0 +1,39 @@ +# 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, is_mono, 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' + ] + + 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: + return tests + +run_test(__name__)