繁体   English   中英

使用async / await时是否需要使字段成为线程安全的?

[英]Is it needed to make fields thread-safe when using async/await?

有时我遇到访问对象字段的async / await代码。 例如,Stateless项目中的这段代码:

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之后执行的代码不一定必须在同一个上下文中执行。 所以这里的while循环可以在ThreadPool线程上执行。 我不知道是什么确保_firing_eventQueue字段是同步的,例如在这里创建一个lock / memory-fence / barrier的是什么? 所以我的问题是; 我需要使字段是线程安全的,还是在async / await结构中处理这个问题?

编辑:澄清我的问题; 在这种情况下,应始终在同一线程上调用InternalFireQueuedAsync 在这种情况下,只有延续可以在不同的线程上运行,这让我想知道,我是否需要同步机制(如显式屏障)以确保值同步以避免此处描述的问题: http:// www。 albahari.com/threading/part4.aspx

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

我不知道是什么确保_firing和_eventQueue字段是同步的,例如在这里创建一个lock / memory-fence / barrier的是什么? 所以我的问题是; 我需要使字段是线程安全的,还是在async / await结构中处理这个问题?

await将确保所有必要的记忆障碍到位。 但是,这并不能使它们“线程安全”。

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

然后_firing是好的,不需要volatile或类似的东西。

但是, _eventQueue的使用不正确。 考虑当线程池线程在await之后恢复代码时会发生什么:完全有可能Queue<T>.CountQueue<T>.Dequeue()将被线程池线程同时调用Queue<T>.Enqueue由主线程调用。 这不是线程安全的。

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

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

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

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

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

使用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;
}

}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM