c# 高级编程 11章237页 【并发集合】【BlockingCollection】

323 阅读3分钟

线程安全的集合 可以防止 多个线程 以互相冲突的方式 访问集合

为了对集合进行线程安全的访问,定义了IProducerConsumerCollection<T>接口

  • TryAdd():
    • 尝试给集合添加一项
    • 如果集合禁止添加项,这个操作就可能失败
    • 返回的布尔值说明是成功还是失败
  • TryTake():
    • 尝试返回集合中的项
    • 如果集合禁止返回项,这个操作就可能失败
    • 返回的布尔值说明是成功还是失败

并发集合

System.Collections.Concurrent中提供:

  1. ConcurrentQueue<T>
    • 用一种免锁定的算法实现。使用在内部合并到一个链表中的32项数组。
    • 有以下方法
      • Enqueue()
      • TryDequeue()
      • TryPeek()
    • 实现了IProducerConsumerCollection<T>
      • TryAdd()调用Enqueue()
      • TryTake()调用TryDequeue()

  1. ConcurrentStack<T>
    • 在内部,使用其元素的链表
    • 有以下方法
      • Push()
      • PushRange()
      • TryPeek()
      • Pop()
      • TryPopRange()

  1. ConcurrentBag<T>
    • 没有定义添加或提取项的任何顺序
    • 把线程映射到内部使用的数组上,因此尝试减少锁定
    • 有以下方法
      • Add()
      • TryPeek()
      • TryTake()

  1. ConcurrentDictionary<TKey, TValue>
    • 有以下方法,以非阻塞的方式访问成员
      • TryAdd()
      • TryGetValue()
      • TryRemove()
      • TryUpdate()
    • 有以下方法
      • UpdateOrAdd()
        • 如果键没有添加到字典中,第二个参数 就定义应该设置的值
        • 如果键已经存在于字典中,updateValueFactory参数就定义值的改变方式
    • 没有实现IProducerConsumerCollection<T>

  1. BlockingCollection<T>
    • 添加 或 提取 元素 之前,会阻塞线程 并一直等待
    • 有以下方法
      • Add()
      • Take()
      • TryAdd()
      • TryTake()

ConcurrentXXX 集合 是线程安全的

如果某个动作不适用于线程的当前状态,就返回false在继续之前,总是需要确认 添加 或提取元素是否成功

不能相信 集合会完成任务


BlockingCollection<T>

添加 或 提取 元素 之前,会阻塞线程 并一直等待

  • Add()添加元素
    • 阻塞线程,直到可以执行
    • 有重载版本:允许传递一个CancellationToken,来取消调用
  • Take()删除元素
    • 阻塞线程,直到可以执行
    • 有重载版本:允许传递一个CancellationToken,来取消调用

如果 不希望 线程无限期地等待下去,且 不希望 从外部取消调用,则可以使用以下方法:(可以设置一个超时)

  • TryAdd()
  • TryTake()

此处,超时:表示在调用失败之前,阻塞线程 的最长时间


示例:

  1. 读取文件名
  2. 加载内容
  3. 处理内容
  4. 转换内容
  5. 添加颜色
  6. 显示内容
    class Program
    {
        static void Main()
        {
            StartPipelineAsync().Wait();
            Console.ReadLine();
        }

        public static async Task StartPipelineAsync()
        {
            var fileNames = new BlockingCollection<string>();
            var lines = new BlockingCollection<string>();
            var words = new ConcurrentDictionary<string, int>();
            var items = new BlockingCollection<Info>();
            var coloredItems = new BlockingCollection<Info>();
            Task t1 = PipelineStages.ReadFilenamesAsync(@"../..", fileNames);
            ColoredConsole.WriteLine("started stage 1");
            Task t2 = PipelineStages.LoadContentAsync(fileNames, lines);
            ColoredConsole.WriteLine("started stage 2");
            Task t3 = PipelineStages.ProcessContentAsync(lines, words);
            await Task.WhenAll(t1, t2, t3);
            ColoredConsole.WriteLine("stages 1, 2, 3 completed");
            Task t4 = PipelineStages.TransferContentAsync(words, items);
            Task t5 = PipelineStages.AddColorAsync(items, coloredItems);
            Task t6 = PipelineStages.ShowContentAsync(coloredItems);
            ColoredConsole.WriteLine("stages 4, 5, 6 started");

            await Task.WhenAll(t4, t5, t6);
            ColoredConsole.WriteLine("all stages finished");
        }
    }
    public static class PipelineStages
    {
        public static Task ReadFilenamesAsync(string path, BlockingCollection<string> output)
        {
            return Task.Factory.StartNew(() =>
            {
                foreach (string filename in Directory.EnumerateFiles(path, "*.cs",
                    SearchOption.AllDirectories))
                {
                    output.Add(filename);
                    ColoredConsole.WriteLine($"stage 1: added {filename}");
                }
                output.CompleteAdding();
            }, TaskCreationOptions.LongRunning);
        }

        public static async Task LoadContentAsync(BlockingCollection<string> input, BlockingCollection<string> output)
        {
            foreach (var filename in input.GetConsumingEnumerable())
            {
                using (FileStream stream = File.OpenRead(filename))
                {
                    var reader = new StreamReader(stream);
                    string line = null;
                    while ((line = await reader.ReadLineAsync()) != null)
                    {
                        output.Add(line);
                        ColoredConsole.WriteLine($"stage 2: added {line}");
                        await Task.Delay(20);
                    }
                }
            }
            output.CompleteAdding();
        }

        public static Task ProcessContentAsync(BlockingCollection<string> input,  ConcurrentDictionary<string, int> output)
        {
            return Task.Factory.StartNew(() =>
            {
                foreach (var line in input.GetConsumingEnumerable())
                {
                    string[] words = line.Split(' ', ';', '\t', '{', '}', '(', ')',
                        ':', ',', '"');
                    foreach (var word in words.Where(w => !string.IsNullOrEmpty(w)))
                    {
                        output.AddOrUpdate(key: word, addValue: 1, updateValueFactory: (s, i) => ++i);
                        ColoredConsole.WriteLine($"stage 3: added {word}");
                    }
                }
            }, TaskCreationOptions.LongRunning);
        }

        public static Task TransferContentAsync(ConcurrentDictionary<string, int> input, BlockingCollection<Info> output)
        {
            return Task.Factory.StartNew(() =>
            {
                foreach (var word in input.Keys)
                {
                    if (input.TryGetValue(word, out int value))
                    {
                        var info = new Info(word, value);
                        output.Add(info);
                        ColoredConsole.WriteLine($"stage 4: added {info}");
                    }
                }
                output.CompleteAdding();
            }, TaskCreationOptions.LongRunning);
        }

        public static Task AddColorAsync(BlockingCollection<Info> input, BlockingCollection<Info> output)
        {
            return Task.Factory.StartNew(() =>
            {
                foreach (var item in input.GetConsumingEnumerable())
                {
                    if (item.Count > 40)
                    {
                        item.Color = "Red";
                    }
                    else if (item.Count > 20)
                    {
                        item.Color = "Yellow";
                    }
                    else
                    {
                        item.Color = "Green";
                    }
                    output.Add(item);
                    ColoredConsole.WriteLine($"stage 5: added color {item.Color} to {item}");
                }
                output.CompleteAdding();
            }, TaskCreationOptions.LongRunning);
        }

        public static Task ShowContentAsync(BlockingCollection<Info> input)
        {
            return Task.Factory.StartNew(() =>
            {
                foreach (var item in input.GetConsumingEnumerable())
                {
                    ColoredConsole.WriteLine($"stage 6: {item}", item.Color);
                }
            }, TaskCreationOptions.LongRunning);
        }
    }
    public class Info
    {
        public Info(string word, int count)
        {
            Word = word;
            Count = count;
        }

        public string Word { get; }
        public int Count { get; }
        public string Color { get; set; }

        public override string ToString() => $"{Count} times: {Word}";
    }
    public class ColoredConsole
    {
        private static object syncOutput = new object();
        public static void WriteLine(string message)
        {
            lock (syncOutput)
            {
                Console.WriteLine(message);
            }
        }
        public static void WriteLine(string message, string color)
        {
            lock (syncOutput)
            {
                Console.ForegroundColor = (ConsoleColor)Enum.Parse(
                    typeof(ConsoleColor), color);
                Console.WriteLine(message);
                Console.ResetColor();
            }
        }
    }

解析

调用CompleteAdding() 通知 所有读取器 不应再等待。 因为集合中不再有新的项被添加进来了

这种情形下,调用CompleteAdding()很重要:写入器往集合中添加项的同时,读取器从集合中读取。

        public static Task ReadFilenamesAsync(string path, BlockingCollection<string> output)
        {
            return Task.Factory.StartNew(() =>
            {
                foreach (string filename in Directory.EnumerateFiles(path, "*.cs",
                    SearchOption.AllDirectories))
                {
                    output.Add(filename);
                    ColoredConsole.WriteLine($"stage 1: added {filename}");
                }
                output.CompleteAdding();
            }, TaskCreationOptions.LongRunning);
        }

直接用input,而不适用input.GetConsumingEnumerable() 是可以的。

但是!这只会迭代当前状态的集合,而不会迭代以后添加的项

这种情形下,调用GetConsumingEnumerable()很重要:写入器往集合中添加项的同时,读取器从集合中读取。

        public static async Task LoadContentAsync(BlockingCollection<string> input, BlockingCollection<string> output)
        {
            foreach (var filename in input.GetConsumingEnumerable())
            {
                using (FileStream stream = File.OpenRead(filename))
                {
                    var reader = new StreamReader(stream);
                    string line = null;
                    while ((line = await reader.ReadLineAsync()) != null)
                    {
                        output.Add(line);
                        ColoredConsole.WriteLine($"stage 2: added {line}");
                        await Task.Delay(20);
                    }
                }
            }
            output.CompleteAdding();
        }