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

4.x: Make RefCountDisposable lock-free #574

Merged
merged 4 commits into from
Jun 6, 2018
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
112 changes: 75 additions & 37 deletions Rx.NET/Source/src/System.Reactive/Disposables/RefCountDisposable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ namespace System.Reactive.Disposables
public sealed class RefCountDisposable : ICancelable
{
private readonly bool _throwWhenDisposed;
private readonly object _gate = new object();
private IDisposable _disposable;
private bool _isPrimaryDisposed;
/// <summary>
/// Holds the number of active child disposables and the
/// indicator bit (31) if the main _disposable has been marked
/// for disposition.
/// </summary>
private int _count;

/// <summary>
Expand All @@ -38,15 +41,14 @@ public RefCountDisposable(IDisposable disposable, bool throwWhenDisposed)
throw new ArgumentNullException(nameof(disposable));

_disposable = disposable;
_isPrimaryDisposed = false;
_count = 0;
_throwWhenDisposed = throwWhenDisposed;
}

/// <summary>
/// Gets a value that indicates whether the object is disposed.
/// </summary>
public bool IsDisposed => _disposable == null;
public bool IsDisposed => Volatile.Read(ref _count) == int.MinValue;

/// <summary>
/// Returns a dependent disposable that when disposed decreases the refcount on the underlying disposable.
Expand All @@ -56,20 +58,34 @@ public RefCountDisposable(IDisposable disposable, bool throwWhenDisposed)
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Backward compat + non-trivial work for a property getter.")]
public IDisposable GetDisposable()
{
lock (_gate)
// the current state
var cnt = Volatile.Read(ref _count);

for (; ; )
{
if (_disposable == null)
// If bit 31 is set and the active count is zero, don't create an inner
if (cnt == int.MinValue)
{
if (_throwWhenDisposed)
throw new ObjectDisposedException("RefCountDisposable");

return Disposable.Empty;
}
else

// Should not overflow the bits 0..30
if ((cnt & 0x7FFFFFFF) == int.MaxValue)
{
throw new OverflowException($"RefCountDisposable can't handle more than {int.MaxValue} disposables");
}

// Increment the active count by one, works because the increment
// won't affect bit 31
var u = Interlocked.CompareExchange(ref _count, cnt + 1, cnt);
if (u == cnt)
{
_count++;
return new InnerDisposable(this);
}
cnt = u;
}
}

Expand All @@ -78,50 +94,72 @@ public IDisposable GetDisposable()
/// </summary>
public void Dispose()
{
var disposable = default(IDisposable);
lock (_gate)
var cnt = Volatile.Read(ref _count);

for (; ; )
{
if (_disposable != null)

// already marked as disposed via bit 31?
if ((cnt & 0x80000000) != 0)
{
if (!_isPrimaryDisposed)
{
_isPrimaryDisposed = true;
// yes, nothing to do
break;
}

if (_count == 0)
{
disposable = _disposable;
_disposable = null;
}
// how many active disposables are there?
int active = cnt & 0x7FFFFFFF;

// keep the active count but set the dispose marker of bit 31
var u = int.MinValue | active;

var b = Interlocked.CompareExchange(ref _count, u, cnt);

if (b == cnt) {
// if there were 0 active disposables, there can't be any more after
// the CAS so we can dispose the underlying disposable
if (active == 0)
{
_disposable?.Dispose();
_disposable = null;
}
break;
}
cnt = b;
}

disposable?.Dispose();
}

private void Release()
{
var disposable = default(IDisposable);
lock (_gate)
{
if (_disposable != null)
{
_count--;

System.Diagnostics.Debug.Assert(_count >= 0);
var cnt = Volatile.Read(ref _count);

if (_isPrimaryDisposed)
for (; ; )
{
// extract the main disposed state (bit 31)
var main = (int)(cnt & 0x80000000);
// get the active count
var active = cnt & 0x7FFFFFFF;

// keep the main disposed state but decrement the counter
// in theory, active should be always > 0 at this point,
// guaranteed by the InnerDisposable.Dispose's Exchange operation.
System.Diagnostics.Debug.Assert(active > 0);
var u = main | (active - 1);

var b = Interlocked.CompareExchange(ref _count, u, cnt);

if (b == cnt) {
// if after the CAS there was zero active disposables and
// the main has been also marked for disposing,
// it is safe to dispose the underlying disposable
if (u == int.MinValue)
{
if (_count == 0)
{
disposable = _disposable;
_disposable = null;
}
_disposable?.Dispose();
_disposable = null;
}
break;
}
cnt = b;
}

disposable?.Dispose();
}

private sealed class InnerDisposable : IDisposable
Expand Down