【问题标题】:Is it safe to dispose a SemaphoreSlim while waiting for pending operations to cancel?在等待挂起的操作取消时处置 SemaphoreSlim 是否安全?
【发布时间】:2021-08-16 07:59:45
【问题描述】:

我不得不使用 SemaphoreSlim 来确保对我的代码的某些部分进行单线程访问,并且想确保我正确地处理了所有内容。假设我有以下课程:

public class Foo
{
    private readonly CancellationTokenSource _canceller = new CancellationTokenSource();
    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1);

    ~Foo()
    {
        Dispose(false);
        GC.SuppressFinalize(this);
    }

    public void Dispose()
    {
        Dispose(true);
    }

    protected void Dispose(bool disposing)
    {
        if (_disposed)
            return;

        _canceller.Cancel();

        if (_disposing)
            _semaphore.Dispose();

        _disposed = true;
    }

    public async Task ExecuteAsync()
    {
        try
        {
            await _semaphore.WaitAsync(_canceller.Token);
        }
        catch (OperationCanceledException)
        {
            throw new ObjectDisposedException(nameof(Foo), "Cannot execute on a Foo after it has been disposed");
        }
        
        try
        {
            // Critical section
        }
        finally
        {
            _semaphore.Release();
        }
    }
}

当我调用Dispose(true) 时,我实际上是一个接一个地执行以下几行:

_canceller.Cancel();
_semaphore.Dispose();

我的问题是,当前等待关键部分的任何其他线程/任务会发生什么?我能保证他们总是首先看到 cancellation,因此信号量被处理不会有问题吗?到目前为止,这还没有造成任何问题,但这并不意味着它是安全的。

【问题讨论】:

标签: c# semaphore dispose


【解决方案1】:

documentation for SemaphoreSlim 似乎对您的问题非常具体。

SemaphoreSlim 的所有公共和受保护成员都是线程安全的,并且可以从多个线程同时使用,但 Dispose() 除外,它必须仅在 SemaphoreSlim 上的所有其他操作完成时使用。

保证所有其他操作都已完成的唯一真正方法是确保所有任务都已完成,如文档中的示例所示。

public class Foo
{
    private readonly List<Task> _semaphoreTasks = new list<Task>();

    protected void Dispose(bool disposing)
    {
        if (_disposed)
            return;

        _canceller.Cancel();

        if (_disposing) 
        {
            Task.WaitAll(tasks);
            _semaphore.Dispose();
        }

        _disposed = true;
    }

    public async Task ExecuteAsync()
    {
        try
        {
            var task = _semaphore.WaitAsync(_canceller.Token);
            _semaphoreTasks.Add(task);
            await task;
        // ...

这也很可能需要确保在 dispose 执行时将内容添加到列表中。 (您可以将私有类变量设置为局部方法变量,然后将类变量设置为null)

【讨论】:

  • 我希望通过使用取消令牌,我可以保证所有其他操作都已完成。我不确定 SemaphoreSlim 类在等待操作被取消后是否继续在内部执行其他代码,但我认为不会。
  • 另一种选择是我跟踪已对 ExecuteAsync 进行了多少调用,并等待它们全部取消,然后再继续处置(我宁愿不这样做),或者记录 Dispose不是线程安全的,并让调用者在处理之前等待。我只是在考虑在操作期间调用处理 Dispose 的其他类,例如“Stream”和“Socket”。这是一个基于连接的类,我想要类似的行为
  • 或者只是将所有任务存储在一个列表中,如示例中一样,然后在处理之前等待它们,也如示例中。
  • @AndrewWilliamson: I'm not sure if the SemaphoreSlim class continues executing other code internally after a wait operation has been cancelled - Cancel 只是启动取消。其他代码仍在执行SemaphoreSlim.Wait。也许该代码在调用 SemaphoreSlim.Dispose 之前完成,也许不是。我会考虑根本不打电话给SemaphoreSlim.Dispose
  • @ErikPhilips 如果不释放 SemaphoreSlim 是安全的,我宁愿选择那个选项,因为它使实现更简单,更不容易出错。似乎有some debate in this question 关于我们是否需要处置,以及我们是否应该依赖这种特定于实现的细节,但我认为这里的利大于弊。感谢您的帮助
猜你喜欢
  • 1970-01-01
  • 2010-12-27
  • 1970-01-01
  • 2014-10-07
  • 1970-01-01
  • 1970-01-01
  • 2016-10-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多