使用队列在两个BackgroundWorkers之间传递数据

时间:2019-05-17 09:19:37

标签: c# .net multithreading queue backgroundworker

我正在逐字节读取和解码二进制文件。为此,我使用了两个BackgroundWorker:一个用于读取文件,为文件的每个“行”生成一个List<byte>大小可变的文件,另一个用于处理“行”。

由于我希望它们并行运行,所以我不知道哪个会比另一个更快,因此我使用Queue在两个BackgroundWorker之间传递数据。

这就是问题:List<byte>随时都不应包含任何0值。我检查了之前将它们添加到队列中。尽管如此,在Queue的另一端,某些列表仍包含0值。但是,我每次调用List<byte>时都会创建一个新的Dequeue(),因为显然,如果我不这样做,则数据会在处理完成之前被修改。

我尝试手动创建一个新的List<byte>对象,然后然后将其分配为Dequeue()的结果,而没有进行任何改进。这是我第一次使用Queue,并且由于我的代码是多线程的,因此逐步调试几乎是不可能的。

Queue<List<byte>> q = new Queue<List<byte>>(); // My FIFO queue

// Reading thread
private void BackgroudWorkerRead_DoWork(object sender, DoWorkEventArgs e)
{
      // ... read the file
      List<byte> line_list = new List<byte>();
      // ... filling line_list with data
      // in this part I check that no byte added to line_list has the value 0, or else I display an errror message and end the process
      q.Enqueue(line_list);
      if (!backgroundWorkerNewLine.IsBusy) backgroundWorkerNewLine.RunWorkerAsync(); // if the other BackgroundWorker isn't processing data, now it needs to since we just added some to the queue
}

// Processing thread
private void backgroundWorkerNewLine_DoWork(object sender, DoWorkEventArgs e)
{
    while (q.Count > 0) // While there is data to process
    {
          string line_str = DecodeBytes(new List<byte>(q.Dequeue())); // Decoding
          string[] elements = line_str.Split(separator, StringSplitOptions.None); // Separating values

          Form1.ActiveForm.Invoke(new MethodInvoker(() => AddRow(elements))); // Add the line to a DataTable from the main thread
    }
}

public string DecodeBytes(List<byte> line)
{
 /// ... read each byte and return a string of the whole decoded line
}

public void AddRow(string[] el)
{
    MyDataTable.Rows.Add(el);
}

q.Dequeue()返回的列表似乎没有返回q.Enqueue()添加的相同数据

2 个答案:

答案 0 :(得分:0)

您应该使用Microsoft的Reactive Framework(又名Rx)-NuGet System.Reactive.Windows.Forms(假设您正在编写WinForms应用程序)并添加using System.Reactive.Linq;

Rx让您使用熟悉的LINQ语法进行并行操作。

您尚未向我们展示如何将文件分解为List<byte>的列表,因此,我假设您使用的方法类似于IObservable<List<byte>> DeconstructFile(FileInfo fileInfo)

现在您可以执行以下操作:

IObservable<string[]> query =
    from bytes in DeconstructFile(new FileInfo("myFile.bin"))
    from line_str in Observable.Start(() => DecodeBytes(bytes))
    select line_str.Split(separator, StringSplitOptions.None);

IDisposable subscription =
    query
        .ObserveOn(Form1.ActiveForm)
        .Subscribe(el => MyDataTable.Rows.Add(el));

就是这样。它并行运行,Observable.Start根据需要启动新线程,并自动将结果传递到每个步骤。 .ObserveOn(Form1.ActiveForm)自动将.Subscribe编组到UI线程。

如果您需要在代码完成之前停止代码,只需调用subscription.Dispose()。很简单。

答案 1 :(得分:0)

创建多线程应用程序时,必须非常小心,以防止不同的线程同时访问共享资源。如果您不阻止它,那么坏事就会发生。您正在丢失更新,数据结构已损坏,所有这些情况都无法预料地和不一致地发生。为避免这些问题,您应该同步所有对来自不同线程的共享资源的访问。这可以通过使用lock语句来实现。因此建议是:在阅读更新共享资源时始终锁定。在您的情况下,共享资源为Queue。您应该这样锁定:

// Reading thread
lock (q)
{
    q.Enqueue(line_list);
}

// Processing thread
while (true)
{
    List<byte> list;
    lock (q)
    {
        if (q.Count == 0) break;
        list = new List<byte>(q.Dequeue());
    }
    string line_str = DecodeBytes(list); // Decoding
    // ...

锁定的缺点是它会引起争用,因此锁定不应超过绝对必要。尤其要避免在持有锁的同时进行大量计算。

除此之外,您要实现的模式是生产者-消费者模式,.NET提供了专门的类来简化此模式。它是BlockingCollection类,它为您处理所有这些混乱的线程同步。它可以帮助您减少必须编写的代码,但需要花一点时间才能完成。基本上,您需要学习方法AddCompleteAddingGetConsumingEnumerable,然后就可以开始了。