Skip to content

Commit

Permalink
Reference counted disposables (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
StephenCleary authored Dec 30, 2021
1 parent 9d42d1f commit 9bcc26f
Show file tree
Hide file tree
Showing 20 changed files with 1,149 additions and 6 deletions.
9 changes: 6 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased]

- [Feature] Added `ReferenceCountedDisposable`, `ReferenceCountedAsyncDisposable`, and associated types. #12
- [Fix] Fixed race condition bug (never observed). #14

## [2.2.1] - 2021-09-25
- [Fix] Bumped `System.Collections.Immutable` dependency version from `1.4.0` to `1.7.1`. This fixes the shim dlls issue on .NET Framework targets.

## [2.2.0] - 2020-10-02
- [Feature] Added support for `null` disposables and delegates (which are ignored).
- [Feature] Added support for `null` disposables and delegates (which are ignored). #13
- [Feature] Added `Disposable` and `AsyncDisposable`.

## [2.1.0] - 2020-06-08
Expand All @@ -21,10 +24,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [2.0.1] - 2019-07-20
- [Fix] Published NuGet symbol packages.
- [Fix] Added explicit `net461` target.
- [Fix] Added explicit `net461` target. #4

## [2.0.0] - 2018-06-02
- [Breaking] Fixed typo: `SingleDisposable<T>.IsDispoed` is now `SingleDisposable<T>.IsDisposed`.
- [Breaking] Fixed typo: `SingleDisposable<T>.IsDispoed` is now `SingleDisposable<T>.IsDisposed`. #3
- [Feature] Added source linking.

## [1.2.3] - 2017-09-09
Expand Down
43 changes: 43 additions & 0 deletions src/Nito.Disposables/IReferenceCountedAsyncDisposable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#if NETSTANDARD2_1
using System;

namespace Nito.Disposables
{
/// <summary>
/// An instance that represents a reference count. All members are threadsafe.
/// </summary>
public interface IReferenceCountedAsyncDisposable<out T> : IAsyncDisposable
where T : class, IAsyncDisposable
{
/// <summary>
/// Adds a weak reference to this reference counted disposable. Throws <see cref="ObjectDisposedException"/> if this instance is disposed.
/// </summary>
IWeakReferenceCountedAsyncDisposable<T> AddWeakReference();

/// <summary>
/// Returns a new reference to this reference counted disposable, incrementing the reference counter. Throws <see cref="ObjectDisposedException"/> if this instance is disposed.
/// </summary>
IReferenceCountedAsyncDisposable<T> AddReference();

/// <summary>
/// Gets the target object. Throws <see cref="ObjectDisposedException"/> if this instance is disposed.
/// </summary>
T? Target { get; }

/// <summary>
/// Whether this instance is currently disposing or has been disposed.
/// </summary>
public bool IsDisposeStarted { get; }

/// <summary>
/// Whether this instance is disposed (finished disposing).
/// </summary>
public bool IsDisposed { get; }

/// <summary>
/// Whether this instance is currently disposing, but not finished yet.
/// </summary>
public bool IsDisposing { get; }
}
}
#endif
41 changes: 41 additions & 0 deletions src/Nito.Disposables/IReferenceCountedDisposable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System;

namespace Nito.Disposables
{
/// <summary>
/// An instance that represents a reference count. All members are threadsafe.
/// </summary>
public interface IReferenceCountedDisposable<out T> : IDisposable
where T : class, IDisposable
{
/// <summary>
/// Adds a weak reference to this reference counted disposable. Throws <see cref="ObjectDisposedException"/> if this instance is disposed.
/// </summary>
IWeakReferenceCountedDisposable<T> AddWeakReference();

/// <summary>
/// Returns a new reference to this reference counted disposable, incrementing the reference counter. Throws <see cref="ObjectDisposedException"/> if this instance is disposed.
/// </summary>
IReferenceCountedDisposable<T> AddReference();

/// <summary>
/// Gets the target object. Throws <see cref="ObjectDisposedException"/> if this instance is disposed.
/// </summary>
T? Target { get; }

/// <summary>
/// Whether this instance is currently disposing or has been disposed.
/// </summary>
public bool IsDisposeStarted { get; }

/// <summary>
/// Whether this instance is disposed (finished disposing).
/// </summary>
public bool IsDisposed { get; }

/// <summary>
/// Whether this instance is currently disposing, but not finished yet.
/// </summary>
public bool IsDisposing { get; }
}
}
23 changes: 23 additions & 0 deletions src/Nito.Disposables/IWeakReferenceCountedAsyncDisposable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#if NETSTANDARD2_1
using System;

namespace Nito.Disposables
{
/// <summary>
/// An instance that represents an uncounted weak reference. All members are threadsafe.
/// </summary>
public interface IWeakReferenceCountedAsyncDisposable<out T>
where T : class, IAsyncDisposable
{
/// <summary>
/// Adds a reference to this reference counted disposable. Returns <c>null</c> if the underlying disposable has already been disposed or garbage collected.
/// </summary>
IReferenceCountedAsyncDisposable<T>? TryAddReference();

/// <summary>
/// Attempts to get the target object. Returns <c>null</c> if the underlying disposable has already been disposed or garbage collected.
/// </summary>
T? TryGetTarget();
}
}
#endif
21 changes: 21 additions & 0 deletions src/Nito.Disposables/IWeakReferenceCountedDisposable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;

namespace Nito.Disposables
{
/// <summary>
/// An instance that represents an uncounted weak reference. All members are threadsafe.
/// </summary>
public interface IWeakReferenceCountedDisposable<out T>
where T : class, IDisposable
{
/// <summary>
/// Adds a reference to this reference counted disposable. Returns <c>null</c> if the underlying disposable has already been disposed or garbage collected.
/// </summary>
IReferenceCountedDisposable<T>? TryAddReference();

/// <summary>
/// Attempts to get the target object. Returns <c>null</c> if the underlying disposable has already been disposed or garbage collected.
/// </summary>
T? TryGetTarget();
}
}
2 changes: 1 addition & 1 deletion src/Nito.Disposables/Internals/BoundAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public bool TryUpdateContext(Func<T, T> contextUpdater)
_ = contextUpdater ?? throw new ArgumentNullException(nameof(contextUpdater));
while (true)
{
var original = Interlocked.CompareExchange(ref _field, _field, _field);
var original = Interlocked.CompareExchange(ref _field, null, null);
if (original == null)
return false;
var updatedContext = new BoundAction(original, contextUpdater);
Expand Down
2 changes: 1 addition & 1 deletion src/Nito.Disposables/Internals/BoundAsyncAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public bool TryUpdateContext(Func<T, T> contextUpdater)
_ = contextUpdater ?? throw new ArgumentNullException(nameof(contextUpdater));
while (true)
{
var original = Interlocked.CompareExchange(ref _field, _field, _field);
var original = Interlocked.CompareExchange(ref _field, null, null);
if (original == null)
return false;
var updatedContext = new BoundAction(original, contextUpdater);
Expand Down
25 changes: 25 additions & 0 deletions src/Nito.Disposables/Internals/IReferenceCounter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;

namespace Nito.Disposables.Internals
{
/// <summary>
/// A reference count for an underlying target.
/// </summary>
public interface IReferenceCounter
{
/// <summary>
/// Increments the reference count and returns <c>true</c>. If the reference count has already reached zero, returns <c>false</c>.
/// </summary>
bool TryIncrementCount();

/// <summary>
/// Decrements the reference count and returns <c>null</c>. If this call causes the reference count to reach zero, returns the underlying target.
/// </summary>
object? TryDecrementCount();

/// <summary>
/// Returns the underlying target. Returns <c>null</c> if the reference count has already reached zero.
/// </summary>
object? TryGetTarget();
}
}
56 changes: 56 additions & 0 deletions src/Nito.Disposables/Internals/ReferenceCountedAsyncDisposable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#if NETSTANDARD2_1
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;

namespace Nito.Disposables.Internals
{
/// <summary>
/// An instance that represents a reference count.
/// </summary>
public sealed class ReferenceCountedAsyncDisposable<T> : SingleAsyncDisposable<IReferenceCounter>, IReferenceCountedAsyncDisposable<T>
where T : class, IAsyncDisposable
{
/// <summary>
/// Initializes a reference counted disposable that refers to the specified reference count. The specified reference count must have already been incremented for this instance.
/// </summary>
public ReferenceCountedAsyncDisposable(IReferenceCounter referenceCounter)
: base(referenceCounter)
{
_ = referenceCounter ?? throw new ArgumentNullException(nameof(referenceCounter));

// Ensure we can cast from the stored IDisposable to T.
_ = ((IReferenceCountedAsyncDisposable<T>) this).Target;
}

/// <inheritdoc/>
protected override ValueTask DisposeAsync(IReferenceCounter referenceCounter) => (referenceCounter.TryDecrementCount() as IAsyncDisposable)?.DisposeAsync() ?? new ValueTask();

T? IReferenceCountedAsyncDisposable<T>.Target => (T?) ReferenceCounter.TryGetTarget();

IReferenceCountedAsyncDisposable<T> IReferenceCountedAsyncDisposable<T>.AddReference()
{
var referenceCounter = ReferenceCounter;
if (!referenceCounter.TryIncrementCount())
throw new ObjectDisposedException(nameof(ReferenceCountedAsyncDisposable<T>)); // cannot actually happen
return new ReferenceCountedAsyncDisposable<T>(referenceCounter);
}

IWeakReferenceCountedAsyncDisposable<T> IReferenceCountedAsyncDisposable<T>.AddWeakReference() => new WeakReferenceCountedAsyncDisposable<T>(ReferenceCounter);

private IReferenceCounter ReferenceCounter
{
get
{
IReferenceCounter referenceCounter = null!;
// Implementation note: this always "succeeds" in updating the context since it always returns the same instance.
// So, we know that this will be called at most once. It may also be called zero times if this instance is disposed.
if (!TryUpdateContext(x => referenceCounter = x))
throw new ObjectDisposedException(nameof(ReferenceCountedAsyncDisposable<T>));
return referenceCounter;
}
}
}
}
#endif
53 changes: 53 additions & 0 deletions src/Nito.Disposables/Internals/ReferenceCountedDisposable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace Nito.Disposables.Internals
{
/// <summary>
/// An instance that represents a reference count.
/// </summary>
public sealed class ReferenceCountedDisposable<T> : SingleDisposable<IReferenceCounter>, IReferenceCountedDisposable<T>
where T : class, IDisposable
{
/// <summary>
/// Initializes a reference counted disposable that refers to the specified reference count. The specified reference count must have already been incremented for this instance.
/// </summary>
public ReferenceCountedDisposable(IReferenceCounter referenceCounter)
: base(referenceCounter)
{
_ = referenceCounter ?? throw new ArgumentNullException(nameof(referenceCounter));

// Ensure we can cast from the stored IDisposable to T.
_ = ((IReferenceCountedDisposable<T>) this).Target;
}

/// <inheritdoc/>
protected override void Dispose(IReferenceCounter referenceCounter) => (referenceCounter.TryDecrementCount() as IDisposable)?.Dispose();

T? IReferenceCountedDisposable<T>.Target => (T?) ReferenceCounter.TryGetTarget();

IReferenceCountedDisposable<T> IReferenceCountedDisposable<T>.AddReference()
{
var referenceCounter = ReferenceCounter;
if (!referenceCounter.TryIncrementCount())
throw new ObjectDisposedException(nameof(ReferenceCountedDisposable<T>)); // cannot actually happen
return new ReferenceCountedDisposable<T>(referenceCounter);
}

IWeakReferenceCountedDisposable<T> IReferenceCountedDisposable<T>.AddWeakReference() => new WeakReferenceCountedDisposable<T>(ReferenceCounter);

private IReferenceCounter ReferenceCounter
{
get
{
IReferenceCounter referenceCounter = null!;
// Implementation note: this always "succeeds" in updating the context since it always returns the same instance.
// So, we know that this will be called at most once. It may also be called zero times if this instance is disposed.
if (!TryUpdateContext(x => referenceCounter = x))
throw new ObjectDisposedException(nameof(ReferenceCountedDisposable<T>));
return referenceCounter;
}
}
}
}
56 changes: 56 additions & 0 deletions src/Nito.Disposables/Internals/ReferenceCounter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System;
using System.Threading;

namespace Nito.Disposables.Internals
{
/// <summary>
/// A reference count for an underlying target.
/// </summary>
public sealed class ReferenceCounter : IReferenceCounter
{
private object? _target;
private int _count;

/// <summary>
/// Creates a new reference counter with a reference count of 1 referencing the specified target.
/// </summary>
public ReferenceCounter(object? target)
{
_target = target;
_count = 1;
}

bool IReferenceCounter.TryIncrementCount() => TryUpdate(x => x + 1) != null;

object? IReferenceCounter.TryDecrementCount()
{
var updateResult = TryUpdate(x => x - 1);
if (updateResult != 0)
return null;
return Interlocked.Exchange(ref _target, null);
}

object? IReferenceCounter.TryGetTarget()
{
var result = Interlocked.CompareExchange(ref _target, null, null);
var count = Interlocked.CompareExchange(ref _count, 0, 0);
if (count == 0)
return null;
return result;
}

private int? TryUpdate(Func<int, int> func)
{
while (true)
{
var original = Interlocked.CompareExchange(ref _count, 0, 0);
if (original == 0)
return null;
var updatedCount = func(original);
var result = Interlocked.CompareExchange(ref _count, updatedCount, original);
if (original == result)
return updatedCount;
}
}
}
}
Loading

0 comments on commit 9bcc26f

Please sign in to comment.