Loading

并发编程-9.在 .NET 中使用并发集合

BlockingCollection

BlockingCollection<T> 是最有用的并发集合之一。 正如我们在第 7 章中看到的,BlockingCollection<T> 被创建为 .NET 生产者/消费者模式的实现。 在创建不同类型的示例项目之前,让我们回顾一下该集合的一些细节。

BlockingCollection 细节

对于使用并行代码实现的开发人员来说,BlockingCollection<T> 的主要吸引力之一是它可以替换为 List<T>,而无需太多额外的修改。您可以对两者使用 Add() 方法。 与 BlockingCollection<T> 的区别在于,如果正在进行另一个读取或写入操作,则调用 Add() 添加项目将阻塞当前线程。 如果要指定操作的超时时间,可以使用 TryAdd()TryAdd() 方法可以选择支持超时和取消令牌。

使用 Take()BlockingCollection<T> 中删除项目有一个等效的 TryTake(),它允许定时操作和取消。 Take()TryTake() 方法将获取并删除添加到集合中的第一个剩余项目。 这是因为 BlockingCollection<T> 中的默认基础集合类型是 ConcurrentQueue<T>。 或者,您可以指定集合使用 ConcurrentStack<T>ConcurrentBag<T> 或任何实现 IProducerConsumerCollection<T> 接口的集合。 下面是 BlockingCollection<T> 被初始化为使用 ConcurrentStack 的示例,其容量限制为 100 个项目:

var itemCollection = new BlockingCollection<string>(new ConcurrentStack<string>(), 100);

如果您的应用程序需要迭代 BlockingCollection<T> 中的项目,则可以在 forforeach 循环中使用 GetConsumingEnumerable() 方法。 但是,请记住,对集合的这次迭代也会删除项目,如果继续枚举直到集合为空,它将完成集合。 这是GetConsumingEnumerable() 方法名称的使用部分。

如果您需要使用多个相同类型的 BlockingCollection<T> 类,则可以通过将它们添加到数组中来将它们作为一个整体添加或从中获取。 BlockingCollection<T> 数组使 TryAddToAny()TryTakeFromAny() 方法可用。 如果数组中的任何集合处于正确状态以接受或向调用代码提供对象,这些方法将成功。 Microsoft Docs 有一个如何在管道中使用 BlockingCollection<T> 数组的示例:https://docs.microsoft.com/dotnet/standard/collections/thread-safe/how-to-use-arrays-of-blockingcollections。

将 BlockingCollection 与 Parallel.ForEach 和 PLINQ 结合使用

我们已经在第 7 章中介绍了一个实现生产者/消费者模式的示例,所以让我们在本节中尝试一些不同的东西。 我们将创建一个 WPF 应用程序,该应用程序从 1.5 MB 文本文件加载书籍内容并搜索以特定字母开头的单词:

此示例使用从最初基于 .NET Framework 4.0 构建的 Microsoft 扩展示例创建的 .NET Standard NuGet 包。 该扩展名为 ParallelExtensionsExtras,原始来源可在 GitHub 上找到:https://github.com/dotnet/samples/tree/main/csharp/parallel/ParallelExtensionsExtras。 我们将使用包中的扩展方法,使 Parallel.ForEach 操作和 PLINQ 查询通过并发集合更高效地运行。 要了解有关扩展的更多信息,您可以查看 .NET 并行编程博客上的这篇文章:https://devblogs.microsoft.com/pfxteam/parallelextensionsextras-tour-4-blockingcollectionextensions/。

  1. 首先在 Visual Studio 中创建一个新的 WPF 应用程序。 将项目命名为 ParallelExtras.BlockingCollection

  2. 在 NuGet 包管理器页面上,搜索最新稳定版本的 ParallelExtensionsExtras.NetFxStandard 包并将其添加到您的项目中:

图 9.1 – ParallelExtensionsExtras.NetFxStandard NuGet 包

image

  1. 我们将阅读詹姆斯·乔伊斯所著的《尤利西斯》一书中的文本。 本书在美国和世界上大多数国家属于公共领域。 可以从古腾堡项目下载 UTF-8 纯文本格式:https://www.gutenberg.org/ebooks/4300。下载副本,将文件命名为 ulysses.txt,并将其与其他项目文件放在主文件夹中。
  2. 在 Visual Studio 中,右键单击 ulysses.txt 并选择“属性”。 在“属性”窗口中,将“复制到输出目录”属性更新为“如果较新则复制”。
  3. 打开 MainWindow.xaml 并添加 Grid.RowDefinitionsGrid。 Grid控件的列定义如下:
<Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
    <ColumnDefinition/>
    <ColumnDefinition/>
</Grid.ColumnDefinitions>
  1. Grid.ColumnDefinitions 元素后面的 Grid 定义内添加 ComboBoxButton。 这些控件将位于网格的第一行:
<ComboBox x:Name="LettersComboBox" Grid.Row="0" Grid.Column="0" Margin="4">
    <ComboBoxItem Content="A"/>
    <ComboBoxItem Content="D"/>
    <ComboBoxItem Content="F"/>
    <ComboBoxItem Content="G"/>
    <ComboBoxItem Content="M"/>
    <ComboBoxItem Content="O"/>
    <ComboBoxItem Content="A"/>
    <ComboBoxItem Content="T"/>
    <ComboBoxItem Content="W"/>
</ComboBox>
<Button Grid.Row="0" Grid.Column="1" Margin="4" Content="Load Words" Click="Button_Click"/>

ComboBox 将包含九个不同的字母可供选择。 您可以根据需要添加任意数量的这些内容。 Button 包含一个 Click 事件处理程序,我们将很快将其添加到 MainWindow.xaml.cs 中。

  1. 最后,将名为WordsListViewListView添加到Grid的第二行。 它将跨越两列:
<ListView x:Name="WordsListView" Margin="4" Grid.Row="1" Grid.ColumnSpan="2"/>
  1. 现在,打开 MainWIndow.xaml.cs。 我们要做的第一件事是创建一个名为 LoadBookLinesFromFile() 的方法,该方法将 ulysses.txt 中的每一行文本读取到 BlockingCollection<string> 中。 只有一个线程从文件中读取,因此最好使用 Add() 方法而不是 TryAdd()
private async Task<BlockingCollection<string>> LoadBookLinesFromFile()
{
    var lines = new BlockingCollection<string>();
    using var reader = File.OpenText(Path.Combine( 
        Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),"ulysses.txt"));
    
    string line;
    while ((line = await reader.ReadLineAsync()) != null)
    {
    	lines.Add(line);
    }
    lines.CompleteAdding();
    return lines;
}

请记住,在方法结束之前调用lines.CompleteAdding()非常重要。 否则,该集合的后续查询将挂起并继续等待更多项目添加到流中。

  1. 现在,创建一个名为 GetWords() 的方法,该方法从文本文件中获取行并使用正则表达式将每一行解析为单独的单词。 这些单词将全部添加到新的 BlockingCollection<string> 中。 在此方法中,我们使用 Parallel.ForEach 循环同时解析多行。 ParallelExtentionsExtras.NetFxStandard 包提供了 GetConsumingPartitioner() 扩展方法,该方法告诉 Parallel.ForEach 循环 BlockingCollection 将执行自己的阻塞,因此循环不需要执行任何操作。 这使得整个过程更加高效:
private BlockingCollection<string> GetWords(BlockingCollection<string> lines)
{
    var words = new BlockingCollection<string>();
    Parallel.ForEach(lines.GetConsumingPartitioner(),
        (line) =>
        {
            var matches = Regex.Matches(line, @"\b[\w']*\b");
            foreach (var m in matches.Cast<Match>())
            {
                if (!string.IsNullOrEmpty(m.Value))
                {
                    words.TryAdd(TrimSuffix(m.Value,'\''));
                }
            }
    });
    words.CompleteAdding();
    return words;
}

private string TrimSuffix(string word, char charToTrim)
{
    int charLocation = word.IndexOf(charToTrim);
    if (charLocation != -1)
    {
    	word = word[..charLocation];
    }
    return word;
}

TrimSuffix() 方法将从单词末尾删除特定字符; 在本例中,我们传递要删除的撇号字符。

  1. 接下来,创建一个名为 GetWordsByLetter() 的方法来调用我们刚刚创建的其他方法。 获取包含书中所有单词的 BlockingCollection<string> 后,此方法将使用 PLINQ 和 GetConsumingPartitioner() 查找以所选字母的大写或小写版本开头的所有单词:
private async Task<List<string>> GetWordsByLetter(char letter)
{
    BlockingCollection<string> lines = await LoadBookLinesFromFile();
    BlockingCollection<string> words =  GetWords(lines);
    // 275,506 words in total
    return words.GetConsumingPartitioner()
                .AsParallel()
                .Where(w => w.StartsWith(letter) || w.StartsWith(char.ToLower(letter)))
                .ToList();
}
  1. 最后,我们将添加 Button_Click 事件来启动书籍文本的加载、解析和查询。 不要忘记将事件处理程序标记为异步:
private async void Button_Click(object sender, RoutedEventArgs e)
{
    if (LettersComboBox.SelectedIndex < 0)
    {
        MessageBox.Show("Please select a letter.");
        return;
    }
    WordsListView.ItemsSource = await
    	GetWordsByLetter( char.Parse(GetComboBoxValue(LettersComboBox.SelectedValue)));
}

private string GetComboBoxValue(object item)
{
    var comboxItem = item as ComboBoxItem;
    return comboxItem.Content.ToString();
}

GetComboBoxValue() 辅助方法将从 LettersComboBox.SelectedValue 中获取对象,并查找其中包含所选字母的字符串。

  1. MainWindow.xaml.cs 中需要以下 using 声明来编译和运行项目:
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
  1. 现在,运行项目,选择一个字母,然后单击“加载单词”:

图 9.2 – 显示 ulysses.txt 中以 T 开头的单词

image

考虑到这本书总共包含超过 275,000 字,整个过程运行得非常快。 尝试向 PLINQ 查询添加一些排序,看看性能受到怎样的影响。

ConcurrentBag

ConcurrentBag<T> 是一个无序的对象集合,可以安全地同时添加、查看或删除。 请记住,与所有并发集合一样,ConcurrentBag<T> 公开的方法是线程安全的,但不保证任何扩展方法都是安全的。 在利用它们时始终实现您自己的同步。 要查看安全方法列表,您可以查看此 Microsoft 文档页面:https://docs.microsoft.com/dotnet/api/system.collections.concurrent.concurrentbag-1#methods。

我们将创建一个示例应用程序来模拟使用对象池。 如果您有一些处理利用内存密集型有状态对象,则此方案可能会很有用。您希望最大限度地减少创建的对象数量,但在上一次迭代完成使用它并将其返回到池中之前无法重用对象。

在我们的示例中,我们将使用一个模拟的 PDF 处理类,该类被假定为内存密集型。实际上,文档处理库可能非常繁重,并且它们通常依赖于每个实例中的文档状态。 控制台应用程序将并行迭代 15 次以创建这些假 PDF 对象并向每个对象附加一些文本。 每次循环时,我们都会输出文本内容和池中 PDF 处理器的当前数量。 如果当前计数仍然很低,则应用程序正在按预期工作:

  1. 首先在 Visual Studio 中创建一个名为 ConcurrentBag.PdfProcessor 的新 .NET 控制台应用程序。
  2. 添加一个新类来表示模拟的 PDF 数据。 将类命名为 ImposterPdfData
public class ImposterPdfData
{
    private string _plainText;
    private byte[] _data;
    
    public ImposterPdfData(string plainText)
    {
        _plainText = plainText;
        _data = System.Text.Encoding.ASCII.GetBytes(plainText);
    }
    
    public string PlainText => _plainText;
    public byte[] PdfData => _data;
}

我们存储纯文本和 ASCII 编码版本的文本(我们将假装为 PDF 格式)。 这可以避免在我们的示例应用程序中实现任何第三方库。如果您有任何熟悉的 PDF 库,欢迎您调整此示例以使用它们。
3. 接下来,添加一个名为 PdfParser 的新类。 此类将是从 ConcurrentBag<PdfParser> 获取并返回到ConcurrentBag<PdfParser> 的类。 我们将在接下来的步骤中为该集合创建主机:

public class PdfParser
{
    private ImposterPdfData? _pdf;
    public void SetPdf(ImposterPdfData pdf) =>  _pdf = pdf;
    public ImposterPdfData? GetPdf() => _pdf;
    
    public string GetPdfAsString()
    {
        if (_pdf != null)
        	return _pdf.PlainText;
        else
        	return "";
    }
    
    public byte[] GetPdfBytes()
    {
        if (_pdf != null)
        	return _pdf.PdfData;
        else
        	return new byte[0];
    }
}

该有状态类保存 ImposterPdfData 对象的实例,并可以以字符串或 ASCII 编码的字节数组形式返回数据。
4. 向 PdfParser 添加一个名为 AppendString 的方法。 此方法将在新行上向 ImposterPdfData 添加一些附加文本:

public void AppendString(string data)
{
    string newData;
    if (_pdf == null)
    {
    	newData = data;
    }
    else
    {
    	newData = _pdf.PlainText + Environment.NewLine + data;
    }
    
    _pdf = new ImposterPdfData(newData);
}
  1. 现在,添加一个名为 PdfWorkerPool 的类:
public class PdfWorkerPool
{
    private ConcurrentBag<PdfParser> _workerPool = new();
    
    public PdfWorkerPool()
    {
        // Add initial worker
        _workerPool.Add(new PdfParser());
    }
    public PdfParser Get() => _workerPool.TryTake(out var parser) ? parser : new PdfParser();
    
    public void Return(PdfParser parser) => _workerPool.Add(parser);
    
    public int WorkerCount => _workerPool.Count();
}

请务必添加 using System.Collections.Concurrent; 对 PdfWorkerPool.cs 的声明。 该池存储名为 _workerPoolConcurrentBag<PdfParser>。 当 PdfWorkerPool 初始化时,它会向 _workerPool 添加一个新实例。 Get 方法将通过 TryTake 从池中返回一个现有实例(如果存在)。 如果池为空,则创建一个新实例并将其返回给调用者。 当使用者完成时,Return 方法将 PdfParser 添加回池中。 我们将使用 WorkerCount 属性随时跟踪池中的对象数量。

6.最后将Program.cs中的内容替换为以下代码:

using ConcurrentBag.PdfProcessor;

Console.WriteLine("Hello, ConcurrentBag!");
var pool = new PdfWorkerPool();

Parallel.For(0, 15, async (i) =>
{
    var parser = pool.Get();
    var data = new ImposterPdfData($"Data index: {i}");
    try
    {
        parser.SetPdf(data);
        parser.AppendString(DateTime.UtcNow .ToShortDateString());
        Console.WriteLine($" {parser.GetPdfAsString()}");
        Console.WriteLine($"Parser count: {pool.WorkerCount}");
        await Task.Delay(100);
    }
    finally
    {
        pool.Return(parser);
        await Task.Delay(250);
    }
});
Console.WriteLine("Press the Enter key to exit.");
Console.ReadLine();

创建新的 PdfWorkerPool 后,我们使用 Parallel.For 循环迭代 15 次。 每次通过循环,我们都会获取 PdfParser,设置文本,附加 DateTime.UtcNow,并将内容以及池中解析器的当前计数写入控制台。
7. 运行应用程序并检查输出:

图 9.3 – 运行 PdfProcessor 控制台应用程序

image

就我而言,解析器数量最多达到七。 如果您调整 Task.Delay 间隔或完全删除它们,您可能会看到计数永远不会超过 1。 这种池可以配置得非常高效。
此应用程序是一个示例,其中我们不关心返回集合的哪个实例,因此 ConcurrentBag<T> 是一个完美的选择。 在下一节中,我们将使用 ConcurrentDictionary<TKey, TValue> 创建一个药物查找示例。

ConcurrentDictionary

在本节中,我们将创建一个 WinForms 应用程序以同时从两个文件加载美国食品和药物管理局 (FDA) 药物数据。 加载到 ConcurrentDictionary 后,我们可以使用国家药品代码 (NDC) 值执行快速查找来获取名称。 FDA 药物数据可以从 NDC 目录中以多种格式免费下载:https://www.fda.gov/drugs/drug-approvals-and-databases/national-drug-codedirectory。 我们将使用制表符分隔的文本文件。 我已下载了product.txt 文件,并将大约一半的记录移至product2.txt 文件中,复制了第二个文件中的标题行。

  1. 首先在 Visual Studio 中创建一个面向 .NET 的新 WinForms 项目 6. 将项目命名为 FdaNdcDrugLookup。
  2. 打开 Form1.cs 的 WinForm 设计器。 布置两个 TextBox 控件、两个 Button 控件和 Label:

图 9.4 – Form1.cs 的布局

image

加载数据按钮将设置以下属性:名称 - btnLoad 和文本 - loadData。 NDC 代码文本字段将命名为 txtNdc。 “查找药物”按钮将设置以下属性:名称 - btnLookup、文本 - 查找药物和启用 - False。 最后,药物名称文本字段将设置以下属性:Name –txtDrugName 和 ReadOnly – True。
3. 接下来,通过右键单击“解决方案资源管理器”中的项目并选择“添加”|“添加”,将product.txt 和product2.txt 文件添加到您的项目中。 现有项目。
4. 在“属性”面板中,将我们刚刚添加的两个文本文件的“复制到输出目录”更改为“如果较新则复制”。

  1. 向名为 Drug 的项目添加一个新类,并添加以下实现:
public class Drug
{
    public string? Id { get; set; }
    public string? Ndc { get; set; }
    public string? TypeName { get; set; }
    public string? ProprietaryName { get; set; }
    public string? NonProprietaryName { get; set; }
    public string? DosageForm { get; set; }
    public string? Route { get; set; }
    public string? SubstanceName { get; set; }
}

这将包含从 NDC 药物文件加载的每条记录的数据。
6. 接下来,向名为 DrugService 的项目添加一个类,并开始以下实现。 首先,我们只有 private ConcurrentDictionary<string,
Drug>. 我们将在下一步中添加一个加载数据的方法:

using System.Collections.Concurrent;
using System.Data;
using System.Reflection;
namespace FdaNdcDrugLookup
{
    public class DrugService
    {
    	private ConcurrentDictionary<string, Drug> _drugData = new();
    }
}
  1. 接下来,向 DrugService 添加一个名为 LoadData 的公共方法:
public void LoadData(string fileName)
{
    using DataTable dt = new();
    using StreamReader sr = new(Path.Combine(
    Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), fileName));
    
    var del = new char[] { '\t' };
    string[] colheaders = sr.ReadLine().Split(del);
    foreach (string header in colheaders)
    {
    	dt.Columns.Add(header); // add headers
    }
    
    while (sr.Peek() > 0)
    {
        DataRow dr = dt.NewRow(); // add rows
        dr.ItemArray = sr.ReadLine().Split(del);
        dt.Rows.Add(dr);
    }
    foreach (DataRow row in dt.Rows)
    {
        Drug drug = new(); // map to Drug object
        foreach (DataColumn column in dt.Columns)
        {
            switch (column.ColumnName)
            {
            case "PRODUCTID":
            	drug.Id = row[column].ToString();
            break;
            case "PRODUCTNDC":
            	drug.Ndc = row[column].ToString();
            break;
            ...
            // REMAINING CASE STATEMENTS IN GITHUB
        }
    }
    	_drugData.TryAdd(drug.Ndc, drug);
    }
}

在此方法中,我们将数据从提供的 fileName 加载到 StreamReader,将列标题添加到 DataTable,从文件填充其行,然后迭代 DataTable 的行和列以创建 Drug 对象。 每个 Drug 对象都通过调用 TryAdd 来添加到 ConcurrentDictionary,并使用 Ndc 属性作为键。
8. 现在,将 GetDrugByNdc 方法添加到 DrugService 以完成该类。 如果找到,此方法将返回所提供的 ndcCodeDrug

public Drug GetDrugByNdc(string ndcCode)
{
    bool result = _drugData.TryGetValue(ndcCode, out var drug);
    if (result && drug != null)
    	return drug;
    else
    	return new Drug();
}
  1. 打开 Form1.cs 的代码并为 DrugService 添加私有变量:
private DrugService _drugService = new();
  1. 打开 Form1.cs 的设计器,然后双击“加载数据”按钮以创建 btnLoad_Click 事件处理程序。 添加以下实现。 请注意,我们创建了异步事件处理程序以允许我们使用await 关键字:
private async void btnLoad_Click(object sender,EventArgs e)
{
    var t1 = Task.Run(() => _drugService.LoadData("product.txt"));
    var t2 = Task.Run(() => _drugService.LoadData("product2.txt"));
    
    await Task.WhenAll(t1, t2);
    btnLookup.Enabled = true;
    btnLoad.Enabled = false;
}

为了加载这两个文本文件,我们创建两个并行运行的任务,然后使用 Task.WhenAll 等待它们。 然后,我们可以安全地启用 btnLookup 按钮并禁用 btnLoad 按钮以防止第二次加载。
11. 接下来,切换回 Form1.cs 的设计器视图,然后双击 Lookup Drug 按钮。这将创建 btnLookup_Click 事件处理程序。 将以下实现添加到该处理程序中,以根据 UI 中输入的 NDC 代码查找药物名称:

private void btnLookup_Click(object sender,EventArgs e)
{
    if (!string.IsNullOrWhiteSpace(txtNdc.Text))
    {
        var drug = _drugService.GetDrugByNdc (txtNdc.Text);
        txtDrugName.Text = drug.ProprietaryName;
    }
}
  1. 现在,运行应用程序并单击“加载数据”按钮。 加载过程完成并启用“查找药物”按钮后,输入 70518-1120 NDC 代码。 点击查找
    药品:

图 9.5 – 通过 NDC 代码查找药物泼尼松

image

  1. 尝试其他一些 NDC 代码并查看每个记录的加载速度。 以下是从每个文件中获取的一些随机 NDC 代码。 如果全部成功,您就知道两个文件已成功并行加载:0002-0800、0002-4112、43063-825 和 51662-1544。

    就是这样! 您现在拥有自己的快速且简单的药物查找应用程序。 尝试自行将药物名称 TextBox 替换为 DataGrid 以显示完整的药物记录。

ConcurrentQueue

在本节中,我们将创建一个示例项目,它是实际场景的简化版本。 我们将使用 ConcurrentQueue<T> 创建一个订单排队系统。 该应用程序将是一个控制台应用程序,它并行地将两个客户的订单排队。 我们将为每个客户创建五个订单,并且为了混合队列的顺序,每个客户排队过程将在对 Enqueue 的调用之间使用不同的 Task.Delay。 最终输出应显示第一个客户和第二个客户的出队订单组合。 请记住,ConcurrentQueue<T> 采用先进先出 (FIFO) 逻辑:

  1. 首先打开 Visual Studio 并创建一个名为 ConcurrentOrderQueue 的 .NET 控制台应用程序。
  2. 在项目中添加一个名为Order的新类:
public class Order
{
    public int Id { get; set; }
    public string? ItemName { get; set; }
    public int ItemQty { get; set; }
    public int CustomerId { get; set; }
    public decimal OrderTotal { get; set; }
}
  1. 现在,创建一个名为 OrderService 的新类,其中包含私有ConcurrentQueue<Order> 名为 _orderQueue。 我们将在此类中为两个客户将订单入队和出队:
using System.Collections.Concurrent;
namespace ConcurrentOrderQueue
{
    public class OrderService
    {
    	private ConcurrentQueue<Order> _orderQueue = new();
    }
}
  1. 让我们从 DequeueOrders 的实现开始。 在此方法中,我们将使用 while 循环调用 TryDequeue 直到集合为空,然后将每个订单添加到 List<Order> 以返回给调用者:
public List<Order> DequeueOrders()
{
    List<Order> orders = new();
    while (_orderQueue.TryDequeue(out var order))
    {
    	orders.Add(order);
    }
    return orders;
}
  1. 现在,我们将创建公共和私有 EnqueueOrders 方法。 公共无参数方法将调用私有方法两次,每个 customerId 一次。 这两个调用将并行进行,然后调用 Task.WhenAll 来等待它们:
public async Task EnqueueOrders()
{
    var t1 = EnqueueOrders(1);
    var t2 = EnqueueOrders(2);
    await Task.WhenAll(t1, t2);
}

private async Task EnqueueOrders(int customerId)
{
    for (int i = 1; i < 6; i++)
    {
        var order = new Order
        {
            Id = i * customerId,
            CustomerId = customerId,
            ItemName = "Widget for customer " +
            customerId,
            ItemQty = 20 - (i * customerId)
        };
        order.OrderTotal = order.ItemQty * 5;
        _orderQueue.Enqueue(order);
        await Task.Delay(100 * customerId);
    }
}

私有 EnqueueOrders 方法迭代五次来为给定的 customerId 创建订单并将其放入队列。 这也用于改变 ItemNameItemQtyTask.Delay 的持续时间。
6. 最后,打开 Program.cs 并添加以下代码以将订单入队和出队,并将结果列表输出到控制台:

using ConcurrentOrderQueue;
Console.WriteLine("Hello, World!");
var service = new OrderService();
await service.EnqueueOrders();
var orders = service.DequeueOrders();

foreach(var order in orders)
{
	Console.WriteLine(order.ItemName);
}
  1. 运行程序并查看输出中的订单列表。 你的怎么样?

图 9.6 – 查看订单队列的输出

image

尝试在 EnqueueOrders 方法中改变延迟因子或更改一个或两个客户的 customerId,以查看输出顺序如何变化。

ConcurrentStack

在本节中,我们将尝试使用 BlockingCollection<T>ConcurrentStack<T>。 在本章的第一个示例中,我们使用 BlockingCollection<T> 来读取《尤利西斯》书中以特定字母开头的单词。 我们将复制该项目并更改读取文本行的代码以在 BlockingCollection<T> 内使用 ConcurrentStack<T>。 这将使行以相反的顺序输出,因为堆栈使用后进先出 (LIFO) 逻辑。 让我们开始吧!

  1. 复制本章中的 ParallelExtras.BlockingCollection 项目,或者根据需要修改现有项目。
  2. 打开 MainWindow.xaml.cs 并修改 LoadBookLinesFromFile 方法,将新的 ConcurrentStack<string> 传递给 BlockingCollection<string> 的构造函数:
private async Task<BlockingCollection<string>> LoadBookLinesFromFile()
{
    var lines = new BlockingCollection<string>(new
    ConcurrentStack<string>());
    ...
    return lines;
}
  1. 现在,当您运行应用程序并搜索与之前相同的字母(在我们的例子中为 T)时,您将在列表的开头看到一组不同的单词:

图 9.7 – 搜索《尤利西斯》中以 T 开头的单词
image

如果滚动到列表底部,您应该会看到本书开头的单词。 请注意,该列表并未完全反转,因为我们在解析每一行的单词时没有使用 ConcurrentStack<string>。 您可以自己尝试这个作为另一个实验。

总结

在本章中,我们深入研究了 System.Collections.Concurrent 命名空间中的五个集合。 我们在本章中创建了五个示例应用程序,以获得 .NET 6 中可用的每种并发集合类型的一些实践经验。通过混合使用 WPF、WinForms 和 .NET 控制台应用程序项目,我们研究了在自己的应用程序中利用这些集合的一些实际方法。

posted @ 2024-03-30 03:59  F(x)_King  阅读(103)  评论(0编辑  收藏  举报