如何在单独的CPU内核上并行执行任务

我有一个WinForms C#程序,我将在用户的机器上一次打开(在内存中)一百万个业务对象。

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

这是用户本地计算机上的IO密集型操作,具有零IO。 这听起来像我应该引发单独的任务,以在我的CPU上的单独核心上的独立线程上运行。 全部完成后,将结果合并到一个列表中并将结果显示给用户。

我是个老派,听起来像是一个BackgroundWorker的工作,但是我读到BackgroundWorker在.NET 4.5中被明确标记为过时(悲伤的脸)。 请参阅:异步/等待与BackgroundWorker

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

但是,这里有一些很好的例子,我发现沿着“异步等待并不保证单独的线程”的意见,并且所有的例子都显示了等待任务(不是CPU密集型任务)上的IO /网络密集型任务。

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

BackgroundWorker调查的结果是:

  • 如果机器上每个物理内核有一个任务,我的虚拟机有3个内核,则任务运行速度最快,3个后台工作任务可以获得最佳性能提升。
  • 当您有太多的后台工作任务时,性能就会消失。
  • 当你有太多的进度通知返回到UI线程时,性能就会消失。
  • 问题:

    后台工作人员是否使用这样的CPU密集型任务是正确的技术? 如果不是,哪种技术更好? 在这样的CPU密集型任务中有没有好的例子? 如果我使用后台工作者,我会面临什么风险?

    基于单个Background Worker的代码示例

    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 rnSingle Background Worker with only 1 worker: Total duration (seconds): {_Stopwatch.ElapsedMilliseconds/1000}";
          txt_Output.Text   = $"{lTimeMsg}rn{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 rnMultiple Background Workers with {_NumberOfChuncks} workers: Total duration (seconds): {_Stopwatch.ElapsedMilliseconds/1000}";
            txt_Output.Text   = $"{lTimeMsg}rn{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();
        }
      }
    }
    

    我将一次打开多达100万个业务对象

    当然,但你不会一次在屏幕上显示那么多。

    此外,这需要尽可能接近实时而不会阻塞用户界面。

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

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

    async不是BackgroundWorker的替代品。 但是, Task.Run是。 我有一篇博客文章系列,描述了Task.Run如何优于BackgroundWorker

    当你有太多的进度通知返回到UI线程时,性能就会消失。

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

    后台工作人员是否使用这样的CPU密集型任务是正确的技术?

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

    什么技术更好?

    我建议:

  • 先测试一下。 如果它足够快来过滤UI线程中的所有项目,那么您已经完成了。
  • 实施虚拟化。 即使过滤所有项目速度太慢,只有一些项目过滤,直到您有足够的显示可能足够快。
  • 如果以上都不够快,那么除了虚拟化之外,还应该使用Task.Run (带有ObserverProgress )。
  • 链接地址: http://www.djcxy.com/p/50169.html

    上一篇: How to parallel execute tasks on separate CPU cores

    下一篇: Limit number of Threads in Task Parallel Library