作为多线程和并行计算不得不考虑的问题就是临界资源的访问问题,解决临界资源的访问通常是加锁或者是使用信号量,这个大家应该很熟悉了。

  而集合作为一种重要的临界资源,通用性更广,为了让大家更安全的使用它们,微软为我们带来了强大的并行集合:System.Collections.Concurrent里面的各位仁兄们。

  首先,咱们从一个经典的问题谈起。

生产者消费者问题

  这个问题是最为经典的多线程应用问题,简单的表述这个问题就是:有一个或多个线程(生产者线程)产生一些数据,同时,还有一个或者多个线程(消费者线程)要取出这些数据并执行一些相应的工作。如下图所示:

  下面就是使用程序去描述这个问题了。

  最直接的想法可能是这样:

static void Main(string[] args)
{
int count = ;
// 临界资源区
var queue = new Queue<string>();
// 生产者线程
Task.Factory.StartNew(() =>
{
while (true)
{
queue.Enqueue("value" + count);
count++;
}
}); // 消费者线程1
Task.Factory.StartNew(() =>
{
while (true)
{
if (queue.Count > )
{
string value = queue.Dequeue();
Console.WriteLine("Worker 1: " + value);
}
} });
// 消费者线程2
Task.Factory.StartNew(() =>
{
while (true)
{
if (queue.Count > )
{
string value = queue.Dequeue();
Console.WriteLine("Worker 2: " + value);
}
} }); Thread.Sleep();
}

  使用Queue<string>模拟了一个简单的资源池,一个生产者放数据,两个消费者消费数据。上面这个程序运行以后会产生异常,异常的原因很简单,当某个时刻,第一个消费者判断queue.Count > 0为true时,就会到Queue中取数据,但是这个时候数据可能会被第二个消费者拿走了,因为第二个消费者也判断出此时有数据可取。这是一个简单的临界资源线程安全问题。

  知道问题了,那么如何解决呢?
  第一种方案是加锁,这个方案是可行的,很多时候我们也是这么做的,包括微软早期实现线程安全的ArrayList和Hashtable内部(Synchronized方法)也是这么实现的。这个方案适用于只有少量的消费者,并且每个消费者都会执行大量操作的时候,这时lock并没什么太大问题,但是,如果是大批量短小精悍的消费者存在的话,lock会严重影响代码的执行效率。
  第二种方案就是我们直接用新的线程安全的集合区解决这个问题。新的线程安全的这些集合内部不再使用lock机制这种比较低效的方式去实现线程安全,而是转而使用SpinWait和Interlocked等机制,间接实现了线程安全,这种方式的效率要高于使用lock的方式。看一下实现代码:

var queue = new ConcurrentQueue<string>();
Task.Factory.StartNew(() =>
{
while (true)
{
queue.Enqueue("value" + count);
count++;
}
}); Task.Factory.StartNew(() =>
{
while (true)
{
string value;
if (queue.TryDequeue(out value))
{
Console.WriteLine("Worker 1: " + value);
}
}
}); Task.Factory.StartNew(() =>
{
while (true)
{
string value;
if (queue.TryDequeue(out value))
{
Console.WriteLine("Worker 2: " + value);
} }
});

  执行这段代码,可以工作,但是有点不太优雅,能不能不要去判断集合是否为空?集合当自己没有元素的时候自己Block一下可以吗?答案当然是可以的,使用BlockingCollection即可:

var blockingCollection = new BlockingCollection<string>();
Task.Factory.StartNew(() =>
{
while (true)
{
blockingCollection.Add("value" + count);
count++;
}
}); Task.Factory.StartNew(() =>
{
while (true)
{
Console.WriteLine("Worker 1: " + blockingCollection.Take());
}
}); Task.Factory.StartNew(() =>
{
while (true)
{
Console.WriteLine("Worker 2: " + blockingCollection.Take());
}
});

  BlockingCollection集合是一个拥有阻塞功能的集合,它就是完成了经典生产者消费者的算法功能。它没有实现底层的存储结构,而是使用了实现IProducerConsumerCollection接口的几个集合作为底层的数据结构,例如ConcurrentBag, ConcurrentStack或者是ConcurrentQueue。你可以在构造BlockingCollection实例的时候传入这个参数,如果不指定的话,则默认使用ConcurrentQueue作为存储结构。

  而对于生产者来说,只需要通过调用其Add方法放数据,消费者只需要调用Take方法来取数据就可以了。
  当然了上面的消费者代码中还有一点是让人不爽的,那就是while语句,可以更优雅一点吗?答案还是肯定的:

Task.Factory.StartNew(() =>
{
foreach (string value in blockingCollection.GetConsumingEnumerable())
{
Console.WriteLine("Worker 1: " + value);
}
});

  GetConsumingEnumerable()方法是关键,这个方法会遍历集合取出数据,一旦发现集合空了,则阻塞自己,直到集合中又有元素了再开始遍历,神奇吧。

  好了,到此完美了解决了生产者消费者问题。然而通常来说,还有两个问题我们有时需要去控制:

第一个问题:控制集合中数据的最大数量。

  这个问题由BlockingCollection构造函数解决,构造该对象实例的时候,构造函数中的BoundedCapacity决定了集合最大的可容纳数据数量,这个比较简单,不多说了。

第二个问题:何时停止的问题。

  这个问题由CompleteAdding和IsCompleted两个配合解决。
  CompleteAdding方法是直接不允许任何元素被加入集合;当使用了CompleteAdding方法后且集合内没有元素的时候,另一个属性IsCompleted此时会为True,这个属性可以用来判断是否当前集合内的所有元素都被处理完。看一下生产者修改后的代码:

Task.Factory.StartNew(() =>
{
for (int count = ; count < ; count++)
{
blockingCollection.Add("value" + count);
} blockingCollection.CompleteAdding();
});

  当使用了CompleteAdding方法后,对象停止往集合中添加数据,这时如果是使用GetConsumingEnumerable枚举的,那么这种枚举会自然结束,不会再Block住集合,这种方式最优雅,也是推荐的写法。但是如果是使用TryTake访问元素的,则需要使用IsCompleted判断一下,因为这个时候使用TryTake会抛InvalidOperationException异常。

看一下最终的代码形式:

static void Main(string[] args)
{
var blockingCollection = new BlockingCollection<string>();
var producer = Task.Factory.StartNew(() =>
{
for (int count = ; count < ; count++)
{
blockingCollection.Add("value" + count);
Thread.Sleep();
} blockingCollection.CompleteAdding();
}); var consumer1 = Task.Factory.StartNew(() =>
{
foreach (string value in blockingCollection.GetConsumingEnumerable())
{
Console.WriteLine("Worker 1: " + value);
}
}); var consumer2 = Task.Factory.StartNew(() =>
{
foreach (string value in blockingCollection.GetConsumingEnumerable())
{
Console.WriteLine("Worker 2: " + value);
}
}); Task.WaitAll(producer, consumer1, consumer2);
}

BlockingCollection的枚举

  此外,需要注意BlockingCollection有两种枚举方法,首先BlockingCollection本身继承自IEnumerable<T>,所以它自己就可以被foreach枚举,首先BlockingCollection包装了一个线程安全集合,那么它自己也是线程安全的,而当多个线程在同时修改或访问线程安全容器时,BlockingCollection自己作为IEnumerable会返回一个一定时间内的集合片段,也就是只会枚举在那个时间点上内部集合的元素。使用这种方式枚举的时候,不会有Block效果。
  另外一种方式就是我们上面使用的GetConsumingEnumerable方式的枚举,这种方式会有Block效果,直到CompleteAdding被调用为止。

  最后提一下实现IProducerConsumerCollection接口的几个集合:ConcurrentBag(线程安全的无序的元素集合), ConcurrentStack(线程安全的堆栈)和ConcurrentQueue(线程安全的队列)。这些都很简单,功能与非线程安全的那些集合都一样,只不多是多了TryXXX方法,多线程环境下使用这些方法就好了,其他就不多说了。

  到此生产者和消费者这个经典的问题告一段落了。

  System.Collections.Concurrent下面的集合除了解决生产者消费者问题外,还有一些与多线程相关的集合,例如:

1. ConcurrentDictionary,这个是键/值对字典的线程安全实现,这个类在原来的基础上也添加了一下新的方法,例如:AddOrUpdate,GetOrAdd,TryXXX等等,都很容易理解。

2. 各种Partitioner 类,提供针对数组、列表和可枚举项的常见分区策略。

  若要对数据源操作进行并行化,其中一个必要步骤是将源分区为可由多个线程同时访问的多个部分。 PLINQ 和任务并行库 (TPL) 提供了默认的分区程序,当编写并行查询或ForEach循环时,默认的分区程序以透明方式工作。 但是毫无疑问,对于一些复杂的情况,我们是可以插入自己的分区程序的,这就是微软为我们提供的各种Partitioner类,这个不多说了,感兴趣的同学请自己参考一下MSDN。

 

C#的变迁史 - C# 4.0 之线程安全集合篇的更多相关文章

  1. C#的变迁史 - C# 5.0 之调用信息增强篇

    Caller Information CallerInformation是一个简单的新特性,包括三个新引入的Attribute,使用它们可以用来获取方法调用者的信息, 这三个Attribute在Sys ...

  2. C#的变迁史 - C# 5.0 之并行编程总结篇

    C# 5.0 搭载于.NET 4.5和VS2012之上. 同步操作既简单又方便,我们平时都用它.但是对于某些情况,使用同步代码会严重影响程序的可响应性,通常来说就是影响程序性能.这些情况下,我们通常是 ...

  3. C# 4.0 之线程安全集合篇

    资料:http://www.cnblogs.com/chengxiaohui/articles/5672768.html

  4. C#的变迁史 - C# 4.0 之并行处理篇

    前面看完了Task对象,这里再看一下另一个息息相关的对象Parallel. Parallel对象 Parallel对象封装了能够利用多核并行执行的多线程操作,其内部使用Task来分装多线程的任务并试图 ...

  5. C#的变迁史 - C# 4.0篇

    C# 4.0 (.NET 4.0, VS2010) 第四代C#借鉴了动态语言的特性,搞出了动态语言运行时,真的是全面向“高大上”靠齐啊. 1. DLR动态语言运行时 C#作为静态语言,它需要编译以后运 ...

  6. C#的变迁史 - C# 4.0 之多线程篇

    在.NET 4.0中,并行计算与多线程得到了一定程度的加强,这主要体现在并行对象Parallel,多线程Task,与PLinq.这里对这些相关的特性一起总结一下. 使用Thread方式的线程无疑是比较 ...

  7. C#的变迁史 - C# 3.0篇

    C# 3.0 (.NET 3.5, VS2008) 第三代C#在语法元素基本完备的基础上提供了全新的开发工具和集合数据查询方式,极大的方便了开发. 1. WPF,WCF,WF 这3个工程类型奠定了新一 ...

  8. C#的变迁史 - C# 2.0篇

    在此重申一下,本文仅代表个人观点,如有不妥之处,还请自己辨别. 第一代的值类型装箱与拆箱的效率极其低下,特别是在集合中的表现,所以第二代C#重点解决了装箱的问题,加入了泛型.1. 泛型 - 珍惜生命, ...

  9. C#的变迁史 - C# 5.0 之其他增强篇

    1. 内置zip压缩与解压 Zip是最为常用的文件压缩格式之一,也被几乎所有操作系统支持.在之前,使用程序去进行zip压缩和解压要靠第三方组件去支持,这一点在.NET4.5中已有所改观,Zip压缩和解 ...

随机推荐

  1. 送干货,实用内联gulp插件——gulp-embed

    现在npm上有很多gulp内联工具,用于把脚本和样式内嵌到HTML页面上,之前搞项目我也在这些插件中寻觅许久,但均不满足公司项目的一个需求—— HTML上同时插入了开发(dev版,src文件夹下,比如 ...

  2. .NET单元测试的艺术-3.测试代码

    开篇:上一篇我们学习单元测试和核心技术:存根.模拟对象和隔离框架,它们是我们进行高质量单元测试的技术基础.本篇会集中在管理和组织单元测试的技术,以及如何确保在真实项目中进行高质量的单元测试. 系列目录 ...

  3. ASP.NET MVC 5 - 将数据从控制器传递给视图

    在我们讨论数据库和数据模型之前,让我们先讨论一下如何将数据从控制器传递给视图.控制器类将响应请求来的URL.控制器类是给您写代码来处理传入请求的地方,并从数据库中检索数据,并最终决定什么类型的返回结果 ...

  4. Elasticsearch Mantanence Lessons Learned Today

    Today I troubleshooted an Elasticsearch-cluster-down issue. Several lessons were learned: When many ...

  5. web应用中使用JavaMail发送邮件

    现在很多的网站都提供有用户注册功能, 通常我们注册成功之后就会收到一封来自注册网站的邮件.邮件里面的内容可能包含了我们的注册的用户名和密码以及一个激活账户的超链接等信息.今天我们也来实现一个这样的功能 ...

  6. Android开发-之SQLite数据库

    之前我们讲了如何将数据存储在文件中,那么除了这种方式呢,就是我们常见的大家都知道的将数据存储在数据库当中了. 将数据存储在数据库中的优势: 1)存储在数据库中的数据更加方便操作,比如增.删.改.查等 ...

  7. MongoDB 文档的删除操作

    在db中删除数据是十分危险的事,建议使用logic delete,即在doc中增加一个field:IsDeleted,将其设置为1,表示该doc在逻辑上被删除,这种workaround将delete操 ...

  8. Security9:查询Login被授予的权限

    在给一个Login授予权限时,发现该Login已经存在,其对应的User也存在于指定的DB中,查看该Login在指定DB中已被授予的权限. 1,查看Login的Server PrincipalID s ...

  9. 使用 DBCC IND 查看BTree 和 Heap结构

    Table 有两种结构:BTree 和 Heap,一个table的结构必定是其中之一.如果在表上创建clustered index,那么表的结构是BTree:如果表上没有创建Clustered ind ...

  10. HTML5系列:HTML5表单

    1. input元素新增类型 url类型 url类型的input元素是一种用来输入url的文本框,提交时如果该文本框中内容不是url格式,则不允许提交. <input type="ur ...