Skip to content
Merged
Prev Previous commit
Next Next commit
LowLevelLifoSemaphore: decrement timeoutMs if we lost InterlockedComp…
…areExchange

When a thread wakes after waiting for a semaphore to be released, if
it raced with another thread that is also trying to update the
semaphore counts and loses, it has to go back to waiting again.

In that case, decrement the remaining timeout by the elapsed wait time
so that the next wait is shorter.
  • Loading branch information
lambdageek committed Apr 20, 2023
commit ec34316c53f2380bacc0ea53ee0df805f54508b4
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,15 @@ private bool WaitForSignal(int timeoutMs)

while (true)
{
int startTicks = Environment.TickCount;
if (!WaitCore(timeoutMs))
{
// Unregister the waiter. The wait subsystem used above guarantees that a thread that wakes due to a timeout does
// not observe a signal to the object being waited upon.
_separated._counts.InterlockedDecrementWaiterCount();
return false;
}
int elapsedTicks = Environment.TickCount;

// Unregister the waiter if this thread will not be waiting anymore, and try to acquire the semaphore
Counts counts = _separated._counts;
Expand Down Expand Up @@ -173,6 +175,11 @@ private bool WaitForSignal(int timeoutMs)
}

counts = countsBeforeUpdate;
if (timeoutMs != -1) {
int waitMs = elapsedTicks - startTicks;
if (waitMs <= timeoutMs)
timeoutMs -= waitMs;
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ protected override void ReleaseCore(int count)
ReleaseInternal(lifo_semaphore, count);
}

private sealed record WaitEntry (LowLevelLifoAsyncWaitSemaphore Semaphore, int TimeoutMs, Action<LowLevelLifoAsyncWaitSemaphore, object?> OnSuccess, Action<LowLevelLifoAsyncWaitSemaphore, object?> OnTimeout, object? State);
private sealed record WaitEntry (LowLevelLifoAsyncWaitSemaphore Semaphore, Action<LowLevelLifoAsyncWaitSemaphore, object?> OnSuccess, Action<LowLevelLifoAsyncWaitSemaphore, object?> OnTimeout, object? State)
{
public int TimeoutMs {get; internal set;}
public int StartWaitTicks {get; internal set; }
}

public void PrepareAsyncWait(int timeoutMs, Action<LowLevelLifoAsyncWaitSemaphore, object?> onSuccess, Action<LowLevelLifoAsyncWaitSemaphore, object?> onTimeout, object? state)
{
Expand Down Expand Up @@ -108,7 +112,12 @@ private void PrepareAsyncWaitForSignal(int timeoutMs, Action<LowLevelLifoAsyncWa

_onWait();

PrepareAsyncWaitCore(timeoutMs, new WaitEntry(this, timeoutMs, onSuccess, onTimeout, state));
WaitEntry we = new WaitEntry(this, onSuccess, onTimeout, state)
{
TimeoutMs = timeoutMs,
StartWaitTicks = Environment.TickCount,
};
PrepareAsyncWaitCore(timeoutMs, we);
// on success calls InternalAsyncWaitSuccess, on timeout calls InternalAsyncWaitTimeout
}

Expand All @@ -124,6 +133,7 @@ private static void InternalAsyncWaitTimeout(LowLevelLifoAsyncWaitSemaphore self
private static void InternalAsyncWaitSuccess(LowLevelLifoAsyncWaitSemaphore self, WaitEntry internalWaitEntry)
{
WaitEntry we = internalWaitEntry!;
int elapsedTicks = Environment.TickCount;
// Unregister the waiter if this thread will not be waiting anymore, and try to acquire the semaphore
Counts counts = self._separated._counts;
while (true)
Expand Down Expand Up @@ -158,10 +168,12 @@ private static void InternalAsyncWaitSuccess(LowLevelLifoAsyncWaitSemaphore self
// if we get here, we need to keep waiting because the SignalCount above was 0 after we did
// the CompareExchange - someone took the signal before us.

// Note: we could choose to decrement TimeoutMs here by the amount of time we've already
// waited, but that means we'd have to collect the time before we started waiting, which is
// an extra call out to JS that isn't needed in the case where there's no contention. The
// TimeoutMs is a minimum that we would wait, so it's ok to waitlonger.
if (we.TimeoutMs != -1) {
int waitMs = elapsedTicks - we.StartWaitTicks;
if (waitMs <= we.TimeoutMs)
we.TimeoutMs -= waitMs;
we.StartWaitTicks = elapsedTicks;
}
self.PrepareAsyncWaitCore (we.TimeoutMs, we);
// on success calls InternalAsyncWaitSuccess, on timeout calls InternalAsyncWaitTimeout
}
Expand Down