【问题标题】:How to parallel execute tasks on separate CPU cores如何在不同的 CPU 内核上并行执行任务
【发布时间】:2016-08-18 23:49:47
【问题描述】:

我有一个 WinForms C# 程序,我将在用户计算机上一次(在内存中)打开多达 100 万个业务对象。

我的经理要求对这些业务对象进行真正简单的过滤。 因此,如果您过滤“Fred”,用户将看到在任何文本字段(姓名、地址、联系人等)中包含“Fred”的所有对象的列表。 此外,这需要在不阻塞 UI 的情况下尽可能接近实时。 因此,如果您在过滤器文本框中输入“Fred”,只要输入“F”,搜索就会开始在任何文本字段中查找带有“F”的结果(我想我可能会坚持至少搜索中的 3 个字符)。 当文本框更改为“Fr”时,旧的搜索将停止(如果仍在执行)并开始新的搜索。

这是在用户本地机器上以零 IO 执行的高度 CPU 密集型操作。 这听起来像是我应该启动单独的任务以在我的 CPU 上单独内核上的单独线程上运行。 当它们都完成后,将结果合并到一个列表中并向用户显示结果。

我是老派,这听起来像是 BackgroundWorker 的工作,但我读到 BackgroundWorker 在 .NET 4.5 中被明确标记为过时(悲伤的脸)。见:Async/await vs BackgroundWorker

我发现很多帖子说我应该用新的 async await c# 命令替换 BackgroundWorker。

但是,这方面的好例子很少,我发现 cmets 类似于“异步等待不保证单独的线程”,所有示例都显示等待任务的 IO / 网络密集型任务(不是 CPU 密集型任务) .

我找到了一个寻找素数的 BackgroundWorker 的好例子,这是一个类似的 CPU 密集型任务,我尝试了一下,发现它可以满足我的大部分需求。但我的问题是 BackgroundWorker 在 .NET 4.5 中已过时。

我在 BackgroundWorker 调查中的发现是:

  • 当您每个人有一个任务时,可以获得最佳的性能改进 机器上的物理核心,我的虚拟机有 3 个核心,任务运行 最快的 3 个后台工作任务。
  • 当您使用时,性能就会消失 有太多的后台工作任务。
  • 当你有 返回 UI 线程的进度通知过多。

问题:

后台工作者是否适合用于此类 CPU 密集型任务? 如果不是,什么技术更好? 对于这样的 CPU 密集型任务,有什么好的例子吗? 如果我使用后台工作者,我会承担什么风险?

基于单个后台工作者的代码示例

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

// This code is based on code found at: https://social.msdn.microsoft.com/Forums/vstudio/en-US/b3650421-8761-49d1-996c-807b254e094a/c-backgroundworker-for-progress-dialog?forum=csharpgeneral
// Well actually at: http://answers.flyppdevportal.com/MVC/Post/Thread/e98186b1-8705-4840-ad39-39ac0bdd0a33?category=csharpgeneral

namespace PrimeNumbersWithBackgroundWorkerThread
{
  public partial class Form_SingleBackground_Worker : Form
  {
    private const int        _MaxValueToTest    = 300 * 1000; 
    private const int        _ProgressIncrement = 1024 * 2  ; // How often to display to the UI that we are still working
    private BackgroundWorker _Worker;
    private Stopwatch        _Stopwatch;
    public Form_SingleBackground_Worker()
    {
      InitializeComponent();
    }
    private void btn_Start_Click           ( object sender, EventArgs e)
    {
      if ( _Worker == null )
      {
        progressBar.Maximum                 = _MaxValueToTest;
        txt_Output.Text                     = "Started";
        _Stopwatch                          = Stopwatch.StartNew();
        _Worker                             = new BackgroundWorker();
        _Worker.WorkerReportsProgress       = true;
        _Worker.WorkerSupportsCancellation  = true;
        _Worker.DoWork                     += new DoWorkEventHandler            ( worker_DoWork             );
        _Worker.ProgressChanged            += new ProgressChangedEventHandler   ( worker_ProgressChanged    );
        _Worker.RunWorkerCompleted         += new RunWorkerCompletedEventHandler( worker_RunWorkerCompleted );
        _Worker.RunWorkerAsync( _MaxValueToTest );  // do the work
      }
    }
    private void btn_Cancel_Click          ( object sender, EventArgs e)
    {
      if ( _Worker != null && _Worker.IsBusy)
      {
        _Worker.CancelAsync();
      }
    }
    private void worker_DoWork             ( object sender, DoWorkEventArgs e)
    {
      int              lMaxValueToTest    = (int)e.Argument;
      BackgroundWorker lWorker            = (BackgroundWorker)sender; // BackgroundWorker running this code for Progress Updates and Cancelation checking
      List<int>        lResult            = new List<int>(); 
      long             lCounter           = 0;

      //Check all uneven numbers between 1 and whatever the user choose as upper limit
      for (int lTestValue = 1; lTestValue < lMaxValueToTest; lTestValue += 2)
      {
        lCounter++;
        if ( lCounter % _ProgressIncrement == 0 )
        {
          lWorker.ReportProgress(lTestValue);  // Report progress to the UI every lProgressIncrement tests (really slows down if you do it every time through the loop)
          Application.DoEvents();

          //Check if the Cancelation was requested during the last loop
          if (lWorker.CancellationPending )
          {
            e.Cancel = true; //Tell the Backgroundworker you are canceling and exit the for-loop
            e.Result = lResult.ToArray(); 
            return;
          }
        }

        bool lIsPrimeNumber = IsPrimeNumber( lTestValue ); //Determine if lTestValue is a Prime Number
        if ( lIsPrimeNumber )
          lResult.Add(lTestValue);
      }
      lWorker.ReportProgress(lMaxValueToTest);  // Tell the progress bar you are finished
      e.Result = lResult.ToArray();                // Save Return Value
    }
    private void worker_ProgressChanged    ( object sender, ProgressChangedEventArgs e)
    {
      int lNumber       = e.ProgressPercentage;
      txt_Output.Text   = $"{lNumber.ToString("#,##0")} ({(lNumber/_Stopwatch.ElapsedMilliseconds).ToString("#,##0")} thousand per second)";
      progressBar.Value = lNumber;
      Refresh();
    }
    private void worker_RunWorkerCompleted ( object sender, RunWorkerCompletedEventArgs e)
    {
      progressBar.Value = progressBar.Maximum;
      Refresh();

      if ( e.Cancelled )
      {
        txt_Output.Text = "Operation canceled by user";
        _Worker         = null;
        return;
      }
      if ( e.Error != null)
      {
        txt_Output.Text = $"Error: {e.Error.Message}";
        _Worker         = null;
        return;
      }
      int[]  lIntResult = (int[])e.Result;
      string lStrResult = string.Join( ", ", lIntResult );
      string lTimeMsg   = $"Calculate all primes up to {_MaxValueToTest.ToString("#,##0")} with \r\nSingle Background Worker with only 1 worker: Total duration (seconds): {_Stopwatch.ElapsedMilliseconds/1000}";
      txt_Output.Text   = $"{lTimeMsg}\r\n{lStrResult}";
      _Worker           = null;
    }
    private bool IsPrimeNumber             ( long aValue )
    {
      // see https://en.wikipedia.org/wiki/Prime_number
      // Among the numbers 1 to 6, the numbers 2, 3, and 5 are the prime numbers, while 1, 4, and 6 are not prime.
      if ( aValue <= 1 ) return false;
      if ( aValue == 2 ) return true ;
      if ( aValue == 3 ) return true ;
      if ( aValue == 4 ) return false;
      if ( aValue == 5 ) return true ;
      if ( aValue == 6 ) return false;
      bool      lIsPrimeNumber = true;
      long      lMaxTest       = aValue / 2 + 1;
      for (long lTest          = 3; lTest < lMaxTest && lIsPrimeNumber; lTest += 2)
      {
        long lMod = aValue % lTest;
        lIsPrimeNumber = lMod != 0;
      }
      return lIsPrimeNumber;
    }
  }
}

基于多个后台工作者的代码示例

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

// This code is based on code found at: https://social.msdn.microsoft.com/Forums/vstudio/en-US/b3650421-8761-49d1-996c-807b254e094a/c-backgroundworker-for-progress-dialog?forum=csharpgeneral
// Well actually at: http://answers.flyppdevportal.com/MVC/Post/Thread/e98186b1-8705-4840-ad39-39ac0bdd0a33?category=csharpgeneral

namespace PrimeNumbersWithBackgroundWorkerThread
{
  public partial class Form_MultipleBackground_Workers : Form
  {
    private const int              _MaxValueToTest    = 300 * 1000; 
    private const int              _ProgressIncrement = 1024 * 2  ; // How often to display to the UI that we are still working
    private int                    _NumberOfChuncks   = 2         ; // Best performance looks to be when this value is same as the number of cores
    private List<BackgroundWorker> _Workers           = null      ;
    private List<WorkChunk>        _Results           = null      ;
    private Stopwatch              _Stopwatch;
    public Form_MultipleBackground_Workers () { InitializeComponent(); }
    private void btn_Start_Click           ( object sender, EventArgs e)
    {
      if ( _Workers == null )
      {
        progressBar.Maximum   = _MaxValueToTest;
        txt_Output.Text       = "Started";
        _Stopwatch            = Stopwatch.StartNew();
        _Workers              = new List<BackgroundWorker>();
        _Results              = new List<WorkChunk>();
        int lChunckSize       = _MaxValueToTest / _NumberOfChuncks;
        int lChunckStart      = 1;
        while ( lChunckStart <= _MaxValueToTest )
        {
          int lChunckEnd = lChunckStart + lChunckSize;
          if ( lChunckEnd > _MaxValueToTest ) lChunckEnd = _MaxValueToTest;
          BackgroundWorker lWorker = StartAWorker( lChunckStart, lChunckEnd );
          _Workers.Add( lWorker );
          lChunckStart += lChunckSize + 1;
        }
      }
    }
    private BackgroundWorker StartAWorker  ( int aRangeStart, int aRangeEnd )
    {
      WorkChunk        lWorkChunk         = new WorkChunk() { StartRange = aRangeStart, EndRange = aRangeEnd };
      BackgroundWorker lResult            = new BackgroundWorker();
      lResult.WorkerReportsProgress       = true;
      lResult.WorkerSupportsCancellation  = true;
      lResult.DoWork                     += new DoWorkEventHandler            ( worker_DoWork             );
      lResult.ProgressChanged            += new ProgressChangedEventHandler   ( worker_ProgressChanged    );
      lResult.RunWorkerCompleted         += new RunWorkerCompletedEventHandler( worker_RunWorkerCompleted );
      lResult.RunWorkerAsync( lWorkChunk );  // do the work
      Console.WriteLine( lWorkChunk.ToString() );
      return lResult;
    }
    private void btn_Cancel_Click          ( object sender, EventArgs e)
    {
      if ( _Workers != null )
      {
        foreach( BackgroundWorker lWorker in _Workers )
        {
          if ( lWorker.IsBusy )
            lWorker.CancelAsync();
        }
      }
    }
    private void worker_DoWork             ( object sender, DoWorkEventArgs e)
    {
      WorkChunk        lWorkChunk         = (WorkChunk)e.Argument;
      BackgroundWorker lWorker            = (BackgroundWorker)sender; // BackgroundWorker running this code for Progress Updates and Cancelation checking
      int              lCounter           = 0;
      e.Result = lWorkChunk; 
      lWorkChunk.StartTime = DateTime.Now;
      lWorkChunk.Results   = new List<int>();

      // Check all uneven numbers in range
      for ( int lTestValue = lWorkChunk.StartRange; lTestValue <= lWorkChunk.EndRange; lTestValue++ )
      {
        lCounter++;
        if ( lCounter % _ProgressIncrement == 0 )
        {
          lWorker.ReportProgress(lCounter);  // Report progress to the UI every lProgressIncrement tests (really slows down if you do it every time through the loop)
          Application.DoEvents();            // This is needed for cancel to work
          if (lWorker.CancellationPending )  // Check if Cancelation was requested
          {
            e.Cancel = true; //Tell the Backgroundworker you are canceling and exit the for-loop
            lWorkChunk.EndTime = DateTime.Now;
            return;
          }
        }

        bool lIsPrimeNumber = IsPrimeNumber( lTestValue ); //Determine if lTestValue is a Prime Number
        if ( lIsPrimeNumber )
          lWorkChunk.Results.Add(lTestValue);
      }
      lWorker.ReportProgress( lCounter );  // Tell the progress bar you are finished
      lWorkChunk.EndTime = DateTime.Now;
    }
    private void worker_ProgressChanged    ( object sender, ProgressChangedEventArgs e)
    {
      int lNumber       = e.ProgressPercentage;
      txt_Output.Text   = $"{lNumber.ToString("#,##0")} ({(lNumber/_Stopwatch.ElapsedMilliseconds).ToString("#,##0")} thousand per second)";
      progressBar.Value = lNumber;
      Refresh();
    }
    private void worker_RunWorkerCompleted ( object sender, RunWorkerCompletedEventArgs e)
    {
      // All threads have to complete before we have real completion
      progressBar.Value = progressBar.Maximum;
      Refresh();

      if ( e.Cancelled )
      {
        txt_Output.Text = "Operation canceled by user";
        _Workers        = null;
        return;
      }
      if ( e.Error != null)
      {
        txt_Output.Text = $"Error: {e.Error.Message}";
        _Workers        = null;
        return;
      }
      WorkChunk lPartResult = (WorkChunk)e.Result;
      Console.WriteLine( lPartResult.ToString() );
      _Results.Add( lPartResult );
      if ( _Results.Count == _NumberOfChuncks )
      {
        // All done, all threads are back
        _Results = (from X in _Results orderby X.StartRange select X).ToList(); // Make sure they are all in the right sequence
        List<int> lFullResults = new List<int>();
        foreach ( WorkChunk lChunck in _Results )
        {
          lFullResults.AddRange( lChunck.Results );
        }
        string lStrResult = string.Join( ", ", lFullResults );
        string lTimeMsg   = $"Calculate all primes up to {_MaxValueToTest.ToString("#,##0")} with \r\nMultiple Background Workers with {_NumberOfChuncks} workers: Total duration (seconds): {_Stopwatch.ElapsedMilliseconds/1000}";
        txt_Output.Text   = $"{lTimeMsg}\r\n{lStrResult}";
        _Workers = null;
      }
    }
    private bool IsPrimeNumber             ( long aValue )
    {
      // see https://en.wikipedia.org/wiki/Prime_number
      // Among the numbers 1 to 6, the numbers 2, 3, and 5 are the prime numbers, while 1, 4, and 6 are not prime.
      if ( aValue <= 1 ) return false;
      if ( aValue == 2 ) return true ;
      if ( aValue == 3 ) return true ;
      if ( aValue == 4 ) return false;
      if ( aValue == 5 ) return true ;
      if ( aValue == 6 ) return false;
      bool       lIsPrimeNumber = true;
      long       lMaxTest       = aValue / 2 + 1;
      for ( long lTest          = 2; lTest < lMaxTest && lIsPrimeNumber; lTest++ )
      {
        long lMod = aValue % lTest;
        lIsPrimeNumber = lMod != 0;
      }
      return lIsPrimeNumber;
    }
  }
  public class WorkChunk
  {
    public int       StartRange { get; set; }
    public int       EndRange   { get; set; }
    public List<int> Results    { get; set; }
    public string    Message    { get; set; }
    public DateTime  StartTime  { get; set; } = DateTime.MinValue;
    public DateTime  EndTime    { get; set; } = DateTime.MinValue;
    public override string ToString()
    {
      StringBuilder lResult = new StringBuilder();
      lResult.Append( $"WorkChunk: {StartRange} to {EndRange}" );
      if ( Results    == null                   ) lResult.Append( ", no results yet" ); else lResult.Append( $", {Results.Count} results" );
      if ( string.IsNullOrWhiteSpace( Message ) ) lResult.Append( ", no message"     ); else lResult.Append( $", {Message}" );
      if ( StartTime  == DateTime.MinValue      ) lResult.Append( ", no start time"  ); else lResult.Append( $", Start: {StartTime.ToString("HH:mm:ss.ffff")}" );
      if ( EndTime    == DateTime.MinValue      ) lResult.Append( ", no end time"    ); else lResult.Append( $", End: {  EndTime  .ToString("HH:mm:ss.ffff")}" );
      return lResult.ToString();
    }
  }
}

【问题讨论】:

  • 你能用Parallel.For吗?
  • 你的问题太宽泛了,坦率地说,尽管也太宽泛了,但其他一些 Stack Overflow 问题已经解决了。当前等效于BackgroundWorker 是使用Task.Run()(替换DoWork 事件)和Progress&lt;T&gt;(替换ProgressChanged 事件)。使用async/await,您还可以获得RunWorkerCompleted 功能。如果你愿意,你仍然可以使用BackgroundWorker。所有技术都默认使用线程池,因此它们将等效地在现有内核上进行调度。不过,如果您需要,基于 TPL 的解决方案可以对此进行更多控制。
  • 大家好,我要感谢 Blorgbead 和 Peter Duniho 的反馈 - 谢谢。现在我想问一下@#$%^&!您如何在没有广泛问题的情况下提出一般架构问题??? 所以,我重复我的问题: - 后台工作者是否适合用于此类 CPU 密集型任务? - 如果不是,什么技术更好? - 有没有像这样的 CPU 密集型任务的好例子? - 如果我使用后台工作人员,我会承担什么风险?问候格雷格哈里斯

标签: c# .net multithreading winforms asynchronous


【解决方案1】:

我将同时打开多达 100 万个业务对象

当然,但您不会在屏幕上同时显示那么多。

此外,这需要尽可能接近实时而不阻塞 UI。

首先要检查的是它是否足够快。给定合理硬件上的实际对象数量,您能否直接在 UI 线程上足够快地过滤?如果它足够快,那么它不需要更快。

我发现很多帖子说我应该用新的 async await c# 命令替换 BackgroundWorker。

async 不能替代BackgroundWorker。但是,Task.Run 是。我有一个描述how Task.Run is superior to BackgroundWorker 的博文系列。

当您有太多的进度通知返回 UI 线程时,性能会下降。

我更喜欢在 UI 层解决这个问题,使用类似 ObserverProgress 的东西。

后台工作者是否适合用于此类 CPU 密集型任务?

在跳转到多线程解决方案之前,请先考虑虚拟化。正如我在开头提到的,您不可能显示 那么多项目。那么为什么不运行过滤器直到你有足够的显示?如果用户滚动,则再运行一些过滤器。

什么技术更好?

我推荐:

  1. 先测试。如果过滤 UI 线程上的所有项目的速度足够快,那么您已经完成了。
  2. 实施虚拟化。即使过滤所有个项目太慢,只过滤一些个项目直到你有足够的显示可能已经足够快了。
  3. 如果以上都不够快,那么除了虚拟化之外,还可以使用Task.Run(和ObserverProgress)。

【讨论】:

    猜你喜欢
    • 2023-04-02
    • 2013-08-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-04-10
    • 1970-01-01
    相关资源
    最近更新 更多