diff --git a/src/YouTubeDownloadTool/Utils/RefCountedFileLock.cs b/src/YouTubeDownloadTool/Utils/RefCountedFileLock.cs
index e82b9c2..5857325 100644
--- a/src/YouTubeDownloadTool/Utils/RefCountedFileLock.cs
+++ b/src/YouTubeDownloadTool/Utils/RefCountedFileLock.cs
@@ -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)
@@ -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;
}
diff --git a/src/YouTubeDownloadTool/Utils/RefCounter.cs b/src/YouTubeDownloadTool/Utils/RefCounter.cs
deleted file mode 100644
index 495b5bf..0000000
--- a/src/YouTubeDownloadTool/Utils/RefCounter.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-namespace YouTubeDownloadTool;
-
-internal sealed class RefCounter
-{
- private Action? action;
- private int referenceCount;
-
- public RefCounter(Action action)
- {
- this.action = action ?? throw new ArgumentNullException(nameof(action));
- }
-
- public bool IsDisposed => Volatile.Read(ref action) is null;
-
- public IDisposable Lease()
- {
- Increment();
- return new RefCounterLease(this);
- }
-
- private void Increment()
- {
- if (Interlocked.Increment(ref referenceCount) <= 0 || IsDisposed)
- {
- throw new ObjectDisposedException(nameof(RefCountedFileLock));
- }
- }
-
- private void Decrement()
- {
- if (Interlocked.Decrement(ref referenceCount) == 0)
- {
- Interlocked.Exchange(ref action, null)?.Invoke();
- }
- }
-
- private sealed class RefCounterLease : IDisposable
- {
- private RefCounter? refCounter;
-
- public RefCounterLease(RefCounter refCounter)
- {
- this.refCounter = refCounter;
- }
-
- public void Dispose()
- {
- Interlocked.Exchange(ref refCounter, null)?.Decrement();
- }
- }
-}
diff --git a/src/YouTubeDownloadTool/Utils/RefCountingDisposer.cs b/src/YouTubeDownloadTool/Utils/RefCountingDisposer.cs
new file mode 100644
index 0000000..1c39d00
--- /dev/null
+++ b/src/YouTubeDownloadTool/Utils/RefCountingDisposer.cs
@@ -0,0 +1,126 @@
+namespace YouTubeDownloadTool;
+
+///
+/// Tracks how many references there are to a disposable object, and disposes it when there are none remaining.
+///
+public sealed class RefCountingDisposer
+{
+ private readonly IDisposable disposable;
+ private uint refCount = 1;
+ private readonly object lockObject = new();
+
+ ///
+ /// Begins tracking an initial reference to . The reference count starts as 1.
+ /// If the next call is , the reference count will go to 0 and will be disposed. An additional call will be needed for each call, if any.
+ ///
+ ///
+ /// A which can track further references and which will dispose when the last reference has been released.
+ ///
+ public RefCountingDisposer(IDisposable disposable)
+ {
+ this.disposable = disposable ?? throw new ArgumentNullException(nameof(disposable));
+ }
+
+ ///
+ ///
+ /// Reflects that an additional reference to the tracked object has been made. This will require an additional call
+ /// to before the tracked object will be disposed by this
+ /// instance.
+ ///
+ ///
+ /// is thrown if all references have already been released and there is no
+ /// longer anything to track.
+ ///
+ ///
+ ///
+ /// Thrown if all references have already been released and there is no longer anything to track.
+ ///
+ public void AddRef()
+ {
+ lock (lockObject)
+ {
+ if (refCount == 0)
+ throw new InvalidOperationException($"{nameof(AddRef)} must not be called after all references have been released.");
+
+ refCount++;
+ }
+ }
+
+ ///
+ /// 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 and will
+ /// throw .
+ ///
+ ///
+ /// Thrown if all references have already been released and there is no longer anything to track.
+ ///
+ 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();
+ }
+
+ ///
+ ///
+ /// Indicates whether all references have been released. When , the tracked object is either
+ /// disposed already or in the process of being disposed on a different thread.
+ ///
+ ///
+ /// ⚠️ Subsequent calls to and may still throw even if this property
+ /// returns . Another thread may have executed the final call in the
+ /// meantime.
+ ///
+ ///
+ public bool IsClosed => Volatile.Read(ref refCount) == 0;
+
+ ///
+ ///
+ /// Reflects that an additional reference to the tracked object has been made and returns a lease object which
+ /// releases that reference when disposed. is idempotent and thread-safe on the
+ /// returned object.
+ ///
+ ///
+ /// is thrown if all references have already been released and there is no
+ /// longer anything to track. If unbalanced calls to are made separately, may also throw due to all references
+ /// already having been released.
+ ///
+ ///
+ ///
+ /// Thrown if all references have already been released and there is no longer anything to track.
+ ///
+ 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();
+ }
+ }
+}