Skip to content

Commit cd87ad6

Browse files
authored
Thread-safe mmap close (#1875)
1 parent e46f933 commit cd87ad6

File tree

1 file changed

+88
-23
lines changed

1 file changed

+88
-23
lines changed

Src/IronPython.Modules/mmap.cs

Lines changed: 88 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -289,8 +289,22 @@ public class MmapDefault : IWeakReferenceable {
289289
private readonly MemoryMappedFileAccess _fileAccess;
290290
private readonly SafeFileHandle? _handle;
291291

292-
private volatile bool _isClosed;
293-
private int _refCount = 1;
292+
// RefCount | Closed | Meaning
293+
// ---------+--------+--------------------------------
294+
// 0 | 0 | Not fully initialized
295+
// 1 | 0 | Fully initialized, not being used by any threads
296+
// >1 | 0 | Object in use by one or more threads
297+
// >0 | 1 | Close/dispose requested, no more addrefs allowed, some threads may still be using it
298+
// 0 | 1 | Fully disposed
299+
private volatile int _state; // Combined ref count and closed/disposed flags (so we can atomically modify them).
300+
301+
private static class StateBits {
302+
public const int Closed = 0b_01; // close/dispose requested; no more addrefs allowed
303+
public const int Exclusive = 0b_10; // TODO: to manage exclusive access for resize
304+
public const int RefCount = unchecked(~0b_11); // 2 bits reserved for state management; ref count gets 29 bits (sign bit unused)
305+
public const int RefCountOne = 1 << 2; // ref count 1 shifted over 2 state bits
306+
}
307+
294308

295309
public MmapDefault(CodeContext/*!*/ context, int fileno, long length, string? tagname, MemoryMappedFileAccess fileAccess, long offset) {
296310
_fileAccess = fileAccess;
@@ -401,6 +415,7 @@ public MmapDefault(CodeContext/*!*/ context, int fileno, long length, string? ta
401415
throw;
402416
}
403417
_position = 0L;
418+
_state = StateBits.RefCountOne; // Fully initialized: ref count 1 and not closed or disposed.
404419

405420
void CheckFileAccessAndSize(Stream stream, bool isWindows) {
406421
bool isValid = _fileAccess switch {
@@ -544,29 +559,81 @@ public void __exit__(CodeContext/*!*/ context, params object[] excinfo) {
544559
close();
545560
}
546561

547-
public bool closed => _isClosed;
548562

549-
public void close() {
550-
if (!_isClosed) {
551-
lock (this) {
552-
if (!_isClosed) {
553-
_isClosed = true;
554-
CloseWorker();
555-
}
563+
public bool closed => (_state & StateBits.Closed) == StateBits.Closed; // Dispose already requested, will self-dispose when ref count drops to 0.
564+
565+
566+
private bool AddRef() {
567+
int oldState, newState;
568+
do {
569+
oldState = _state;
570+
if ((oldState & StateBits.Closed) == StateBits.Closed) {
571+
// mmap closed, dispose already requested, no more addrefs allowed
572+
return false;
556573
}
557-
}
574+
Debug.Assert((oldState & StateBits.RefCount) > 0, "resurrecting disposed mmap object (disposed without being closed)");
575+
576+
newState = oldState + StateBits.RefCountOne;
577+
} while (Interlocked.CompareExchange(ref _state, newState, oldState) != oldState);
578+
return true;
558579
}
559580

560-
private void CloseWorker() {
561-
if (Interlocked.Decrement(ref _refCount) == 0) {
581+
582+
private void Release() {
583+
bool performDispose;
584+
int oldState, newState;
585+
do {
586+
oldState = _state;
587+
Debug.Assert((oldState & StateBits.RefCount) > 0, "mmap ref count underflow (too many releases)");
588+
589+
performDispose = (oldState & StateBits.RefCount) == StateBits.RefCountOne;
590+
newState = oldState - StateBits.RefCountOne;
591+
if (performDispose) {
592+
newState |= StateBits.Closed; // most likely already closed
593+
}
594+
} while (Interlocked.CompareExchange(ref _state, newState, oldState) != oldState);
595+
596+
if (performDispose) {
562597
_view.Flush();
563598
_view.Dispose();
564599
_file.Dispose();
565600
CloseFileHandle();
566601
_sourceStream = null;
602+
_view = null!;
603+
_file = null!;
567604
}
568605
}
569606

607+
608+
public void close() {
609+
// close is idempotent; it must never block
610+
#if NET5_0_OR_GREATER
611+
if ((Interlocked.Or(ref _state, StateBits.Closed) & StateBits.Closed) != StateBits.Closed) {
612+
// freshly closed, release the construction time reference
613+
Release();
614+
}
615+
#else
616+
int current = _state;
617+
while (true)
618+
{
619+
int newState = current | StateBits.Closed;
620+
int oldState = Interlocked.CompareExchange(ref _state, newState, current);
621+
if (oldState == current)
622+
{
623+
// didn't change in the meantime, exchange with newState completed
624+
if ((oldState & StateBits.Closed) != StateBits.Closed) {
625+
// freshly closed, release the construction time reference
626+
Release();
627+
}
628+
return;
629+
}
630+
// try again to set the bit
631+
current = oldState;
632+
}
633+
#endif
634+
}
635+
636+
570637
private void CloseFileHandle() {
571638
if (_handle is not null) {
572639
// mmap owns _sourceStream too (if any) in this case
@@ -1138,34 +1205,31 @@ private static long GetFileSizeUnix(SafeFileHandle handle) {
11381205

11391206
#endregion
11401207

1141-
#region Synchronization
11421208

1143-
private void EnsureOpen() {
1144-
if (_isClosed) {
1145-
throw PythonOps.ValueError("mmap closed or invalid");
1146-
}
1147-
}
1209+
#region Synchronization
11481210

11491211
private readonly struct MmapLocker : IDisposable {
11501212
private readonly MmapDefault _mmap;
11511213

11521214
public MmapLocker(MmapDefault mmap) {
1215+
if (!mmap.AddRef()) {
1216+
throw PythonOps.ValueError("mmap closed or invalid");
1217+
}
11531218
_mmap = mmap;
1154-
_mmap.EnsureOpen();
1155-
Interlocked.Increment(ref _mmap._refCount);
11561219
}
11571220

11581221
#region IDisposable Members
11591222

1160-
public void Dispose() {
1161-
_mmap.CloseWorker();
1223+
public readonly void Dispose() {
1224+
_mmap.Release();
11621225
}
11631226

11641227
#endregion
11651228
}
11661229

11671230
#endregion
11681231

1232+
11691233
#region IWeakReferenceable Members
11701234

11711235
private WeakRefTracker? _tracker;
@@ -1185,6 +1249,7 @@ void IWeakReferenceable.SetFinalizer(WeakRefTracker value) {
11851249
#endregion
11861250
}
11871251

1252+
11881253
#region P/Invoke for allocation granularity
11891254

11901255
[StructLayout(LayoutKind.Sequential)]

0 commit comments

Comments
 (0)