Skip to content

Implement _thread.RLock #1922

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 139 additions & 12 deletions src/core/IronPython.Modules/_thread.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -138,23 +138,33 @@ 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;
}

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<Thread>(ref curHolder, Thread.CurrentThread, null) == null) {
if (Interlocked.CompareExchange(ref curHolder, Thread.CurrentThread, null) is null) {
return true;
}
if (!blocking) {
Expand All @@ -166,16 +176,16 @@ 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);
}
}

public void release(CodeContext/*!*/ context) {
if (Interlocked.Exchange<Thread>(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()
Expand All @@ -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 $"<unlocked _thread.lock object at 0x{IdDispenser.GetId(this):X16}>";
}
return $"<locked _thread.lock object at 0x{IdDispenser.GetId(this):X16}>";
}

private void CreateBlockEvent() {
AutoResetEvent are = new AutoResetEvent(false);
if (Interlocked.CompareExchange<AutoResetEvent>(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 $"<unlocked _thread.RLock object owner=0 count=0 at 0x{IdDispenser.GetId(this):X16}>";
}
return $"<locked _thread.RLock object owner={curHolder?.ManagedThreadId} count={count} at 0x{IdDispenser.GetId(this):X16}>";
}

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) {
Expand Down
5 changes: 5 additions & 0 deletions src/core/IronPython.StdLib/lib/test/script_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion tests/IronPython.Tests/Cases/CPythonCasesManifest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
3 changes: 3 additions & 0 deletions tests/IronPython.Tests/Cases/IronPythonCasesManifest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
39 changes: 39 additions & 0 deletions tests/suite/test_threading_stdlib.py
Original file line number Diff line number Diff line change
@@ -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__)