使用异步/等待时是否需要使字段成为线程安全的?

时间:2018-12-06 10:35:35

标签: c# multithreading asynchronous async-await task-parallel-library

有时候,我会遇到访问对象字段的异步/等待代码。例如,以下snippet来自无状态项目的代码:

private readonly Queue<QueuedTrigger> _eventQueue = new Queue<QueuedTrigger>();
private bool _firing;

async Task InternalFireQueuedAsync(TTrigger trigger, params object[] args)
{
    if (_firing)
    {
        _eventQueue.Enqueue(new QueuedTrigger { Trigger = trigger, Args = args });
        return;
    }

    try
    {
        _firing = true;

        await InternalFireOneAsync(trigger, args).ConfigureAwait(false);

        while (_eventQueue.Count != 0)
        {
            var queuedEvent = _eventQueue.Dequeue();
            await InternalFireOneAsync(queuedEvent.Trigger, queuedEvent.Args).ConfigureAwait(false);
        }
    }
    finally
    {
        _firing = false;
    }
}

如果我正确理解await **.ConfigureAwait(false),则表明在此await之后not necessarily之后执行的代码必须在相同的上下文中执行。因此,此处的while循环可以在ThreadPool线程上执行。我看不到如何确保_firing_eventQueue字段同步,例如,是什么在这里创建了锁/内存围栏/屏障?所以我的问题是;我需要使字段成为线程安全的,还是async / await结构中的某些东西来解决这个问题?

编辑:以澄清我的问题;在这种情况下,InternalFireQueuedAsync应该始终在同一线程上调用。在那种情况下,只有延续可以在不同的线程上运行,这使我感到奇怪,我是否需要同步机制(如显式屏障)来确保值被同步以避免出现此处描述的问题:http://www.albahari.com/threading/part4.aspx < / p>

编辑2:在无状态下也有一个小讨论: https://github.com/dotnet-state-machine/stateless/issues/294

4 个答案:

答案 0 :(得分:3)

  

我看不到是什么确保_firing和_eventQueue字段同步,例如,是什么在这里创建锁/内存围栏/屏障?所以我的问题是;我需要使字段成为线程安全的,还是async / await结构中的某些东西来解决这个问题?

.将确保所有必要的内存屏障均已到位。但是,这并不能使它们“具有线程安全性”。

  

在这种情况下,应始终在同一线程上调用InternalFireQueuedAsync。

那么await很好,不需要_firing或类似的东西。

但是,volatile的使用不正确。考虑一下线程池线程在_eventQueue之后恢复代码时会发生什么:线程池线程同时{{1}或await会调用Queue<T>.CountQueue<T>.Dequeue() 1}}由主线程调用。这不是线程安全的。

如果调用Queue<T>.Enqueue的主线程是具有单线程上下文的线程(例如UI线程),那么一个简单的解决方法是在此方法中删除InternalFireQueuedAsync的所有实例。

答案 1 :(得分:0)

为了安全起见,应将字段_firing标记为volatile-这将保证内存屏障,并确保可能在不同线程上运行的延续部分将读取正确的值。如果没有volatile,则编译器,CLR或JIT编译器甚至CPU可能会进行一些优化,导致代码为其读取错误的值。

对于_eventQueue,您无需修改​​该字段,因此将其标记为volatile是没有用的。如果只有一个线程调用“ InternalFireQueuedAsync”,则您不会同时从多个线程访问它,所以您可以。

但是,如果多个线程调用InternalFireQueuedAsync,则您将需要使用ConcurrentQueue或锁定对_eventQueue的访问。然后,您最好也锁定对_firing的访问权限,或者使用Interlocked对其进行访问,或者将其替换为ManualResetEvent

答案 2 :(得分:0)

ConfigureAwait(false)表示未捕获 Context 来运行延续。使用线程池上下文并不意味着继续并行运行。在await循环之前和之内使用while可确保代码(继续)按顺序运行,因此在这种情况下无需锁定。 但是,在检查_firing值时,您可能有竞赛条件

答案 3 :(得分:0)

使用lockConcurrentQueue

使用lock的解决方案:

private readonly Queue<QueuedTrigger> _eventQueue = new Queue<QueuedTrigger>();
private bool _firing;
private object _eventQueueLock = new object();

async Task InternalFireQueuedAsync(TTrigger trigger, params object[] args)
{
if (_firing)
{
    lock(_eventQueueLock)
       _eventQueue.Enqueue(new QueuedTrigger { Trigger = trigger, Args = args });
    return;
}

try
{
    _firing = true;

    await InternalFireOneAsync(trigger, args).ConfigureAwait(false);

    lock(_eventQueueLock)
    while (_eventQueue.Count != 0)
    {
        var queuedEvent = _eventQueue.Dequeue();
        await InternalFireOneAsync(queuedEvent.Trigger, queuedEvent.Args).ConfigureAwait(false);
    }
}


finally
{
    _firing = false;
}

}

使用ConcurrentQueue的解决方案:

private readonly ConccurentQueue<QueuedTrigger> _eventQueue = new ConccurentQueue<QueuedTrigger>();
private bool _firing;

async Task InternalFireQueuedAsync(TTrigger trigger, params object[] args)
{
if (_firing)
{
    _eventQueue.Enqueue(new QueuedTrigger { Trigger = trigger, Args = args });
    return;
}

try
{
    _firing = true;

    await InternalFireOneAsync(trigger, args).ConfigureAwait(false);

    lock(_eventQueueLock)
    while (_eventQueue.Count != 0)
    {
        object queuedEvent; // change object > expected type
        if(!_eventQueue.TryDequeue())
           continue;
        await InternalFireOneAsync(queuedEvent.Trigger, queuedEvent.Args).ConfigureAwait(false);
    }
}


finally
{
    _firing = false;
}

}