限制使用SemaphoreSlim访问共享变量是否可以保证所有写入都可见?

时间:2017-10-10 11:28:00

标签: c# multithreading asynchronous semaphore

摘要

我有一个使用lock的类来提供对私有字段的线程安全访问。但是,由于下面详述的原因,我正在考虑切换到使用SemaphoreSlim来实现线程安全性。我知道如果我用lock (_lock)(我现在这样做)围绕对该字段的所有访问,我保证对该字段的写入将是原子,所有线程都会看到最近写的价值(source)。我想知道我是否用SemaphoreSlim获得这两种保证,或者只是保证原子写入。

详细

我有一个接口,我写了一段时间来代表可以用来与外部设备通信的服务:

public interface ICommunicationService : IDisposable
{
    event EventHandler<ReceivedMessageEventArgs> MessageReceived;
    void SendMessage(GenericMessage message);
    void Start();
}

我最近遇到过这样一种情况,我更喜欢这些方法的异步实现,显然当你开始做异步时,你需要用它来整理它以避免死锁等,所以我打算更新界面和我为支持异步操作而编写的所有现有实现:

public interface ICommunicationService : IDisposable
{
    event EventHandler<ReceivedMessageEventArgs> MessageReceived;
    Task SendMessage(GenericMessage message);
    Task Start();
}

我的一个实现此接口的类在其SendMessage()Start()的实现中执行了一系列锁定:

public virtual void SendMessage(GenericMessage message)
{
    CheckDisposed();

    lock (_lock)
    {
        if (CommunicationState != CommunicationState.Up)
        {
            throw new InvalidOperationException("Message cannot be sent as communication is not established.");
        }

        try
        {
            _currentDelegate.SendMessage(message);
        }
        catch (Exception)
        {
            TerminateCommunication();
        }
    }
}

尝试将此代码切换为异步实现,我注意到我无法在lock语句中等待任务。根据异步大师Stephen Cleary的blog post on the subject,执行此操作的异步方式是使用SemaphoreSlim

根据该帖子,我已将SendMessage实施更改为:

public virtual async Task SendMessage(GenericMessage message)
{
    CheckDisposed();

    if (CommunicationState != CommunicationState.Up)
    {
        throw new InvalidOperationException("Message cannot be sent as communication is not established.");
    }

    // _mutex = new SemaphoreSlim(0, 1)
    await _mutex.WaitAsync().ConfigureAwait(false);

    try
    {
        await _currentDelegate.SendMessage(message);
    }
    catch (Exception)
    {
        TerminateCommunication();
    }
    finally
    {
        _mutex.Release();
    }
}

我想知道的是,我是否保证任何给定的线程在执行_currentDelegate时会看到await _currentDelegate.SendMessage(message)的最新值,或者我是否需要使用其他构造来确保其他线程立即可以看到写入。

特别是,这个类有另一种方法TryRestartCommunication

private bool TryRestartCommunication(bool initialStart)
{
    // ...
    lock (_lock)
    {
        // ...
        try
        {
            _currentDelegate = _factory();
            _currentDelegate.Start();
            _currentDelegate.MessageReceived += MessageReceived;
            CommunicationState = CommunicationState.Up;
            return true;
        }
        // ...
    }
}

如果我重新实现此方法来锁定信号量并且有一些线程A调用TryRestartCommunication()并且在线程B调用SendMessage()之后立即执行,我保证线程B将看到新的值线程A设置_currentDelegate

如果没有,那么一个合适的解决方案是让_currentDelegate易变,还是使用Interlocked更新其值?

修改

得到了密切的投票,因为显然这个问题不够明确。让我尝试尽可能清楚:如果我从使用lock保护我的关键区域切换到SemaphoreSlim,我是否需要将共享字段标记为volatile(或者某些内容)类似的)确保相同级别的线程安全性?

1 个答案:

答案 0 :(得分:2)

如果您通过lock()保护关键区域/数据切换到SemaphoreSlim(1),您将获得相同级别的线程安全性。

我们已经多次完成,并且从未遇到过单一数据保护错误。

但是,请注意,在await xxx.ConfigureAwait(false);之后,您可能会在不同的同步上下文(线程)中结束,并且方法await _currentDelegate.SendMessage(message);可能无法正常使用它。例如,如果该方法访问Webform UI控件。