有时候,我会遇到访问对象字段的异步/等待代码。例如,以下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
答案 0 :(得分:3)
我看不到是什么确保_firing和_eventQueue字段同步,例如,是什么在这里创建锁/内存围栏/屏障?所以我的问题是;我需要使字段成为线程安全的,还是async / await结构中的某些东西来解决这个问题?
.
将确保所有必要的内存屏障均已到位。但是,这并不能使它们“具有线程安全性”。
在这种情况下,应始终在同一线程上调用InternalFireQueuedAsync。
那么await
很好,不需要_firing
或类似的东西。
但是,volatile
的使用不正确。考虑一下线程池线程在_eventQueue
之后恢复代码时会发生什么:线程池线程同时{{1}或await
会调用Queue<T>.Count
或Queue<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)
使用lock
或ConcurrentQueue
。
使用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;
}
}