为了让共享的数组,集合能够被多线程更新,我们现在(.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. SQL Server 常用函数

    1.DATEADD 在向指定日期加上一段时间的基础上,返回新的 datetime 值. 语法 DATEADD ( datepart , number, date ) 参数 datepart 是规定应向 ...

  2. racle wm_concat(column)函数的使用

    oracle wm_concat(column)函数使我们经常会使用到的,下面就教您如何使用oracle wm_concat(column)函数实现字段合并,如果您对oracle wm_concat( ...

  3. 结合阿里云服务器,设置家中jetson tk1随时远程登陆

    前提条件: 1.路由配置dmz主机为tk1的ip ,设置路由器中ssh 端口22的访问权限 2.有一台远程服务器,服务器安装了php可以运行php文件(我使用的是阿里云) 家中tk1配置: 脚本pyt ...

  4. Ext JS

    官網:http://www.sencha.com/products/extjs/

  5. Ajax Step By Step2

    第二.[$.get()和$.post()方法] .load()方法是局部方法(有需要父$),因为他需要一个包含元素的 jQuery 对象作为前缀.而$.get()和 $.post()是全局方法,无须指 ...

  6. WPF Combobox样式

    <ControlTemplate x:Key="ComboBoxToggleButton" TargetType="{x:Type ToggleButton}&qu ...

  7. 页面轮换,ViewFlipper 和 ViewPager 的区别

    ViewFlipper继承ViewAnimator,切换view的时候是有动画效果的,适合做ppt,多界面的程序欢迎引导界面,算是个轻量级的组件,适合展示静态数据,少量数据. ViewPager继承V ...

  8. 解决SQL server 2014 修改表中的字段,无法保存的问题。

    修改PROJECT表中的字段,保存时,弹出上面的窗体,无法保存. 解决方法为:[工具]->[选项]->[设计器]中,去掉“阻止保存要求重新创建表的更改”前的勾选.

  9. js高阶函数

    我是一个对js还不是很精通的选手: 关于高阶函数详细的解释 一个高阶函数需要满足的条件(任选其一即可) 1:函数可以作为参数被传递 2:函数可以作为返回值输出 吧函数作为参数传递,这代表我们可以抽离一 ...

  10. MFC修改初始窗口大小和窗口名字禁止窗口最大,最小化

    2,在里面就可以修改初始窗口大小和窗口名字 BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs){if( !CFrameWnd::PreCrea ...