为了让共享的数组,集合能够被多线程更新,我们现在(.net4.0之后)可以使用并发集合来实现这个功能。而System.Collections和System.Collections.Generic命名空间中所提供的经典列表,集合和数组都不是线程安全的,如果要使用,还需要添加代码来同步。

先看一个例子,通过并行循环向一个List<string>集合添加元素。因为List不是线程安全的,所以必须对Add方法加锁来串行化。

任务开始:

   private static int NUM_AES_KEYS =;
static void Main(string[] args)
{
Console.WriteLine("任务开始...");
var sw = Stopwatch.StartNew();
for (int i = ; i < ; i++)
{
ParallelGennerateMD5Keys();
Console.WriteLine(_keyList.Count);
}
Console.WriteLine("结束时间:" + sw.Elapsed); Console.ReadKey();
}
        private static List<string> _keyList;

        private static void ParallelGennerateMD5Keys()
{
_keyList=new List<string>(NUM_AES_KEYS);
Parallel.ForEach(Partitioner.Create(, NUM_AES_KEYS + ), range =>
{
var md5M = MD5.Create();
for (int i = range.Item1; i < range.Item2; i++)
{
byte[] data = Encoding.Unicode.GetBytes(Environment.UserName + i);
byte[] result = md5M.ComputeHash(data);
string hexString = ConverToHexString(result);
lock (_keyList)
{
_keyList.Add(hexString);
}
}
});
}

但如果我们去掉lock,得到的结果如下:

没有一次是满80000的。lock关键字创建了一个临界代码区,当一个任务进入之后,其他任务会被阻塞并等待进入。lock关键字引入了一定的开销,而且会降低可扩展性。对于这个问题,.Net4.0提供了System.Collections.Concurrent命名空间用于解决线程安全问题,它包含了5个集合:ConcurrentQueue<T>,ConcurrentStack<T>,ConcurrentBag<T>,BlockingCollection<T>,ConcurrentDictionary<TKey,TValue>。这些集合都在某种程度上使用了无锁技术,性能得到了提升。

ConcurrentQueue

一个FIFO(先进先出)的集合。支持多任务进并发行入队和出队操作。

ConcurrentQueue是完全无锁的,它是System.Collections.Queue的并发版本。提供三个主要的方法:

  • Enqueue--将元素加入到队列尾部。
  • TryDequeue--尝试删除队列头部元素。并将元素通过out参数返回。返回值为bool型,表示是否执行成功。
  • TryPeek--尝试将队列头部元素通过out参数返回,但不会删除这个元素。返回值bool型,表示操作是否成功。

修改上面的代码:

        private static ConcurrentQueue<string> _keyQueue;
private static void ParallelGennerateMD5Keys()
{
_keyQueue = new ConcurrentQueue<string>();
Parallel.ForEach(Partitioner.Create(, NUM_AES_KEYS + ), range =>
{
var md5M = MD5.Create();
for (int i = range.Item1; i < range.Item2; i++)
{
byte[] data = Encoding.Unicode.GetBytes(Environment.UserName + i);
byte[] result = md5M.ComputeHash(data);
string hexString = ConverToHexString(result);
_keyQueue.Enqueue(hexString);
}
});
}

结果如下:

可以看见,它的使用很简单,不用担心同步问题。接下我们通过生产者-消费者模式,对上面的问题进行改造,分解成两个任务。使用两个共享的ConcurrentQueue实例。_byteArraysQueue 和 _keyQueue ,ParallelGennerateMD5Keys 方法生产byte[],ConverKeysToHex方法去消费并产生key。

        private static ConcurrentQueue<string> _keyQueue;
private static ConcurrentQueue<byte[]> _byteArraysQueue;
private static void ParallelGennerateMD5Keys(int maxDegree)
{
var parallelOptions = new ParallelOptions{MaxDegreeOfParallelism = maxDegree};
var sw = Stopwatch.StartNew();
_keyQueue = new ConcurrentQueue<string>();
Parallel.ForEach(Partitioner.Create(, NUM_AES_KEYS + ),parallelOptions, range =>
{
var md5M = MD5.Create();
for (int i = range.Item1; i < range.Item2; i++)
{
byte[] data = Encoding.Unicode.GetBytes(Environment.UserName + i);
byte[] result = md5M.ComputeHash(data);
_byteArraysQueue.Enqueue(result);
}
});
Console.WriteLine("MD5结束时间:" + sw.Elapsed);
} private static void ConverKeysToHex(Task taskProducer)
{
var sw = Stopwatch.StartNew();
while (taskProducer.Status == TaskStatus.Running || taskProducer.Status == TaskStatus.WaitingToRun || _byteArraysQueue.Count > )
{
byte[] result;
if (_byteArraysQueue.TryDequeue(out result))
{
string hexString = ConverToHexString(result);
_keyQueue.Enqueue(hexString);
}
}
Console.WriteLine("key结束时间:" + sw.Elapsed);
}

这次我修改了执行次数为180000

         private static int NUM_AES_KEYS =180000;
static void Main(string[] args)
{
Console.WriteLine("任务开始...");
var sw = Stopwatch.StartNew();
_byteArraysQueue=new ConcurrentQueue<byte[]>();
_keyQueue=new ConcurrentQueue<string>(); //生产key 和 消费key的两个任务
var taskKeys = Task.Factory.StartNew(()=>ParallelGennerateMD5Keys(Environment.ProcessorCount - 1));
var taskHexString = Task.Factory.StartNew(()=>ConverKeysToHex(taskKeys)); string lastKey;
//隔半秒去看一次。
while (taskHexString.Status == TaskStatus.Running || taskHexString.Status == TaskStatus.WaitingToRun)
{
Console.WriteLine("_keyqueue的个数是{0},_byteArraysQueue的个数是{1}", _keyQueue.Count,_byteArraysQueue.Count);
if (_keyQueue.TryPeek(out lastKey))
{
// Console.WriteLine("第一个Key是{0}",lastKey);
}
Thread.Sleep();
}
//等待两个任务结束
Task.WaitAll(taskKeys, taskHexString); Console.WriteLine("结束时间:" + sw.Elapsed);
Console.WriteLine("key的总数是{0}" , _keyQueue.Count);
Console.ReadKey();
}

从结果可以发现,_bytaArraysQueue里面的byte[] 几乎是生产一个,就被消费一个。

理解生产者和消费者

使用ConcurrentQueue可以很容易的实现并行的生产者-消费者模式或多阶段的线性流水线。如下:

我们可以改造上面的main方法,让一半的线程用于生产,一半的线程用于消费。

  static void Main(string[] args)
{
Console.WriteLine("任务开始...");
var sw = Stopwatch.StartNew();
_byteArraysQueue=new ConcurrentQueue<byte[]>();
_keyQueue=new ConcurrentQueue<string>(); var taskKeyMax = Environment.ProcessorCount/; var taskKeys = Task.Factory.StartNew(() => ParallelGennerateMD5Keys(taskKeyMax)); var taskHexMax = Environment.ProcessorCount - taskKeyMax;
var taskHexStrings=new Task[taskHexMax];
for (int i = ; i < taskHexMax; i++)
{
taskHexStrings[i] = Task.Factory.StartNew(() => ConverKeysToHex(taskKeys));
} Task.WaitAll(taskHexStrings); Console.WriteLine("结束时间:" + sw.Elapsed);
Console.WriteLine("key的总数是{0}" , _keyQueue.Count);
Console.ReadKey();
}

而这些消费者的结果又可以继续作为生产者,继续串联下去。

ConcurrentStack

一个LIFO(后进先出)的集合,支持多任务并发进行压入和弹出操作。它是完全无锁的。是System.Collections.Stack的并发版本。

它和ConcurrentQueue非常相似,区别在于使用了不同的方法名,更好的表示一个栈。ConcurrentStack主要提供了下面五个重要方法。

  • Push:将元素添加到栈顶。
  • TryPop:尝试删除栈顶部的元素,并通过out返回。返回值为bool,表示操作是否成功。
  • TryPeek:尝试通过out返回栈顶部的元素,返回值为bool,表示是否成功。
  • PushRange:一次将多个元素插入栈顶。
  • TryPopRange:一次将多个元素从栈顶移除。

为了判断栈是否包含任意项,可以使用IsEmpty属性判断。

if(!_byteArraysStack.IsEmpty)

而使用Count方法,开销相对较大。另外我们可以将不安全的集合或数组转化为并发集合。下例将数组作为参数传入。操作上和List一样。

   private static string[] _HexValues = {"AF", "BD", "CF", "DF", "DA", "FE", "FF", "FA"};
static void Main(string[] args)
{
var invalidHexStack = new ConcurrentStack<string>(_HexValues); while (!invalidHexStack.IsEmpty)
{
string value;
invalidHexStack.TryPop(out value);
Console.WriteLine(value);
}
}

反之,可以用CopyTo和ToArray方法将并发集合创建一个不安全集合。

ConcurrentBag

一个无序对象集合,在同一个线程添加元素(生产)和删除元素(消费)的场合下效率特别高,ConcurrentBag最大程度上减少了同步的需求以及同步带来的开销。然而它在生产线程和消费线程完全分开的情况下,效率低下。  

它提供了3个重要方法

  • Add--添加元素到无序组
  • TryTake--尝试从无序组中删除一个元素,out返回。返回值bool 表示操作是否成功。
  • TryPeek--尝试通过out返回一个参数。返回值bool 表示操作是否成功。

下面的实例中Main方法通过Parallel.Invoke并发的加载三个方法。有多个生产者和消费者。对应三个ConcurrentBag<string>:_sentencesBag,_capWrodsInSentenceBag和_finalSentencesBag。

  • ProduceSentences 随机生产句子 (消费者)
  • CapitalizeWordsInSentence  改造句子 (消费者/生产者)
  • RemoveLettersInSentence  删除句子 (消费者)
    static void Main(string[] args)
{
Console.WriteLine("任务开始...");
var sw = Stopwatch.StartNew(); _sentencesBag=new ConcurrentBag<string>();
_capWrodsInSentenceBag=new ConcurrentBag<string>();
_finalSentencesBag=new ConcurrentBag<string>(); _producingSentences = true; Parallel.Invoke(ProduceSentences,CapitalizeWordsInSentence,RemoveLettersInSentence); Console.WriteLine("_sentencesBag的总数是{0}", _sentencesBag.Count);
Console.WriteLine("_capWrodsInSentenceBag的总数是{0}", _capWrodsInSentenceBag.Count);
Console.WriteLine("_finalSentencesBag的总数是{0}", _finalSentencesBag.Count);
Console.WriteLine("总时间:{0}",sw.Elapsed);
Console.ReadKey();
}
 private static ConcurrentBag<string> _sentencesBag;
private static ConcurrentBag<string> _capWrodsInSentenceBag;
private static ConcurrentBag<string> _finalSentencesBag; private static volatile bool _producingSentences = false;
private static volatile bool _capitalWords = false; private static void ProduceSentences()
{
string[] rawSentences =
{
"并发集合你可知",
"ConcurrentBag 你值得拥有",
"stoneniqiu",
"博客园",
".Net并发编程学习",
"Reading for you",
"ConcurrentBag 是个无序集合"
};
try
{
Console.WriteLine("ProduceSentences...");
_sentencesBag = new ConcurrentBag<string>();
var random = new Random();
for (int i = ; i < NUM_AES_KEYS; i++)
{
var sb = new StringBuilder();
sb.Append(rawSentences[random.Next(rawSentences.Length)]);
sb.Append(' ');
_sentencesBag.Add(sb.ToString());
} }
finally
{
_producingSentences = false;
}
} private static void CapitalizeWordsInSentence()
{
SpinWait.SpinUntil(() => _producingSentences); try
{
Console.WriteLine("CapitalizeWordsInSentence...");
_capitalWords = true;
while ((!_sentencesBag.IsEmpty)||_producingSentences)
{
string sentence;
if (_sentencesBag.TryTake(out sentence))
{
_capWrodsInSentenceBag.Add(sentence.ToUpper()+"stoneniqiu");
}
}
}
finally
{
_capitalWords = false;
}
} private static void RemoveLettersInSentence()
{
SpinWait.SpinUntil(() => _capitalWords);
Console.WriteLine("RemoveLettersInSentence...");
while (!_capWrodsInSentenceBag.IsEmpty || _capitalWords)
{
string sentence;
if (_capWrodsInSentenceBag.TryTake(out sentence))
{
_finalSentencesBag.Add(sentence.Replace("stonenqiu",""));
}
}
}

在CapitalizeWordsInSentence 方法中,使用SpinUntil方法并传入共享bool变量_producingSentences,当其为true的时候,SpinUnit方法会停止自旋。但协调多个生产者和消费者自旋并非最好的解决方案,我们可以使用BlockingCollection(下面会讲)来提升性能。

 SpinWait.SpinUntil(() => _producingSentences);

另外两个用作标志的共享bool变量在声明的时候使用了volatile关键字。这样可以确保在不同的线程中进行访问的时候,可以得到这些变量的最新值。

   private static volatile bool _producingSentences = false;
private static volatile bool _capitalWords = false;

BlockingCollection

与经典的阻塞队列数据结构类似,适用于多个任务添加和删除数据的生产者-消费者的情形。提供了阻塞和界限的能力。

BlockingCollection是对IProducerConsumerCollection<T>实例的一个包装。而这个接口继承于ICollection,IEnumerable<T>。前面的并发集合都继承了这个接口。因此这些集合都可以封装在BlockingCollection中。

将上面的例子换成BlockingCollection

 static void Main(string[] args)
{
Console.WriteLine("任务开始...");
var sw = Stopwatch.StartNew(); _sentencesBC = new BlockingCollection<string>(NUM_SENTENCE);
_capWrodsInSentenceBC = new BlockingCollection<string>(NUM_SENTENCE);
_finalSentencesBC = new BlockingCollection<string>(NUM_SENTENCE); Parallel.Invoke(ProduceSentences,CapitalizeWordsInSentence,RemoveLettersInSentence); Console.WriteLine("_sentencesBag的总数是{0}", _sentencesBC.Count);
Console.WriteLine("_capWrodsInSentenceBag的总数是{0}", _capWrodsInSentenceBC.Count);
Console.WriteLine("_finalSentencesBag的总数是{0}", _finalSentencesBC.Count);
Console.WriteLine("总时间:{0}",sw.Elapsed);
Console.ReadKey();
} private static int NUM_SENTENCE = ;
private static BlockingCollection<string> _sentencesBC;
private static BlockingCollection<string> _capWrodsInSentenceBC;
private static BlockingCollection<string> _finalSentencesBC; private static volatile bool _producingSentences = false;
private static volatile bool _capitalWords = false; private static void ProduceSentences()
{
string[] rawSentences =
{
"并发集合你可知",
"ConcurrentBag 你值得拥有",
"stoneniqiu",
"博客园",
".Net并发编程学习",
"Reading for you",
"ConcurrentBag 是个无序集合"
}; Console.WriteLine("ProduceSentences...");
_sentencesBC = new BlockingCollection<string>();
var random = new Random();
for (int i = ; i < NUM_SENTENCE; i++)
{
var sb = new StringBuilder();
sb.Append(rawSentences[random.Next(rawSentences.Length)]);
sb.Append(' ');
_sentencesBC.Add(sb.ToString());
}
//让消费者知道,生产过程已经完成
_sentencesBC.CompleteAdding(); } private static void CapitalizeWordsInSentence()
{
Console.WriteLine("CapitalizeWordsInSentence...");
//生产者是否完成
while (!_sentencesBC.IsCompleted)
{
string sentence;
if (_sentencesBC.TryTake(out sentence))
{
_capWrodsInSentenceBC.Add(sentence.ToUpper() + "stoneniqiu");
}
}
//让消费者知道,生产过程已经完成
_capWrodsInSentenceBC.CompleteAdding();
} private static void RemoveLettersInSentence()
{
//SpinWait.SpinUntil(() => _capitalWords);
Console.WriteLine("RemoveLettersInSentence...");
while (!_capWrodsInSentenceBC.IsCompleted)
{
string sentence;
if (_capWrodsInSentenceBC.TryTake(out sentence))
{
_finalSentencesBC.Add(sentence.Replace("stonenqiu",""));
}
}
}

无需再使用共享的bool变量来同步。在操作结束后,调用CompeteAdding方法来告之下游的消费者。这个时候IsAddingComplete属性为true。

_sentencesBC.CompleteAdding();

而在生产者中也无需使用自旋了。可以判断IsCompleted属性。而当IsAddingComplete属性为true且集合为空的时候,IsCompleted才为true。这个时候就表示,生产者的元素已经被使用完了。这样代码也更简洁了。

  while (!_sentencesBC.IsCompleted)

最后的结果要比使用ConcurrentBag快了0.8秒。一共是200w条数据,处理三次。

ConcurrentDictionary

与经典字典类似,提供了并发的键值访问。它对读操作是完全无锁的,在添加和修改的时候使用了细粒度的锁。是IDictionary的并发版本。

它提供最重要方法如下:

  • AddOrUpdate--如果键不存在就添加一个键值对。如果键已经存在,就更新键值对。可以使用函数来生成或者更新键值对。需要在委托内添加同步代码来确保线程安全。
  • GetEnumerator--返回遍历整个ConcurrentDictionary的枚举器,而且是线程安全的。
  • GetOrAdd--如果键不存在就添加一个新键值对,如果存在就返回这个键现在的值,而不添加新值。
  • TryAdd
  • TryOrGetVaule
  • TryRemove
  • TryUpdate

下面的例子创建一个ConcurrentDictionary,然后不断的更新。lock关键字确保一次只有一个线程运行Update方法。

 static void Main(string[] args)
{
Console.WriteLine("任务开始...");
var sw = Stopwatch.StartNew(); rectangInfoDic=new ConcurrentDictionary<string, RectangInfo>();
GenerateRectangles();
foreach (var keyValue in rectangInfoDic)
{
Console.WriteLine("{0},{1},更新次数{2}",keyValue.Key,keyValue.Value.Size,keyValue.Value.UpdateTimes);
} Console.WriteLine("总时间:{0}",sw.Elapsed);
Console.ReadKey();
} private static ConcurrentDictionary<string, RectangInfo> rectangInfoDic;
private const int MAX_RECTANGLES = ;
private static void GenerateRectangles()
{
Parallel.For(, MAX_RECTANGLES + , (i) =>
{
for (int j = ; j < ; j++)
{
var newkey = string.Format("Rectangle{0}", i%);
var rect = new RectangInfo(newkey, i, j);
rectangInfoDic.AddOrUpdate(newkey, rect, (key, existRect) =>
{
if (existRect != rect)
{
lock (existRect)
{
existRect.Update(rect.X,rect.Y);
}
return existRect;
}
return existRect;
});
} }); }

Rectangle:

    public class RectangInfo:IEqualityComparer<RectangInfo>
{
public string Name { get; set; }
public int X { get; set; }
public int Y { get; set; }
public int UpdateTimes { get; set; } public int Size
{
get { return X*Y; }
} public DateTime LastUpdate { get; set; } public RectangInfo(string name,int x,int y)
{
Name = name;
X = x;
Y = y;
LastUpdate = DateTime.Now;
} public RectangInfo(string key) : this(key, , )
{
} public void Update(int x,int y)
{
X = x;
Y = y;
UpdateTimes++;
} public bool Equals(RectangInfo x, RectangInfo y)
{
return (x.Name == y.Name && x.Size == y.Size);
} public int GetHashCode(RectangInfo obj)
{
return obj.Name.GetHashCode();
}
}

本章学习了五种并发集合,熟悉了生产者-消费者的并发模型,我们可以使用并发集合来设计并优化流水线。希望本文对你有帮助。

阅读书籍:《C#并行编程高级教程》 。

【读书笔记】.Net并行编程(三)---并行集合的更多相关文章

  1. .Net多线程 并行编程(三)---并行集合

    为了让共享的数组,集合能够被多线程更新,我们现在(.net4.0之后)可以使用并发集合来实现这个功能. 而System.Collections和System.Collections.Generic命名 ...

  2. .NET 并行编程——数据并行

    本文内容 并行编程 数据并行 环境 计算 PI 矩阵相乘 把目录中的全部图片复制到另一个目录 列出指定目录中的所有文件,包括其子目录 最近,对多线程编程,并行编程,异步编程,这三个概念有点晕了,之前我 ...

  3. 一、并行编程 - 数据并行 System.Threading.Tasks.Parallel 类

    一.并行概念 1.并行编程 在.NET 4中的并行编程是依赖Task Parallel Library(后面简称为TPL) 实现的.在TPL中,最基本的执行单元是task(中文可以理解为"任 ...

  4. .NET 并行编程——任务并行

    本文内容 并行编程 任务并行 隐式创建和运行任务 显式创建和运行任务 任务 ID 任务创建选项 创建任务延续 创建分离的子任务 创建子任务 等待任务完成 组合任务 任务中的异常处理 取消任务 Task ...

  5. 【转载】MDX Step by Step 读书笔记(四) - Working with Sets (使用集合)

    1. Set  - 元组的集合,在 Set 中的元组用逗号分开,Set 以花括号括起来,例如: { ([Product].[Category].[Accessories]), ([Product].[ ...

  6. Java并发编程的艺术读书笔记(2)-并发编程模型

    title: Java并发编程的艺术读书笔记(2)-并发编程模型 date: 2017-05-05 23:37:20 tags: ['多线程','并发'] categories: 读书笔记 --- 1 ...

  7. Java并发编程的艺术读书笔记(1)-并发编程的挑战

    title: Java并发编程的艺术读书笔记(1)-并发编程的挑战 date: 2017-05-03 23:28:45 tags: ['多线程','并发'] categories: 读书笔记 --- ...

  8. 《Essential C++》读书笔记 之 C++编程基础

    <Essential C++>读书笔记 之 C++编程基础 2014-07-03 1.1 如何撰写C++程序 头文件 命名空间 1.2 对象的定义与初始化 1.3 撰写表达式 运算符的优先 ...

  9. C# 并行编程 之 并发集合 (.Net Framework 4.0)(转)

    转载地址:http://blog.csdn.net/wangzhiyu1980/article/details/45497907 此文为个人学习<C#并行编程高级教程>的笔记,总结并调试了 ...

随机推荐

  1. <停车卫> 产品需求说明书 version 2.0

    <停车卫> 产品需求说明书 文档版本号: Version 2.0 文档编号: xxxx 文档密级: 归属部门/项目: 产品名: 停车卫 子系统名: 编写人: kina 编写日期: 2015 ...

  2. ASP.NET中的XML和JSON

    一.DOM简介 1.XML 定义:XML是一种跨语言.跨平台的数据储存格式 2.什么是DOM DOM(document object model)文档对象模型:是一种允许程序或脚本动态的访问更新文档内 ...

  3. 在Android Studio中使用xUtils2.6.14,import org.apache.http不可用

    添加依赖 compile 'org.apache.httpcomponents:httpcore:4.4.2' 删除重复的v-4包

  4. [15]APUE:pipe / FIFO

    管道 pipe 一.概述 管道(pipe / FIFO)是一种文件,属于 pipefs 文件系统类型,可以使用 read.write.close 等系统调用进行操作 其本质是内核维护了一块缓冲区与管道 ...

  5. Mac下U盘安装系统“未验证的错误”

    bash下 输入下面命令: date 1220141012015.30

  6. Twitter Bootstrap

    Twitter Bootstrap是一个HTML/CSS/JS框架,适用于移动设备优先的响应式网页开发.主要涉及: HTML:为已有的H5标签扩展了自定义属性 data-* CSS : Reset + ...

  7. the fifth class

      1.实际比背景长,怎么做到的? 2个父级一个做头背景一个做尾背景 2.2层,每次自带背景上下是覆盖关系,如何做到 2层?,子浮动 3.标签 4.border可覆盖:margin-bottom 为负 ...

  8. LeetCode(131)Palindrome Partitioning

    题目 Given a string s, partition s such that every substring of the partition is a palindrome. Return ...

  9. H5中的拖拽事件

    最近浏览了张鑫旭大神的基于HTML5 drag/drop模块拖动插入排序删除完整实例,感觉受益匪浅.于是将最做的demo记录下来. 首先浏览一下事件,这些事件比较好记,只要记住用在谁的身上就好了,无非 ...

  10. swift 如何给tabBarItem的相关设计

    //设置tabBarItem的title,以及点击和不点击状态图片 self.tabBarController.tabBarItem = UITabBarItem(title: "投资理财& ...