diff --git a/src/core/IronPython.Modules/mmap.cs b/src/core/IronPython.Modules/mmap.cs
index b59d7868f..8453bcb8e 100644
--- a/src/core/IronPython.Modules/mmap.cs
+++ b/src/core/IronPython.Modules/mmap.cs
@@ -289,20 +289,23 @@ public class MmapDefault : IWeakReferenceable {
private readonly MemoryMappedFileAccess _fileAccess;
private readonly SafeFileHandle? _handle;
- // RefCount | Closed | Meaning
- // ---------+--------+--------------------------------
- // 0 | 0 | Not fully initialized
- // 1 | 0 | Fully initialized, not being used by any threads
- // >1 | 0 | Object in use by one or more threads
- // >0 | 1 | Close/dispose requested, no more addrefs allowed, some threads may still be using it
- // 0 | 1 | Fully disposed
- private volatile int _state; // Combined ref count and closed/disposed flags (so we can atomically modify them).
+ // RefCount | Closed | Exclusive |Meaning
+ // ---------+--------+-----------+---------------------
+ // 0 | 0 | 0 | Not fully initialized
+ // 1 | 0 | 0 | Fully initialized, not being used by any threads
+ // >1 | 0 | 0 | Object in regular use by one or more threads
+ // 2 | 0 | 1 | Object in exclusive use (`resize` in progress)
+ // >0 | 1 | - | Close/dispose requested, no more addrefs allowed, some threads may still be using it
+ // 0 | 1 | 0 | Fully disposed
+ // Other combinations are invalid state
+ private volatile int _state; // Combined ref count and state flags (so we can atomically modify them).
private static class StateBits {
- public const int Closed = 0b_01; // close/dispose requested; no more addrefs allowed
- public const int Exclusive = 0b_10; // TODO: to manage exclusive access for resize
- public const int RefCount = unchecked(~0b_11); // 2 bits reserved for state management; ref count gets 29 bits (sign bit unused)
- public const int RefCountOne = 1 << 2; // ref count 1 shifted over 2 state bits
+ public const int Closed = 0b_001; // close/dispose requested; no more addrefs allowed
+ public const int Exclusive = 0b_010; // exclusive access for resize requested/in progress
+ public const int Exporting = 0b_100; // TODO: buffer exports extant; exclusive addrefs temporarily not allowed
+ public const int RefCount = unchecked(~0b_111); // 3 bits reserved for state management; ref count gets 29 bits (sign bit unused)
+ public const int RefCountOne = 1 << 3; // ref count 1 shifted over 3 state bits
}
@@ -563,33 +566,74 @@ public void __exit__(CodeContext/*!*/ context, params object[] excinfo) {
public bool closed => (_state & StateBits.Closed) == StateBits.Closed; // Dispose already requested, will self-dispose when ref count drops to 0.
- private bool AddRef() {
+ ///
+ /// Try to add a reference to the mmap object. Return true on success.
+ ///
+ ///
+ /// The reference count is incremented atomically and kept in the mmap state variable.
+ /// The reference count is not incremented if the state variable indicates that the mmap
+ /// in in the process of closing or currently in exclusive use.
+ ///
+ ///
+ /// If true, requests an exclusive reference.
+ ///
+ ///
+ /// If the reference could not be added, this parameter will contain the bit that was set preventing the addref.
+ ///
+ private bool TryAddRef(bool exclusive, out int reason) {
int oldState, newState;
do {
oldState = _state;
if ((oldState & StateBits.Closed) == StateBits.Closed) {
// mmap closed, dispose already requested, no more addrefs allowed
+ reason = StateBits.Closed;
+ return false;
+ }
+ if ((oldState & StateBits.Exclusive) == StateBits.Exclusive) {
+ // mmap in exclusive use, temporarily no more addrefs allowed
+ reason = StateBits.Exclusive;
+ return false;
+ }
+ if (exclusive && (oldState & StateBits.Exporting) == StateBits.Exporting) {
+ // mmap exporting, exclusive addrefs temporarily not allowed
+ reason = StateBits.Exporting;
return false;
}
Debug.Assert((oldState & StateBits.RefCount) > 0, "resurrecting disposed mmap object (disposed without being closed)");
newState = oldState + StateBits.RefCountOne;
+ if (exclusive) {
+ newState |= StateBits.Exclusive;
+ }
} while (Interlocked.CompareExchange(ref _state, newState, oldState) != oldState);
+ reason = 0;
return true;
}
- private void Release() {
+ ///
+ /// Atomically release a reference to the mmap object, and optionally reset the exclusive state flag.
+ ///
+ ///
+ /// If the reference count drops to 0, the mmap object is disposed.
+ ///
+ ///
+ /// If true, the exclusive reference is released.
+ ///
+ private void Release(bool exclusive) {
bool performDispose;
int oldState, newState;
do {
oldState = _state;
+ Debug.Assert(!exclusive || (oldState & StateBits.Exclusive) == StateBits.Exclusive, "releasing exclusive reference without being exclusive");
Debug.Assert((oldState & StateBits.RefCount) > 0, "mmap ref count underflow (too many releases)");
performDispose = (oldState & StateBits.RefCount) == StateBits.RefCountOne;
+ Debug.Assert(!performDispose || (oldState & StateBits.Closed) == StateBits.Closed, "disposing mmap object without being closed");
+
newState = oldState - StateBits.RefCountOne;
- if (performDispose) {
- newState |= StateBits.Closed; // most likely already closed
+ if (exclusive) {
+ newState &= ~StateBits.Exclusive;
}
} while (Interlocked.CompareExchange(ref _state, newState, oldState) != oldState);
@@ -610,25 +654,17 @@ public void close() {
#if NET5_0_OR_GREATER
if ((Interlocked.Or(ref _state, StateBits.Closed) & StateBits.Closed) != StateBits.Closed) {
// freshly closed, release the construction time reference
- Release();
+ Release(exclusive: false);
}
#else
- int current = _state;
- while (true)
- {
- int newState = current | StateBits.Closed;
- int oldState = Interlocked.CompareExchange(ref _state, newState, current);
- if (oldState == current)
- {
- // didn't change in the meantime, exchange with newState completed
- if ((oldState & StateBits.Closed) != StateBits.Closed) {
- // freshly closed, release the construction time reference
- Release();
- }
- return;
- }
- // try again to set the bit
- current = oldState;
+ int oldState, newState;
+ do {
+ oldState = _state;
+ newState = oldState | StateBits.Closed;
+ } while (Interlocked.CompareExchange(ref _state, newState, oldState) != oldState);
+ if ((oldState & StateBits.Closed) != StateBits.Closed) {
+ // freshly closed, release the construction time reference
+ Release(exclusive: false);
}
#endif
}
@@ -861,8 +897,9 @@ public string readline() {
}
}
+
public void resize(long newsize) {
- using (new MmapLocker(this)) {
+ using (new MmapLocker(this, exclusive: true)) {
if (_fileAccess is not MemoryMappedFileAccess.ReadWrite and not MemoryMappedFileAccess.ReadWriteExecute) {
throw PythonOps.TypeError("mmap can't resize a readonly or copy-on-write memory map.");
}
@@ -961,6 +998,7 @@ public void resize(long newsize) {
}
}
+
public object rfind([NotNone] IBufferProtocol s) {
using (new MmapLocker(this)) {
return RFindWorker(s, Position, _view.Capacity);
@@ -1210,18 +1248,32 @@ private static long GetFileSizeUnix(SafeFileHandle handle) {
private readonly struct MmapLocker : IDisposable {
private readonly MmapDefault _mmap;
-
- public MmapLocker(MmapDefault mmap) {
- if (!mmap.AddRef()) {
- throw PythonOps.ValueError("mmap closed or invalid");
+ private readonly bool _exclusive;
+
+ public MmapLocker(MmapDefault mmap, bool exclusive = false) {
+ if (!mmap.TryAddRef(exclusive, out int reason)) {
+ if (reason == StateBits.Closed) {
+ // mmap is permanently closed
+ throw PythonOps.ValueError("mmap closed or invalid");
+ } else if (reason == StateBits.Exporting) {
+ // map is temporarily exporting buffers obtained through the buffer protocol
+ throw PythonOps.BufferError("mmap can't perform the operation with extant buffers exported");
+ } else if (reason == StateBits.Exclusive) {
+ // mmap is temporarily in exclusive use
+ throw PythonNT.GetOsError(PythonErrno.EAGAIN);
+ } else {
+ // should not happen
+ throw new InvalidOperationException("mmap state error");
+ }
}
_mmap = mmap;
+ _exclusive = exclusive;
}
#region IDisposable Members
public readonly void Dispose() {
- _mmap.Release();
+ _mmap.Release(_exclusive);
}
#endregion