并行处理混淆了

时间:2015-05-20 07:14:43

标签: c# parallel-processing

我是C#编程的新手。 我试图使用后台工作程序从服务器列表中获取更新的数量。每个服务器的结果都显示在报告进度方法的列表视图中。 我能够使用foreach循环成功获得结果,但在尝试使用并行foreach获得相同结果时,listview的所有列和行都会混淆。

例如: foreach循环的输出: 服务器名称状态更新可用

  1. server1登录服务器失败! 0
  2. server2更新可用3
  3. server3更新可用3
  4. server4最新日期0 等等..
  5. 并行foreach的输出:

    1. server1更新可用1
    2. server1登录服务器失败! 1
    3. server2登录服务器失败! 0
    4. server3登录服务器失败! 0
    5. server4登录服务器失败! 0
    6. server4更新可用3 等等..
    7. 我已尝试锁定部分代码,并尝试使用并发包,但无法解决问题。以下是parallelforeach代码。我做错了什么?任何建议都会有很大的帮助。

      Parallel.ForEach(namelist, /*new ParallelOptions { MaxDegreeOfParallelism = 4 }, */line =>
      //foreach (string line in namelist)
      {
            if (worker.CancellationPending)
            {
                   e.Cancel = true;
                   worker.ReportProgress(SysCount, obj);
            }
            else
            {
                   this.SystemName = line;//file.ReadLine();
                   Status.sVariables result = new Status.sVariables();
                   result = OneSystem(this.SystemName);
                   switch (result.BGWResult)
                   {
                          case -1:
                             this.StatusString = "Login to server failed!";
                             break;
                          //other status are assigned here;
                   }
                   SysCount++;
                   bag.Add(this);
            }
            Status returnobj;
            bag.TryTake(out returnobj);
            worker.ReportProgress(SysCount, returnobj);
            Thread.Sleep(200);
      });
      

      ReportProgress方法:

      private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
          {
              if (!backgroundWorker1.CancellationPending)
              {
                  Status result = (Status)e.UserState;
                  Complete_label.Visible = true;
                  if (listView1.InvokeRequired)
                      listView1.Invoke(new MethodInvoker(delegate
                      {
                          listView1.Items.Add("");
                          listView1.Items[result.SysCount - 1].SubItems.Add(result.SystemName);
                          listView1.Items[result.SysCount - 1].SubItems.Add(result.StatusString);
                          listView1.Items[result.SysCount - 1].SubItems.Add(result.AvailableUpdatesCount.ToString());
      
                      }));
                  else
                  {
                      try
                      {
                          listView1.Items.Add("");
                          listView1.Items[result.SysCount - 1].SubItems.Add(result.SystemName);
                          listView1.Items[result.SysCount - 1].SubItems.Add(result.StatusString);
                          listView1.Items[result.SysCount - 1].SubItems.Add(result.AvailableUpdatesCount.ToString());
                      }
                      catch (Exception ex)
                      {}
                      //other stuff
                 }
          }
      

2 个答案:

答案 0 :(得分:0)

您的结果全都混淆了,因为您使用并行操作写入全局状态,例如SystemNameStatusString,因此当您使用时,这些全局变量的内容最终会混淆尝试阅读和打印他们的价值观。

你可以引入一个lock,但这会完全打败Parallel.ForEach。因此,要么放弃使用Parallel.ForEach(在这种情况下似乎没有用处),要么需要收集数据并确保以线程安全的方式将其发送给报告者。

为了进一步解释,让我们检查一下代码:

this.SystemName = line; // <- the worker has now written to this, which is global to all workers
...
result = OneSystem(this.SystemName); // <- another worker may have overwritten SystemName at this point
...
                   this.StatusString = "Login to server failed!"; // <- again writing to shared variable
         ...
         bag.Add(this); // <- now trying to "thread protect" already corrupted data

因此,如果必须并行运行循环,则每个工作者必须仅更新其自己的隔离数据,然后将其推送到GUI编组报告方法。

答案 1 :(得分:0)

真正的问题是ListView更新代码使用错误的索引来更新项目。它假定Status.SysCount属性包含正确的索引。如果执行按顺序执行,则可能是这样,但如果执行并行运行则会失败 - 不同的线程可以以不同的速度完成并报告无序的进度。

只需使用ListViewItemCollection.Add

返回的ListViewItem对象即可解决实际问题
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    if (!backgroundWorker1.CancellationPending)
    {
        Status result = (Status)e.UserState;
        Complete_label.Visible = true;
        var newItem=listView1.Items.Add("");
        newItem.SubItems.Add(result.SystemName);
        newItem.SubItems.Add(result.StatusString);
        newItem.SubItems.Add(result.AvailableUpdatesCount.ToString());
        //other stuff
    }
}    

代码有更严重的问题 - State类尝试并行处理数据,将数据存储在自己的属性中,然后发送自己报告。显然,显示的数据将始终在变化。

更好的选择是在循环中创建一个新的State实例,或者更好的是,仅为报告创建一个类:

class StatusProgress 
{
    public string SystemName{get;set;}
    public string StatusString{get;set;}
    public int AvailableUpdatesCount {get;set;}
}

....
int sysCount=0;
Parallel.ForEach(namelist, line =>
{
    var progress=new StatusProgress();
    progress.SystemName = line;//file.ReadLine();

    Status.sVariables result = new Status.sVariables();
    result = OneSystem(line);
    switch (result.BGWResult)
    {
        case -1:
            progress.StatusString = "Login to server failed!";
            break;
                //other status are assigned here;
    }
    var count=Interlocked.Increment(ref sysCount);
  }
  worker.ReportProgress(count, progress);
});

请注意,代替SysCount++使用Interlocked.Increment以原子方式增加值获取递增值的副本。如果我没有这样做,多个线程可以在我有机会报告进度之前修改SysCount

进度报告代码将更改为使用StateProgress

        StatusProgress result = (StatusProgress)e.UserState;

最后,BackgroundWorker已经过时,因为任务并行库以更轻量级的方式提供了BGW所做的一切以及更多功能。例如,您可以{@ 3}}使用CancellationToken,并使用cancel the parallel loop类以类型安全的方式报告进度。

.NET中的大多数异步方法都识别CancellationToken和Progress,这意味着您可以轻松地报告进度并取消异步任务{。{3}}。

代码可以像这样重写:

在UI表单上:

private void ReportServerProgress(StatusProgress result)
{
    Complete_label.Visible = true;
    var newItem=listView1.Items.Add("");
    newItem.SubItems.Add(result.SystemName);
    newItem.SubItems.Add(result.StatusString);
    newItem.SubItems.Add(result.AvailableUpdatesCount.ToString());
    //other stuff
}   

CancellationTokenSource _cts;
Progress<StatusProgress> _progress;

public void StartProcessiong()
{
    _cts=new CancellationTokenSource();
   _progress=new Progress<StatusProgress(progress=>ReportServerProgress(progress);
   StartProcessing(/*input*/,_cts.Token,_progress);
}

public void CancelLoop()
{
   if (_cts!=null)
      _cts.Cancel();
}

处理代码可以在同一表格或任何其他类别上。实际上,将UI与处理代码分开会更好,尤其是当您进行非平凡的处理时,例如调用每个服务器以确定其状态

public void StartProcessing(/*input parameters*/,
                    CancellationTokenSource token,
                    IProgress<StatusProgress> progress)
{
    .....
    var po=new ParallelOptions();
    po.CancellationToken=token;
    Parallel.ForEach(namelist, po,line =>
    {
       var status=new StatusProgress();
       status.SystemName = line;//file.ReadLine();
       Status.sVariables result = new Status.sVariables();
       result = OneSystem(line);
       switch (result.BGWResult)
       {
           case -1:
               progress.StatusString = "Login to server failed!";
               break;
               //other status are assigned here;
       }
       progress.Report(status);
    }
}

许多异步.NET方法接受取消令牌,因此您可以将其传递给Web Service调用,并确保取消循环和任何未完成的长调用。