英文:
Race condition from long running task firing update events
问题
我的目标是使用基于事件的异步编程(EBAP)模型实现并发方法,以处理视图模型上的状态更改。我有一个长时间运行的任务,该任务是从UI线程排队的,使用视图模型类上的 RunNextItem
方法,该类看起来像下面的 DemoViewModel
类。
public class DemoViewModel
{
private DemoRunClass DemoRunner;
private ItemViewModel _currentlyRunningItem;
/// <summary>
/// 在另一个线程上执行长时间运行的任务。
/// </summary>
public async Task RunNextItem()
{
// 获取下一个项目的视图模型并为其创建一个命令。
var nextRunnableItemViewModel = GetNextItem();
var runItemCommand = GenerateCommand(nextRunnableItemViewModel);
// 在本地变量中存储当前项目
_currentlyRunningItem = nextRunnableItemViewModel;
try
{
await DemoRunner.ExecuteLongRunningTask(
runItemCommand, Dispatcher.CurrentDispatcher).ConfigureAwait(false);
}
catch (Exception ex)
{ ... }
finally
{
// 清除本地变量
_currentlyRunningItem = null;
}
}
/// <summary>
/// 处理来自 DemoRunner 类的更新
/// </summary>
private void OnUpdateEvent(object status)
{
if (status == "Method1 complete")
_curentlyRunningItem.OnMethod1Complete();
// ...
else if (status == "All Complete")
_curentlyRunningItem.OnAllComplete();
}
}
实现长时间运行任务的类如下:
public class DemoRunClass
{
public event EventHandler<object> ExecutionUpdate;
public Dispatcher EventDispatcher;
public Task ExecuteLongRunningTask(object command, Dispatcher dispatcher)
{
EventDispatcher = dispatcher; // 存储来自UI线程的调度程序
return Task.Factory.StartNew(() =>
{
Method1(); // 调用 UpdateEvent(方法1完成)
Method2(); // 调用 UpdateEvent(方法2完成)..等等
Method3();
Method4();
Method5();
UpdateEvent("All Complete");
});
}
public void UpdateEvent(object status)
{
if (!EventDispatcher.CheckAccess())
EventDispatcher.Invoke(new Action<object>(UpdateEvent), status);
else
{
ExecutionUpdate?.Invoke(null, status);
}
}
private void Method1()
{
// 做一些工作..
UpdateEvent("Method1 complete");
}
// ...
}
这个思想是,DemoViewModel
类存储要“运行”的对象的 ItemViewModel
的本地实例。执行“运行”由 DemoRunClass
处理,它简单地执行多个方法,每个方法都会引发一个事件来表示任务的进度/状态。当任务完成时,它再次触发相同的事件,状态为“Complete”。DemoViewModel
等待任务完成,然后将 _currentlyRunningItem
字段设置为 null。
DemoViewModel
中的事件处理程序处理这些状态事件,并相应地更新 _currentlyRunningItem
视图模型。来自UI线程的调度程序作为参数传递,以便可以在UI线程上处理事件更新。
**问题是:**存在竞争条件。事件“All Complete”在UI线程调度程序上被调用,但在等待期间控制立即返回到调用方法之后,finally
块被执行,并将当前项目设置为 null,然后 OnUpdateEvent
事件处理程序将抛出一个异常,尝试使用 null 的当前项目字段。
Dispatcher.Invoke 的 MSDN 文档声称它“在与 Dispatcher 关联的线程上同步执行指定的委托”。这与我上面的经验不一致,似乎它会异步调用委托并在我的事件处理程序方法被调用之前返回。我还尝试使用 SyncronisationContext.Send()
代替调度程序,结果完全相同。
我开始拔头发了。要么我完全误解了“同步”的含义,要么我做错了什么。如何解决这个问题,确保程序按照我想要的方式执行,或者是否有比这更好的解决方案?
英文:
My goal is to implement a concurrent method using event based asynchronous programming (EBAP) model, to handle status changes on a viewmodel. I have a long running task that is queued from the UI thread, using a RunNextItem
method on a viewmodel class that looks like the DemoViewModel
class below.
public class DemoViewModel
{
private DemoRunClass DemoRunner;
private ItemViewModel _currentlyRunningItem;
/// <summary>
/// Execute long running task on another thread.
/// </summary>
public async Task RunNextItem()
{
// get viewmodel of next item and create a command for it.
var nextRunnableItemViewModel = GetNextItem();
var runItemCommand = GenerateCommand(nextRunnableItemViewModel);
// store the current item in local variable
_currentlyRunningItem = nextRunnableItemViewModel;
try
{
await DemoRunner.ExecuteLongRunningTask(
runItemCommand, Dispatcher.CurrentDispatcher).ConfigureAwait(false);
}
catch (Exception ex)
{ ... }
finally
{
// clear local variable
_currentlyRunningItem = null;
}
}
/// <summary>
/// Handle updates from DemoRunner class
/// </summary>
private void OnUpdateEvent(object status)
{
if (status == "Method1 complete")
_curentlyRunningItem.OnMethod1Complete();
// ...
else if (status == "All Complete")
_curentlyRunningItem.OnAllComplete();
}
}
The class that implements the long running task looks like:
public class DemoRunClass
{
public event EventHandler<object> ExecutionUpdate;
public Dispatcher EventDispatcher;
public Task ExecuteLongRunningTask(object command, Dispatcher dispatcher)
{
EventDispatcher = dispatcher; // store dispatcher from UI thread
return Task.Factory.StartNew(() =>
{
Method1(); // call UpdateEvent(method 1 complete)
Method2(); // call UpdateEvent(method 2 complete).. etc
Method3();
Method4();
Method5();
UpdateEvent("All Complete");
});
}
public void UpdateEvent(object status)
{
if (!EventDispatcher.CheckAccess())
EventDispatcher.Invoke(new Action<object>(UpdateEvent), status);
else
{
ExecutionUpdate?.Invoke(null, status);
}
}
private void Method1()
{
// do some work ..
UpdateEvent("Method1 complete");
}
// ...
}
The idea is that the DemoViewModel
class stores a local instance of the ItemViewModel
of the object that is to be "run". The execution of the "run" is handled in the DemoRunClass
which simply executes a number of methods, each of which will raise an event to signal the progress/status of the task. When it is complete, it fires the same event again with a "Complete" status. The task is awaited by the DemoViewModel
, after which it sets the _currentlyRunningItem
field to null;
The event handler in DemoViewModel
handles these status events and updates the _currentlyRunningItem
viewmodel accordingly. The dispatcher from the UI thread is passed in as a parameter so the event updates can be handled back on the UI thread.
The problem is: there is a race condition. The event for "All Complete" is invoked on the UI thread dispatcher, but then immediately after the await yields control back to the calling method, the finally block is hit and sets the current item to null, and then the OnUpdateEvent
event handler will throw an exception trying to use the null current item field.
The MSDN docs for Dispatcher.Invoke claims it "Executes the specified delegate synchronously on the thread the Dispatcher is associated with." This does not align with my experience above, where it seems to invoke the delegate asynchronously and return before my eventhandler method is called. I have also tried using SyncronisationContext.Send()
instead of the dispatcher, with the exact same result.
I'm starting to pull hairs here. Either Ι'm completely misunderstanding the meaning of "synchronous" or Ι'm doing something horribly wrong. How can I fix this problem and ensure the program executes the way I want it to, or is there a better solution than this?
答案1
得分: 2
"有没有比这个更好的解决方案?"
当然有。有一种标准进度模式可以大大简化这个问题:
public class DemoViewModel
{
private DemoRunClass DemoRunner;
public async Task RunNextItem()
{
// 获取下一个项目的视图模型并为其创建一个命令。
var nextRunnableItemViewModel = GetNextItem();
var runItemCommand = GenerateCommand(nextRunnableItemViewModel);
var progress = new Progress<string>(status =>
{
if (status == "Method1 complete")
nextRunnableItemViewModel.OnMethod1Complete();
// ...
else if (status == "All Complete")
nextRunnableItemViewModel.OnAllComplete();
});
await Task.Run(() => DemoRunner.ExecuteLongRunningTask(runItemCommand, progress));
}
}
public class DemoRunClass
{
public Task ExecuteLongRunningTask(object command, IProgress<string> progress)
{
Method1(progress); // 调用 progress?.Report(method 1 complete)
Method2(progress); // 调用 progress?.Report(method 2 complete).. 等等
Method3(progress);
Method4(progress);
Method5(progress);
progress?.Report("All Complete");
}
private void Method1(IProgress<string> progress)
{
// 做一些工作..
progress?.Report("Method1 complete");
}
}
注:
- 使用
Task.Run
而不是Task.Factory.StartNew
(如我在我的博客中所解释的)。 - 在调用中使用
Task.Run
,而不是在实现中使用它(如我在我的博客中所解释的)。 Progress<T>
会自动捕获当前的SynchronizationContext
(在这种情况下会捕获当前的Dispatcher
)。
英文:
> is there a better solution than this?
Absolutely. There's a standard progress pattern that simplifies this considerably:
public class DemoViewModel
{
private DemoRunClass DemoRunner;
public async Task RunNextItem()
{
// get viewmodel of next item and create a command for it.
var nextRunnableItemViewModel = GetNextItem();
var runItemCommand = GenerateCommand(nextRunnableItemViewModel);
var progress = new Progress<string>(status =>
{
if (status == "Method1 complete")
nextRunnableItemViewModel.OnMethod1Complete();
// ...
else if (status == "All Complete")
nextRunnableItemViewModel.OnAllComplete();
});
await Task.Run(() => DemoRunner.ExecuteLongRunningTask(runItemCommand, progress));
}
}
public class DemoRunClass
{
public Task ExecuteLongRunningTask(object command, IProgress<string> progress)
{
Method1(progress); // call progress?.Report(method 1 complete)
Method2(progress); // call progress?.Report(method 2 complete).. etc
Method3(progress);
Method4(progress);
Method5(progress);
progress?.Report("All Complete");
}
private void Method1(IProgress<string> progress)
{
// do some work ..
progress?.Report("Method1 complete");
}
}
Notes:
- Use
Task.Run
instead ofTask.Factory.StartNew
(as explained on my blog). - Use
Task.Run
in the invocation, not as the implementation (as explained on my blog). Progress<T>
captures the currentSynchronizationContext
for you (which in this case captures the currentDispatcher
).
答案2
得分: 1
已解决竞争条件,方法RunNextItem()中已删除".ConfigureAwait(false)"。我不完全理解底层发生了什么,但似乎这会允许在不同线程上立即继续执行事件处理程序,从而导致竞争条件。
英文:
Answer: The race condition was solved by removing the ".ConfigureAwait(false)" from the RunNextItem() method. I don't have a complete understanding of what was happening under the hood, but it seems that this was allowing immediate continuation on a different thread than the event handlers which in turn caused the race condition.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论