长时间运行的任务触发更新事件的竞争条件

问题描述 投票:0回答:2

我的目标是使用基于事件的异步编程 (EBAP) 模型实现并发方法,以处理视图模型上的状态更改。我有一个从 UI 线程排队的长时间运行的任务,在 viewmodel 类上使用

RunNextItem
方法,看起来像下面的
DemoViewModel
类。

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();
    }
}

实现长时间运行任务的类如下所示:

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");
    }

    // ... 
}

想法是

DemoViewModel
类存储要“运行”的对象的
ItemViewModel
的本地实例。 “运行”的执行在
DemoRunClass
中处理,它简单地执行许多方法,每个方法都会引发一个事件来表示任务的进度/状态。完成后,它会以“完成”状态再次触发相同的事件。任务由
DemoViewModel
等待,之后它将
_currentlyRunningItem
字段设置为空;

DemoViewModel
中的事件处理程序处理这些状态事件并相应地更新
_currentlyRunningItem
视图模型。来自 UI 线程的调度程序作为参数传入,因此事件更新可以在 UI 线程上处理。

问题是: 存在竞争条件。 “All Complete”的事件在 UI 线程调度程序上调用,但在 await 将控制权交还给调用方法之后,立即命中 finally 块并将当前项设置为 null,然后是

OnUpdateEvent
事件处理程序尝试使用空的当前项目字段将抛出异常。

Dispatcher.Invoke 的 MSDN 文档声称它“在 Dispatcher 关联的线程上同步执行指定的委托。”这与我上面的经验不一致,它似乎异步调用委托并返回之前我的事件处理程序方法被调用。我也尝试过使用

SyncronisationContext.Send()
而不是调度程序,结果完全相同。

我开始在这里扯头发了。要么我完全误解了“同步”的意思,要么我做错了一些可怕的事情。我怎样才能解决这个问题并确保程序按照我想要的方式执行,或者有比这更好的解决方案吗?

c# events concurrency task-parallel-library race-condition
2个回答
2
投票

还有比这更好的解决方案吗?

绝对。有一个 standard progress pattern 可以大大简化这个:

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");
  }
}

备注:


1
投票

答案: 通过从 RunNextItem() 方法中删除“.ConfigureAwait(false)”解决了竞争条件。我不完全了解引擎盖下发生的事情,但似乎这允许在与事件处理程序不同的线程上立即继续,这反过来又导致了竞争条件。

© www.soinside.com 2019 - 2024. All rights reserved.