(十一)并发集合

并发集合

命名空间System.Collections.Concurrent 

ConcurrentDictionary

下面的例子演示了ConcurrentDictionary的简单使用。

单线程环境下并发字典和字典的性能对比。

单线程情况下并发字典的写操作比普通字典加锁慢很多,读操作会快些。

在多线程环境下则并发字典性能更好。

using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;

static void Main(string[] args)
{
	var concurrentDict = new ConcurrentDictionary<int, string>();
	var dict = new Dictionary<int, string>();
	var sw = new Stopwatch();
	sw.Start();
	for (int i = 0; i < 1000000; i++)
	{
		lock(dict)
		{
			dict[i] = item;
		}
	}
	sw.Stop();
	Console.WriteLine("加锁写入字典用时:{0}",sw.Elapsed);

	sw.Restart();
	for (int i = 0; i < 1000000; i++)
	{
		concurrentDict[i] = item;
	}
	sw.Stop();
	Console.WriteLine("写入并发字典用时:{0}",sw.Elapsed);

	sw.Start();
	for (int i = 0; i < 1000000; i++)
	{
		lock (dict)
		{
			CurrentItem = dict[i];
		}
	}
	sw.Stop();
	Console.WriteLine("加锁读取字典用时:{0}", sw.Elapsed);

	sw.Restart();
	for (int i = 0; i < 1000000; i++)
	{
		CurrentItem = concurrentDict[i];
		//concurrentDict.TryAdd
	}
	sw.Stop();
	Console.WriteLine("读取并发字典用时:{0}", sw.Elapsed);
	Console.ReadKey();
}

ConcurrentQueue

当程序运行时,使用ConcurrentQueue集合实例创建了一个任务队列。之后创建了一个取消标志,它是用来在我们将任务放入队列后停止工作的。接下来启动了一个单独的工作线程来将任务放入任务队列中。该部分为异步处理产生了工作。

然后创建了四个消费者,它们会随机等待一段时间,然后从任务队列中获取一个任务,处理该任务,一直重复整个过程直到主函数发出取消标志信号。最后启动产生任务的线程,等待该线程完成。然后使用取消标志给消费者发信号,最后一步将等待所有的消费者完成。

using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;	

class Program
{	
	static void Main(string[] args)
	{
		Task t = RunProgram();
		t.Wait();
		Console.ReadKey();
	}

	async static Task RunProgram()
	{
		var taskQueue = new ConcurrentQueue<MyTask>();
		var cts = new CancellationTokenSource();
		var task = Task.Run(() => TaskProducer(taskQueue));
		Task[] processors = new Task[4];
		for (int i = 1; i <=4; i++)
		{
			string processorId = i.ToString();
			processors[i - 1] = Task.Run(() => TaskCustomer(taskQueue, $"processor{processorId}", cts.Token));
		}
		await task;
		cts.CancelAfter(2000);
		await Task.WhenAll(processors);
		cts.Dispose();
	}
	/// <summary>
	/// 任务生产者
	/// </summary>
	/// <param name="queue">任务队列</param>
	/// <returns></returns>
	async static Task TaskProducer(ConcurrentQueue<MyTask> queue)
	{
		for (int i = 1; i <= 100; i++)
		{
			await Task.Delay(50);
			var workItem = new MyTask { Id = i };
			queue.Enqueue(workItem);
			Console.WriteLine("任务{0}已被放置",workItem.Id);
		}
	}
	/// <summary>
	/// 任务消费者
	/// </summary>
	/// <param name="queue">任务队列</param>
	/// <param name="name">消费者名称</param>
	/// <param name="token">任务取消标志</param>
	/// <returns></returns>
	async static Task TaskCustomer(ConcurrentQueue<MyTask> queue,string name,CancellationToken token)
	{
		await GetRandomDelay();
		do
		{
			bool dequeueSuccess = queue.TryDequeue(out MyTask workItem);
			if (dequeueSuccess)
			{
				Console.WriteLine("任务{0}已被{1}取出", workItem.Id, name);
			}
			await GetRandomDelay();
		}
		while (!token.IsCancellationRequested);
	}

	static Task GetRandomDelay()
	{
		int delay = new Random(DateTime.Now.Millisecond).Next(100, 500);
		return Task.Delay(delay);
	}
}

ConcurrentStack

下面的例子创建了一个ConcurrentStack集合的实例。其余的代码与ConcurrentQueue的示例几乎一样,唯一不同之处是对并发堆栈使用PushTryPop方法,而对并发队列使用EnqueueTryDequeue方法。

可以看到任务处理的顺序被改变了。堆栈是一个LIFO集合,消费者先处理最近的任务。

在并发队列中,任务被处理的顺序与被添加的顺序几乎一致。而在堆栈中,早先创建的任务具有较低的优先级,而且直到生产者停止向堆栈中放入更多任务后,该任务才有可能被处理。

using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;	

class Program
{	
	static void Main(string[] args)
	{
		Task t = RunProgram();
		t.Wait();
		Console.ReadKey();
	}

	async static Task RunProgram()
	{
		var taskStack = new ConcurrentStack<MyTask>();
		var cts = new CancellationTokenSource();
		var task = Task.Run(() => TaskProducer(taskStack));
		Task[] processors = new Task[4];
		for (int i = 1; i <=4; i++)
		{
			string processorId = i.ToString();
			processors[i - 1] = Task.Run(() => TaskCustomer(taskStack, $"processor{processorId}", cts.Token));
		}
		await task;
		cts.CancelAfter(2000);
		await Task.WhenAll(processors);
		cts.Dispose();
	}
	/// <summary>
	/// 任务生产者
	/// </summary>
	/// <param name="stack">任务栈</param>
	/// <returns></returns>
	async static Task TaskProducer(ConcurrentStack<MyTask> stack)
	{
		for (int i = 1; i <= 100; i++)
		{
			await Task.Delay(50);
			var workItem = new MyTask { Id = i };
			stack.Push(workItem);
			Console.WriteLine("任务{0}已被放置",workItem.Id);
		}
	}
	/// <summary>
	/// 任务消费者
	/// </summary>
	/// <param name="stack">任务栈</param>
	/// <param name="name">消费者名称</param>
	/// <param name="token">任务取消标志</param>
	/// <returns></returns>
	async static Task TaskCustomer(ConcurrentStack<MyTask> stack,string name,CancellationToken token)
	{
		await GetRandomDelay();
		do
		{
			bool dequeueSuccess = stack.TryPop(out MyTask workItem);
			if (dequeueSuccess)
			{
				Console.WriteLine("任务{0}已被{1}取出", workItem.Id, name);
			}
			await GetRandomDelay();
		}
		while (!token.IsCancellationRequested);
	}

	static Task GetRandomDelay()
	{
		int delay = new Random(DateTime.Now.Millisecond).Next(100, 500);
		return Task.Delay(delay);
	}
}

 ConcurrentBag

该程序模拟了使用多个网络爬虫进行网页索引的场景。

网络爬虫是这样一个程序:它使用网页地址打开一个网页,索引该网页内容,尝试访问该页面包含的所有链接,并且也索引这些链接页面。

首先定义了一个包含不同网页URL的字典,该字典模拟了包含其他页面链接的网页。该实现非常简单,并不关心索引已经访问过的页面。

接着创建了一个并发包,其中包含爬虫任务。我们创建了3个爬虫,并且给每个爬虫都提供了一个不同的网站根URL。然后等待所有爬虫完成工作。现在每个爬虫开始检索提供给它的网站URL。我们通过等待一个随机事件来模拟网络IO处理。如果页面包含的URL越多,爬虫向包中放入的任务也会越多。然后检查包中是否还有任何需要爬虫处理的任务,如果没有 说明爬虫完成了工作。

如果检查前3个根URL后的第一行输出内容,将看到被爬虫N放置的任务通常会被同一个爬虫处理。然而,接下来的行则会不同。这是因为ConcurrentBag 内部针对多个线程既可以添加元素又可以删除元素的场景进行了优化。实现方式是每个线程使用自己的本地队列的元素,所以使用该队列时无需任何锁。只有当本地队列中没有任何元素时,我们才执行一些锁定操作并尝试从其他线程的本地队列中“偷取”工作。这种行为有助于在所有工作者间分发工作并避免使用锁。

using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;	
using System.Collections.Generic;

class Program
{	
	static readonly Dictionary<string, string[]> content = new Dictionary<string, string[]>();
	
	static void Main(string[] args)
	{
		CreateLinks();
		Task t = RunProgram();
		t.Wait();
		Console.ReadKey();
	}

	async static Task RunProgram()
	{
		var bag = new ConcurrentBag<CrawlingTask>();
		string[] urls = new string[] { "https://www.baidu.com", "https://www.taobao.com", "https://www.jd.com" };
		var crawlers = new Task[3];
		for (int i = 1; i <= 3; i++)
		{
			string crawlerName = "crawler" + i;
			bag.Add(new CrawlingTask { UrlToCrawl = urls[i - 1], ProducerName = "root" });
			crawlers[i - 1] = Task.Run(() => CrawlAsync(bag, crawlerName));
		}
		await Task.WhenAll(crawlers);
	}

	async static Task CrawlAsync(ConcurrentBag<CrawlingTask> bag, string crawlerName)
	{
		while (bag.TryTake(out CrawlingTask task))
		{
			IEnumerable<string> urls = await GetLinksFromContent(task);
			if (urls!=null)
			{
				foreach (var url in urls)
				{
					var t = new CrawlingTask { UrlToCrawl = url, ProducerName = crawlerName };
					bag.Add(t);
				}
			}
			Console.WriteLine("indexing url {0} posted by {1} is completed by {2}", task.UrlToCrawl, task.ProducerName, crawlerName); ;
		}
	}

	async static Task<IEnumerable<string>> GetLinksFromContent(CrawlingTask task)
	{
		await GetRandomDelay();
		if (content.ContainsKey(task.UrlToCrawl))
		{
			return content[task.UrlToCrawl];
		}
		return null;
	}

	static void CreateLinks()
	{
		content["https://www.baidu.com"] = new string[] { "https://www.baidu.com/a.html", "https://www.baidu.com/b.html" };
		content["https://www.baidu.com/a.html"] = new string[] { "https://www.baidu.com/c.html", "https://www.baidu.com/d.html" };
		content["https://www.baidu.com/b.html"] = new string[] { "https://www.baidu.com/e.html" };

		content["https://www.taobao.com"] = new string[] { "https://www.taobao.com/a.html", "https://www.taobao.com/b.html" };
		content["https://www.taobao.com/a.html"] = new string[] { "https://www.taobao.com/c.html", "https://www.taobao.com/d.html" };
		content["https://www.taobao.com/b.html"] = new string[] { "https://www.taobao.com/e.html", "https://www.taobao.com/f.html" };

		content["https://www.jd.com"] = new string[] { "https://www.jd.com/a.html", "https://www.jd.com/b.html" };
		content["https://www.jd.com/a.html"] = new string[] { "https://www.jd.com/c.html" };
		content["https://www.jd.com/b.html"] = new string[] { "https://www.jd.com/d.html" };
	}

	static Task GetRandomDelay()
	{
		int delay = new Random(DateTime.Now.Millisecond).Next(100, 300);
		return Task.Delay(delay);
	}
}

BlockingCollection

下面的例子使用了BlockingCollection类,我们能够改变任务存储在阻塞集合中的数据结构。默认情况下它使用的是ConcurrentQueue容器,但是我们能够使用任何实现了IProducerConsumerCollection泛型接口的集合。例子中第二次时使用ConcurrentStack作为底层集合。

消费者通过对阻塞集合迭代调用GetConsumingEnumerable方法来获取工作项。如果在该集合中没有任何元素,迭代器会阻塞工作线程直到有元素被放置到集合中。当生产者调用集合的CompleteAdding时该迭代周期会结束。这标志着工作完成了。

生产者将任务插入到BlockingCollection然后调用CompleteAdding方法,这会使所有消费者完成工作。

using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;	
using System.Collections.Generic;

class Program
{	
	static void Main(string[] args)
	{
		Console.WriteLine("采用队列构造BlockingCollection");
		Console.WriteLine();
		Task t = RunProgram(null);
		t.Wait();
		Console.WriteLine("..........................................................");
		Console.WriteLine("采用栈构造BlockingCollection");
		Console.WriteLine();
		t = RunProgram(new ConcurrentStack<MyTask>());
		t.Wait();
		Console.ReadKey();
	}

	async static Task RunProgram(IProducerConsumerCollection<MyTask> pcc=null)
	{
		var taskPcc = new BlockingCollection<MyTask>();
		if (pcc!=null)
		{
			taskPcc = new BlockingCollection<MyTask>(pcc);
		}
		var task = Task.Run(() => TaskProducer(taskPcc));
		Task[] processers = new Task[4];
		for (int i = 1; i <= 4; i++)
		{
			string processorId = "Processor" + i;
			processers[i - 1] = Task.Run(() => TaskCustomer(taskPcc, processorId));
		}
		await task;
		await Task.WhenAll(processers);
		taskPcc.Dispose();
	}

	/// <summary>
	/// 任务生产者
	/// </summary>
	/// <param name="bc"></param>
	/// <returns></returns>
	async static Task TaskProducer(BlockingCollection<MyTask> bc)
	{
		for (int i = 1; i <= 20; i++)
		{
			await Task.Delay(50);
			var workItem = new MyTask { Id = i };
			bc.Add(workItem);
			Console.WriteLine("任务{0}已被放置",workItem.Id);
		}
		bc.CompleteAdding();
	}
	
	/// <summary>
	/// 任务消费者
	/// </summary>
	/// <param name="bc"></param>
	/// <param name="name">消费者名称</param>
	/// <returns></returns>
	async static Task TaskCustomer(BlockingCollection<MyTask> bc,string name)
	{
		await GetRandomDelay();
		foreach (var item in bc.GetConsumingEnumerable())
		{
			Console.WriteLine("任务{0}已被{1}取出", item.Id, name);
			await GetRandomDelay();
		}
	}

	static Task GetRandomDelay()
	{
		int delay = new Random(DateTime.Now.Millisecond).Next(1, 500);
		return Task.Delay(delay);
	}
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值