简体   繁体   English

从多个线程更新 IProgress 是否安全?

[英]Is it safe to update IProgress from multiple threads?

I have some c# code (MVC WebAPI) which iterates over an array of IDs in parallel and makes an API call for each entry.我有一些 c# 代码 (MVC WebAPI),它并行遍历一组 ID 并对每个条目进行 API 调用。 In the first version, the whole code was a simple, synchronous for loop.在第一个版本中,整个代码是一个简单的同步 for 循环。 Now we changed that to a combination of Task.WhenAll and a LINQ select:现在我们将其更改为 Task.WhenAll 和 LINQ 选择的组合:

private async Task RunHeavyLoad(IProgress<float> progress) {
  List<MyObj> myElements = new List<MyObj>(someEntries);
  float totalSteps = 1f / myElements.Count();
  int currentStep = 0;

  await Task.WhenAll(myElements.Select(async elem => {
    var result = await SomeHeavyApiCall(elem);
    DoSomethingWithThe(result);
    progress.Report(totalSteps * System.Threading.Interlocked.Increment(ref currentStep) * .1f);
  }

  // Do some more stuff
}

This is a simplified version of the original method!这是原始方法的简化版本! The actual method EnforceImmediateImport is called by this SignalR hub method:实际方法EnforceImmediateImport由这个 SignalR 集线器方法调用:

public class ImportStatusHub : Hub {
  public async Task RunUnscheduledImportAsync(DateTime? fromDate, DateTime? toDate) {
    Clients.Others.enableManualImport(false);

    try {
      Progress<float> progress = new Progress<float>((p) => Clients.All.updateProgress(p));
      await MvcApplication.GlobalScheduler.EnforceImmediateImport(progress, fromDate, toDate);

    } catch (Exception ex) {
      Clients.All.importError(ex.Message);
    }

    Clients.Others.enableManualImport(true);
  }
}

Now I wonder, if this is "thread safe" per se, or if I need to do something with the progress.Report calls to prevent anything from going wrong.现在我想知道,这本身是否是“线程安全的”,或者我是否需要对progress.Report调用做一些事情以防止出现任何问题。

From the docs :文档

Remarks评论

Any handler provided to the constructor or event handlers registered with the ProgressChanged event are invoked through a SynchronizationContext instance captured when the instance is constructed.提供给构造函数的任何处理程序或注册到 ProgressChanged 事件的事件处理程序都通过构造实例时捕获的 SynchronizationContext 实例调用。 If there is no current SynchronizationContext at the time of construction, the callbacks will be invoked on the ThreadPool.如果在构造时没有当前 SynchronizationContext,回调将在 ThreadPool 上调用。

For more information and a code example, see the article Async in 4.5: Enabling Progress and Cancellation in Async APIs in the .NET Framework blog.有关详细信息和代码示例,请参阅 .NET Framework 博客中的文章 4.5 中的异步:在异步 API 中启用进度和取消。

Like anything else using the SynchronizationContext , it's safe to post from multiple threads.与使用SynchronizationContext的任何其他内容一样,从多个线程发布是安全的。

Custom implementations of IProgress<T> should have their behavior defined. IProgress<T>的自定义实现应该定义其行为。

On your question, internally, Progress only does invoking.关于您的问题,在内部,Progress 仅进行调用。 It is up to the code you wrote to handle the progress on the other side.由您编写的代码来处理另一端的进度。 I would say that the line progress.Report(totalSteps * System.Threading.Interlocked.Increment(ref currentStep) *.1f);我会说行progress.Report(totalSteps * System.Threading.Interlocked.Increment(ref currentStep) *.1f); can cause a potential progress reporting issue due to the multiplication which is not atomic.由于乘法不是原子的,可能会导致潜在的进度报告问题。

This is what happens internally inside Progress when you call Report 当您调用 Report 时,这就是 Progress 内部发生的事情

  protected virtual void OnReport(T value)
  {
      // If there's no handler, don't bother going through the sync context.
      // Inside the callback, we'll need to check again, in case 
      // an event handler is removed between now and then.
      Action<T> handler = m_handler;
      EventHandler<T> changedEvent = ProgressChanged;
      if (handler != null || changedEvent != null)
      {
          // Post the processing to the sync context.
          // (If T is a value type, it will get boxed here.)
          m_synchronizationContext.Post(m_invokeHandlers, value);
      }
  }

On the code though, a better way to run in parallel is to use PLinq.不过在代码上,并行运行的更好方法是使用 PLinq。 In your current code, if the list contains many items, it will spin up tasks for every single item at the same time and wait for all of them to complete.在您当前的代码中,如果列表包含许多项目,它将同时为每个项目启动任务并等待所有项目完成。 However, in PLinq, the number of concurrent executions will be determined for you to optimize performance.但是,在 PLinq 中,并发执行的数量将由您决定,以优化性能。

myElements.AsParallel().ForAll(async elem =>
{
    var result = await SomeHeavyApiCall(elem);
    DoSomethingWithThe(result);
    progress.Report(totalSteps * System.Threading.Interlocked.Increment(ref currentStep) * .1f);
}

Please keep in mind that AsParallel().ForAll() will immediately return when using async func.请记住,当使用异步函数时,AsParallel().ForAll() 将立即返回。 So you might want to capture all the tasks and wait for them before you proceed.因此,您可能希望捕获所有任务并在继续之前等待它们。

One last thing, if your list is being edited while it is being processed, i recommend using ConcurrentQueue or ConcurrentDictionary or ConcurrentBag.最后一件事,如果您的列表在处理时正在编辑,我建议使用 ConcurrentQueue 或 ConcurrentDictionary 或 ConcurrentBag。

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

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