Skip to content

Commit 3a477d2

Browse files
authored
Threas-safe mmap resize (#1891)
1 parent 990e5cd commit 3a477d2

File tree

1 file changed

+91
-39
lines changed

1 file changed

+91
-39
lines changed

src/core/IronPython.Modules/mmap.cs

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

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).
292+
// RefCount | Closed | Exclusive |Meaning
293+
// ---------+--------+-----------+---------------------
294+
// 0 | 0 | 0 | Not fully initialized
295+
// 1 | 0 | 0 | Fully initialized, not being used by any threads
296+
// >1 | 0 | 0 | Object in regular use by one or more threads
297+
// 2 | 0 | 1 | Object in exclusive use (`resize` in progress)
298+
// >0 | 1 | - | Close/dispose requested, no more addrefs allowed, some threads may still be using it
299+
// 0 | 1 | 0 | Fully disposed
300+
// Other combinations are invalid state
301+
private volatile int _state; // Combined ref count and state flags (so we can atomically modify them).
300302

301303
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
304+
public const int Closed = 0b_001; // close/dispose requested; no more addrefs allowed
305+
public const int Exclusive = 0b_010; // exclusive access for resize requested/in progress
306+
public const int Exporting = 0b_100; // TODO: buffer exports extant; exclusive addrefs temporarily not allowed
307+
public const int RefCount = unchecked(~0b_111); // 3 bits reserved for state management; ref count gets 29 bits (sign bit unused)
308+
public const int RefCountOne = 1 << 3; // ref count 1 shifted over 3 state bits
306309
}
307310

308311

@@ -563,33 +566,74 @@ public void __exit__(CodeContext/*!*/ context, params object[] excinfo) {
563566
public bool closed => (_state & StateBits.Closed) == StateBits.Closed; // Dispose already requested, will self-dispose when ref count drops to 0.
564567

565568

566-
private bool AddRef() {
569+
/// <summary>
570+
/// Try to add a reference to the mmap object. Return <c>true</c> on success.
571+
/// </summary>
572+
/// <remarks>
573+
/// The reference count is incremented atomically and kept in the mmap state variable.
574+
/// The reference count is not incremented if the state variable indicates that the mmap
575+
/// in in the process of closing or currently in exclusive use.
576+
/// </remarks>
577+
/// <param name="exclusive">
578+
/// If true, requests an exclusive reference.
579+
/// </param>
580+
/// <param name="reason">
581+
/// If the reference could not be added, this parameter will contain the bit that was set preventing the addref.
582+
/// </param>
583+
private bool TryAddRef(bool exclusive, out int reason) {
567584
int oldState, newState;
568585
do {
569586
oldState = _state;
570587
if ((oldState & StateBits.Closed) == StateBits.Closed) {
571588
// mmap closed, dispose already requested, no more addrefs allowed
589+
reason = StateBits.Closed;
590+
return false;
591+
}
592+
if ((oldState & StateBits.Exclusive) == StateBits.Exclusive) {
593+
// mmap in exclusive use, temporarily no more addrefs allowed
594+
reason = StateBits.Exclusive;
595+
return false;
596+
}
597+
if (exclusive && (oldState & StateBits.Exporting) == StateBits.Exporting) {
598+
// mmap exporting, exclusive addrefs temporarily not allowed
599+
reason = StateBits.Exporting;
572600
return false;
573601
}
574602
Debug.Assert((oldState & StateBits.RefCount) > 0, "resurrecting disposed mmap object (disposed without being closed)");
575603

576604
newState = oldState + StateBits.RefCountOne;
605+
if (exclusive) {
606+
newState |= StateBits.Exclusive;
607+
}
577608
} while (Interlocked.CompareExchange(ref _state, newState, oldState) != oldState);
609+
reason = 0;
578610
return true;
579611
}
580612

581613

582-
private void Release() {
614+
/// <summary>
615+
/// Atomically release a reference to the mmap object, and optionally reset the exclusive state flag.
616+
/// </summary>
617+
/// <remarks>
618+
/// If the reference count drops to 0, the mmap object is disposed.
619+
/// </remarks>
620+
/// <param name="exclusive">
621+
/// If true, the exclusive reference is released.
622+
/// </param>
623+
private void Release(bool exclusive) {
583624
bool performDispose;
584625
int oldState, newState;
585626
do {
586627
oldState = _state;
628+
Debug.Assert(!exclusive || (oldState & StateBits.Exclusive) == StateBits.Exclusive, "releasing exclusive reference without being exclusive");
587629
Debug.Assert((oldState & StateBits.RefCount) > 0, "mmap ref count underflow (too many releases)");
588630

589631
performDispose = (oldState & StateBits.RefCount) == StateBits.RefCountOne;
632+
Debug.Assert(!performDispose || (oldState & StateBits.Closed) == StateBits.Closed, "disposing mmap object without being closed");
633+
590634
newState = oldState - StateBits.RefCountOne;
591-
if (performDispose) {
592-
newState |= StateBits.Closed; // most likely already closed
635+
if (exclusive) {
636+
newState &= ~StateBits.Exclusive;
593637
}
594638
} while (Interlocked.CompareExchange(ref _state, newState, oldState) != oldState);
595639

@@ -610,25 +654,17 @@ public void close() {
610654
#if NET5_0_OR_GREATER
611655
if ((Interlocked.Or(ref _state, StateBits.Closed) & StateBits.Closed) != StateBits.Closed) {
612656
// freshly closed, release the construction time reference
613-
Release();
657+
Release(exclusive: false);
614658
}
615659
#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;
660+
int oldState, newState;
661+
do {
662+
oldState = _state;
663+
newState = oldState | StateBits.Closed;
664+
} while (Interlocked.CompareExchange(ref _state, newState, oldState) != oldState);
665+
if ((oldState & StateBits.Closed) != StateBits.Closed) {
666+
// freshly closed, release the construction time reference
667+
Release(exclusive: false);
632668
}
633669
#endif
634670
}
@@ -861,8 +897,9 @@ public string readline() {
861897
}
862898
}
863899

900+
864901
public void resize(long newsize) {
865-
using (new MmapLocker(this)) {
902+
using (new MmapLocker(this, exclusive: true)) {
866903
if (_fileAccess is not MemoryMappedFileAccess.ReadWrite and not MemoryMappedFileAccess.ReadWriteExecute) {
867904
throw PythonOps.TypeError("mmap can't resize a readonly or copy-on-write memory map.");
868905
}
@@ -961,6 +998,7 @@ public void resize(long newsize) {
961998
}
962999
}
9631000

1001+
9641002
public object rfind([NotNone] IBufferProtocol s) {
9651003
using (new MmapLocker(this)) {
9661004
return RFindWorker(s, Position, _view.Capacity);
@@ -1210,18 +1248,32 @@ private static long GetFileSizeUnix(SafeFileHandle handle) {
12101248

12111249
private readonly struct MmapLocker : IDisposable {
12121250
private readonly MmapDefault _mmap;
1213-
1214-
public MmapLocker(MmapDefault mmap) {
1215-
if (!mmap.AddRef()) {
1216-
throw PythonOps.ValueError("mmap closed or invalid");
1251+
private readonly bool _exclusive;
1252+
1253+
public MmapLocker(MmapDefault mmap, bool exclusive = false) {
1254+
if (!mmap.TryAddRef(exclusive, out int reason)) {
1255+
if (reason == StateBits.Closed) {
1256+
// mmap is permanently closed
1257+
throw PythonOps.ValueError("mmap closed or invalid");
1258+
} else if (reason == StateBits.Exporting) {
1259+
// map is temporarily exporting buffers obtained through the buffer protocol
1260+
throw PythonOps.BufferError("mmap can't perform the operation with extant buffers exported");
1261+
} else if (reason == StateBits.Exclusive) {
1262+
// mmap is temporarily in exclusive use
1263+
throw PythonNT.GetOsError(PythonErrno.EAGAIN);
1264+
} else {
1265+
// should not happen
1266+
throw new InvalidOperationException("mmap state error");
1267+
}
12171268
}
12181269
_mmap = mmap;
1270+
_exclusive = exclusive;
12191271
}
12201272

12211273
#region IDisposable Members
12221274

12231275
public readonly void Dispose() {
1224-
_mmap.Release();
1276+
_mmap.Release(_exclusive);
12251277
}
12261278

12271279
#endregion

0 commit comments

Comments
 (0)