Skip to content

Commit

Permalink
Use newer ref-counting idiom
Browse files Browse the repository at this point in the history
  • Loading branch information
jnm2 committed Aug 12, 2023
1 parent 4c0da0c commit 2ceb0c8
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 54 deletions.
6 changes: 3 additions & 3 deletions src/YouTubeDownloadTool/Utils/RefCountedFileLock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ namespace YouTubeDownloadTool;
internal sealed class RefCountedFileLock
{
private readonly FileStream stream;
private readonly RefCounter referenceCounter;
private readonly RefCountingDisposer referenceCounter;

private RefCountedFileLock(FileStream stream)
{
this.stream = stream;
referenceCounter = new RefCounter(stream.Dispose);
referenceCounter = new RefCountingDisposer(stream);
}

public static RefCountedFileLock? CreateIfExists(string filePath)
Expand All @@ -27,7 +27,7 @@ private RefCountedFileLock(FileStream stream)

public IDisposable Lease() => referenceCounter.Lease();

public string FilePath => referenceCounter.IsDisposed
public string FilePath => referenceCounter.IsClosed
? throw new ObjectDisposedException(nameof(RefCountedFileLock))
: stream.Name;
}
51 changes: 0 additions & 51 deletions src/YouTubeDownloadTool/Utils/RefCounter.cs

This file was deleted.

126 changes: 126 additions & 0 deletions src/YouTubeDownloadTool/Utils/RefCountingDisposer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
namespace YouTubeDownloadTool;

/// <summary>
/// Tracks how many references there are to a disposable object, and disposes it when there are none remaining.
/// </summary>
public sealed class RefCountingDisposer
{
private readonly IDisposable disposable;
private uint refCount = 1;
private readonly object lockObject = new();

/// <summary>
/// Begins tracking an initial reference to <paramref name="disposable"/>. The reference count starts as <c>1</c>.
/// If the next call is <see cref="Release"/>, the reference count will go to <c>0</c> and <paramref
/// name="disposable"/> will be disposed. An additional <see cref="Release"/> call will be needed for each <see
/// cref="AddRef"/> call, if any.
/// </summary>
/// <returns>
/// A <see cref="RefCountingDisposer"/> which can track further references and which will dispose <paramref
/// name="disposable"/> when the last reference has been released.
/// </returns>
public RefCountingDisposer(IDisposable disposable)
{
this.disposable = disposable ?? throw new ArgumentNullException(nameof(disposable));
}

/// <summary>
/// <para>
/// Reflects that an additional reference to the tracked object has been made. This will require an additional call
/// to <see cref="Release"/> before the tracked object will be disposed by this <see cref="RefCountingDisposer"/>
/// instance.
/// </para>
/// <para>
/// <see cref="InvalidOperationException"/> is thrown if all references have already been released and there is no
/// longer anything to track.
/// </para>
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown if all references have already been released and there is no longer anything to track.
/// </exception>
public void AddRef()
{
lock (lockObject)
{
if (refCount == 0)
throw new InvalidOperationException($"{nameof(AddRef)} must not be called after all references have been released.");

refCount++;
}
}

/// <summary>
/// Reflects that a reference to the tracked object has been released. If the last remaining reference is released,
/// the tracked object will be disposed and future calls to <see cref="AddRef()"/> and <see cref="Release"/> will
/// throw <see cref="InvalidOperationException"/>.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown if all references have already been released and there is no longer anything to track.
/// </exception>
public void Release()
{
bool dispose;

lock (lockObject)
{
if (refCount == 0)
throw new InvalidOperationException($"{nameof(Release)} must not be called after all references have been released.");

refCount--;

dispose = refCount == 0;
}

if (dispose) disposable.Dispose();
}

/// <summary>
/// <para>
/// Indicates whether all references have been released. When <see langword="true"/>, the tracked object is either
/// disposed already or in the process of being disposed on a different thread.
/// </para>
/// <para>
/// ⚠️ Subsequent calls to <see cref="AddRef"/> and <see cref="Release"/> may still throw even if this property
/// returns <see langword="false"/>. Another thread may have executed the final <see cref="Release"/> call in the
/// meantime.
/// </para>
/// </summary>
public bool IsClosed => Volatile.Read(ref refCount) == 0;

/// <summary>
/// <para>
/// Reflects that an additional reference to the tracked object has been made and returns a lease object which
/// releases that reference when disposed. <see cref="IDisposable.Dispose"/> is idempotent and thread-safe on the
/// returned object.
/// </para>
/// <para>
/// <see cref="InvalidOperationException"/> is thrown if all references have already been released and there is no
/// longer anything to track. If unbalanced calls to <see cref="Release"/> are made separately, <see
/// cref="IDisposable.Dispose"/> may also throw <see cref="InvalidOperationException"/> due to all references
/// already having been released.
/// </para>
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown if all references have already been released and there is no longer anything to track.
/// </exception>
public IDisposable Lease()
{
AddRef();
return new RefLease(this);
}

private sealed class RefLease : IDisposable
{
private RefCountingDisposer? disposer;

public RefLease(RefCountingDisposer disposer)
{
this.disposer = disposer;
}

public void Dispose()
{
Interlocked.Exchange(ref disposer, null)?.Release();
}
}
}

0 comments on commit 2ceb0c8

Please sign in to comment.