Skip to content
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

Tweak SemaphoreDisposer to null out the held semaphore #57831

Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public static async ValueTask<SemaphoreDisposer> DisposableWaitAsync(this Semaph
[NonCopyable]
internal struct SemaphoreDisposer : IDisposable
{
private readonly SemaphoreSlim _semaphore;
private SemaphoreSlim? _semaphore;

public SemaphoreDisposer(SemaphoreSlim semaphore)
{
Expand All @@ -35,7 +35,23 @@ public SemaphoreDisposer(SemaphoreSlim semaphore)

public void Dispose()
{
_semaphore.Release();
// Officially, Dispose() being called more than once is allowable, but in this case
// if that were to ever happen that means something is very, very wrong. Since it's an internal
// type, better to be strict.

// Nulling this out also means it's a bit easier to diagnose some async deadlocks; if you have an
// async deadlock where a SemaphoreSlim is held but you're unsure why, as long all the users of the
// SemaphoreSlim used the Disposable helpers, you can search memory and find the instance that
// is pointing to the SemaphoreSlim that hasn't nulled out this field yet; in that case you know
// that's holding the lock and can figure out who is holding that SemaphoreDisposer.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're in a struct. so this does only work as long as the NonCopyable part above really is maintained. i can allow this :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CyrusNajmabadi Indeed, the [NonCopyable] being violated does mean we could end up with "surprises", but at this point, this is the least of your concerns IMHO. 😄

Copy link
Member

@jaredpar jaredpar Nov 18, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so this does only work as long as the NonCopyable part above really is maintained

Instances of this type are copied twice in this very file so [NonCopyable] is more like [NonCopyableForSomeVersionsOfCopy]

Copy link
Member

@sharwell sharwell Nov 18, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instances of this type are copied twice in this very file

There are no copies of this type in this file (moves are not copies). A copy is a case where the new location and the old location can still be accessed with the value; the analyzer ideally would ignore cases where the old location is accessible but never accessed, but it doesn't pay attention to that right now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two returns which are copies. You label them moves but that doesn't change that copies occur.

var semaphoreToDispose = Interlocked.Exchange(ref _semaphore, null);

if (semaphoreToDispose is null)
{
throw new ObjectDisposedException($"Somehow a {nameof(SemaphoreDisposer)} is being disposed twice.");
}

semaphoreToDispose.Release();
}
}
}
Expand Down