C# 多线程小试牛刀
2019年6月28日更新
采用预先生成随机数 + bitarray 来判断重复 + 数组分段插入隔离进行插入 1000w的数据的不重复随机数插入可以压缩到 3 s 内。
前言
昨天在上班时浏览博问,发现了一个问题,虽然自己在 C# 多线程上没有怎么尝试过,看了几遍 CLR 中关于 线程的概念和讲解(后面三章)。也想拿来实践实践。问题定义是这样的:
对于多线程不是很懂,面试的时候遇到一个多线程的题,不会做,分享出来,懂的大佬指点一下,谢谢
建一个winform窗体,在窗体中放上一个开始按钮,一个停止按钮,一个文本框,在窗体中声明一个List类型的属性,点击开始按钮后开启10个线程,所有线程同时不间断的给List集合中添加1-10000之间的随机数,要求添加List集合中的数字不能重复,并且实时在文本框中显示集合的长度,当集合List的长度等于1000时自动停止所有线程,如果中途点击停止按钮也停止所有线程,点击开始又继续执行。
我其实没有完全实现了这位博问中提问的同学的需求,具体问题的来源可查看该地址 问题来源
开始尝试
刚拿到这个需求的时候,映入我脑海里的是 Task, Threadpool,Concurrent,和 Lock 等概念,接下来就是组装和编码的过程了,首先理一理头绪,
- 生成随机数
- 插入到 List 中,且不能重复
- 开启多个线程同时插入。
首先是生成 随机数,使用 System.Random 类来生成伪随机数(这个其实性能和效率贼低,后面再叙述)
private int GenerateInt32Num()
{
var num = random.Next(0, TOTAL_NUM);
return num;
}
然后是插入到 List<Int32> 中的代码,判断是否 已经达到了 我们需要的 List 长度,如果已满足,则退出程序。
private void AddToList(int num)
{
if (numList.Count == ENDNUM)
{
return;
}
numList.Add(num);
}
如果是个 单线程的,按照上面那样 while(true) 然后一直插入即可,可这个是个 多线程,那么需要如何处理呢?
我思考了一下,想到了之前在 CLR 中学到的 可以用 CancellationTokenSource 中的 Cancel 来通知 Task 来取消操作。所以现在的逻辑是,用线程池来实现多线程。然后传入 CancellationTokenSource.Token 来取消任务。
最后用 Task.WhanAny() 来获取到第一个到达此 Task 的 ID。
首先是建立 Task[] 的数组
internal void DoTheCompeteSecond()
{
Task[] tasks = new Task[10];
for (int i = 0; i < 10; ++i)
{
int num = i;
tasks[i] = Task.Factory.StartNew(() => AddNumToList(num, cts), cts.Token);
}
Task.WaitAny(tasks);
}
然后 AddNumToList 方法是这样定义的,
private void AddNumToList(object state, CancellationTokenSource cts)
{-
Console.WriteLine("This is the {0} thread,Current ThreadId={1}",
state,
Thread.CurrentThread.ManagedThreadId);
while (!cts.Token.IsCancellationRequested)
{
if (GetTheListCount() == ENDNUM)
{
cts.Cancel();
Console.WriteLine("Current Thread Id={0},Current Count={1}",
Thread.CurrentThread.ManagedThreadId,
GetTheListCount());
break;
}
var insertNum = GenerateInt32Num();
if (numList.Contains(insertNum))
{
insertNum = GenerateInt32Num();
}
AddToList(insertNum);
}
}
看起来是没有什么问题的,运行了一下。得到了如下结果,

这应该是昨晚运行时得到的数据,当时也没有多想,就贴了上去,回答了那位提问同学的问题。但是心里有一个疑惑,为什么会同时由 两个 Thread 同时达到了该目标呢?
发现问题
今天早上到公司时,我又打开了这个 代码,发现确实有点不对劲,于是就和我边上 做 Go 语言开发的同学,问了问他,哪里出现了问题,他和我说:“你加了读写锁了吗?” 你这里有数据脏读写。心里面有了点眉目。
按照他说的,修改了一下 AddToList 里面的逻辑,这时候,确实解决了上面的问题,
private void AddToList(int num)
{
rwls.EnterReadLock();
if (numList.Count == ENDNUM)
return;
rwls.ExitReadLock();
rwls.EnterWriteLock();
numList.Add(num);
rwls.ExitWriteLock();
}
得到的结果如下:

完整的代码如下所示:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace CSharpFundamental
{
class MultipleThreadCompete
{
List<int> numList = new List<int>();
Random random = new Random();
CancellationTokenSource cts = new CancellationTokenSource();
private const int ENDNUM = 1000000;
ReaderWriterLockSlim rwls = new ReaderWriterLockSlim();
internal void DoTheCompeteSecond()
{
Stopwatch sw = new Stopwatch();
sw.Start();
Task[] tasks = new Task[100];
for (int i = 0; i < 100; ++i)
{
int num = i;
tasks[i] = Task.Run(() => AddNumToList(num, cts), cts.Token);
}
Task.WaitAny(tasks);
Console.WriteLine("ExecuteTime={0}", sw.ElapsedMilliseconds / 1000);
}
private int GetTheListCount()
{
return numList.Count;
}
private void AddToList(int num)
{
rwls.EnterReadLock();
if (numList.Count == ENDNUM)
return;
rwls.ExitReadLock();
rwls.EnterWriteLock();
numList.Add(num);
rwls.ExitWriteLock();
}
private void AddNumToList(object state, CancellationTokenSource cts)
{
Console.WriteLine("This is the {0} thread,Current ThreadId={1}",
state,
Thread.CurrentThread.ManagedThreadId);
while (!cts.Token.IsCancellationRequested)
{
try
{
rwls.EnterReadLock();
if (numList.Count == ENDNUM)
{
cts.Cancel();
Console.WriteLine("Current Thread Id={0},Current Count={1}",
Thread.CurrentThread.ManagedThreadId,
GetTheListCount());
break;
}
}
finally
{
rwls.ExitReadLock();
}
var insertNum = GenerateInt32Num();
if (numList.Contains(insertNum))
{
insertNum = GenerateInt32Num();
}
AddToList(insertNum);
}
}
private int GenerateInt32Num()
{
return random.Next(1, ENDNUM);
}
}
}
这时候,那位 Go 语言的同学和我说,我们试试 1000w 的数据插入,看看需要多少时间?于是我让他用 Go 语言实现了一下上面的逻辑,1000w数据用了 三分钟,我让他看看总共生成了多少随机数,他查看了一下生成了 1亿4千多万的数据。
最开始我用上面的代码来测,发现我插入 1000w 的数据,CPU 到100% 而且花了挺长时间,程序根本没反应,查看了一下我判断重复的语句numList.Contains()
底层实现的代码为:
[__DynamicallyInvokable]
public bool Contains(T item)
{
if ((object) item == null)
{
for (int index = 0; index < this._size; ++index)
{
if ((object) this._items[index] == null)
return true;
}
return false;
}
EqualityComparer<T> equalityComparer = EqualityComparer<T>.Default;
for (int index = 0; index < this._size; ++index)
{
if (equalityComparer.Equals(this._items[index], item))
return true;
}
return false;
}
可想而知,如果数据量很大的话,这个循环不就 及其缓慢吗?
我于是请教了那位 GO 的同学,判断重复的逻辑用什么来实现的,他和我说了一个位图 bitmap 的概念,
我用其重写了一下判断重复的逻辑,代码如下:
int[] bitmap = new int[MAX_SIZE];
var index = num % TOTAL_NUM;
bitMap[index] = 1;
return bitMap[num] == 1;
在添加到 List 的时候,顺便插入到 bitmap 中,判断重复只需要根据当前元素的位置是否 等于 1 即可,
我修改代码后,跑了一下 1000w 的数据用来 3000+ ms。
这时候,引起了他的极度怀疑,一向以高性能并发 著称的 Go 速度竟然这么慢吗?他一度怀疑我的逻辑有问题。
下午结束了一个阶段的工作后,我又拾起了我上午写的代码,果不其然,发现了逻辑错误:
如下:
var insertNum = GenerateInt32Num();
if (numList.Contains(insertNum))
{
insertNum = GenerateInt32Num();
}
生成随机数这里,这里有个大问题,就是其实只判断了一次,导致速度那么快,正确的写法应该是
while (ContainsNum(currentNum))
{
currentNum = GenerateInt32Num();
}
private int GenerateInt32Num()
{
var num = random.Next(0, TOTAL_NUM);
//Console.WriteLine(num);
return num;
}
最后的代码如下:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace CSharpFundamental
{
class MultipleThreadCompete
{
List<int> numList = new List<int>();
Random random = new Random();
CancellationTokenSource cts = new CancellationTokenSource();
private const int TOTAL_NUM = 1000000;
private const int CURRENT_THREAD_COUNT = 35;
ReaderWriterLockSlim rwls = new ReaderWriterLockSlim();
int[] bitMap = new int[TOTAL_NUM];
internal void DoTheCompete()
{
//ThreadPool.SetMinThreads(CURRENT_THREAD_COUNT, CURRENT_THREAD_COUNT);
Stopwatch sw = new Stopwatch();
sw.Start();
Task[] tasks = new Task[CURRENT_THREAD_COUNT];
for (int i = 0; i < CURRENT_THREAD_COUNT; ++i)
{
int num = i;
tasks[i] = Task.Run(() => ExecuteTheTask(num, cts), cts.Token);
}
Task.WaitAny(tasks);
Console.WriteLine("ExecuteTime={0}", sw.ElapsedMilliseconds);
}
private int GetTheListCount()
{
return numList.Count;
}
private void AddToList(int num)
{
if (numList.Count == TOTAL_NUM)
return;
numList.Add(num);
var index = num % TOTAL_NUM;
bitMap[index] = 1;
}
private void ExecuteTheTask(object state, CancellationTokenSource cts)
{
Console.WriteLine("This is the {0} thread,Current ThreadId={1}",
state,
Thread.CurrentThread.ManagedThreadId);
while (!cts.Token.IsCancellationRequested)
{
try
{
rwls.EnterReadLock();
if (numList.Count == TOTAL_NUM)
{
cts.Cancel();
Console.WriteLine("Current Thread Id={0},Current Count={1}",
Thread.CurrentThread.ManagedThreadId,
GetTheListCount());
break;
}
}
finally
{
rwls.ExitReadLock();
}
var currentNum = GenerateInt32Num();
while (ContainsNum(currentNum))
{
currentNum = GenerateInt32Num();
}
rwls.EnterWriteLock();
AddToList(currentNum);
rwls.ExitWriteLock();
}
}
private int GenerateInt32Num()
{
var num = random.Next(0, TOTAL_NUM);
//Console.WriteLine(num);
return num;
}
private bool ContainsNum(int num)
{
rwls.EnterReadLock();
var contains = bitMap[num] == 1;
rwls.ExitReadLock();
return contains;
}
}
}
结果如下:

但是这个代码执行 1000w的数据需要好久。 这个问题继续研究。
源码地址:https://github.com/doublnt/dotnetcore/tree/master/CSharpFundamental
欢迎大佬指点,还望不吝赐教。
C# 多线程小试牛刀的更多相关文章
- Python中的多进程与多线程(一)
一.背景 最近在Azkaban的测试工作中,需要在测试环境下模拟线上的调度场景进行稳定性测试.故而重操python旧业,通过python编写脚本来构造类似线上的调度场景.在脚本编写过程中,碰到这样一个 ...
- 多线程爬坑之路-Thread和Runable源码解析之基本方法的运用实例
前面的文章:多线程爬坑之路-学习多线程需要来了解哪些东西?(concurrent并发包的数据结构和线程池,Locks锁,Atomic原子类) 多线程爬坑之路-Thread和Runable源码解析 前面 ...
- 多线程爬坑之路-学习多线程需要来了解哪些东西?(concurrent并发包的数据结构和线程池,Locks锁,Atomic原子类)
前言:刚学习了一段机器学习,最近需要重构一个java项目,又赶过来看java.大多是线程代码,没办法,那时候总觉得多线程是个很难的部分很少用到,所以一直没下决定去啃,那些年留下的坑,总是得自己跳进去填 ...
- Java多线程
一:进程与线程 概述:几乎任何的操作系统都支持运行多个任务,通常一个任务就是一个程序,而一个程序就是一个进程.当一个进程运行时,内部可能包括多个顺序执行流,每个顺序执行流就是一个线程. 进程:进程 ...
- .NET基础拾遗(5)多线程开发基础
Index : (1)类型语法.内存管理和垃圾回收基础 (2)面向对象的实现和异常的处理基础 (3)字符串.集合与流 (4)委托.事件.反射与特性 (5)多线程开发基础 (6)ADO.NET与数据库开 ...
- Java多线程基础——对象及变量并发访问
在开发多线程程序时,如果每个多线程处理的事情都不一样,每个线程都互不相关,这样开发的过程就非常轻松.但是很多时候,多线程程序是需要同时访问同一个对象,或者变量的.这样,一个对象同时被多个线程访问,会出 ...
- C#多线程之线程池篇3
在上一篇C#多线程之线程池篇2中,我们主要学习了线程池和并行度以及如何实现取消选项的相关知识.在这一篇中,我们主要学习如何使用等待句柄和超时.使用计时器和使用BackgroundWorker组件的相关 ...
- C#多线程之线程池篇2
在上一篇C#多线程之线程池篇1中,我们主要学习了如何在线程池中调用委托以及如何在线程池中执行异步操作,在这篇中,我们将学习线程池和并行度.实现取消选项的相关知识. 三.线程池和并行度 在这一小节中,我 ...
- C#多线程之线程池篇1
在C#多线程之线程池篇中,我们将学习多线程访问共享资源的一些通用的技术,我们将学习到以下知识点: 在线程池中调用委托 在线程池中执行异步操作 线程池和并行度 实现取消选项 使用等待句柄和超时 使用计时 ...
随机推荐
- python已安装好第三方库,pycharm import时仍标红的解决办法
pip install pymysql之后导入import pymysql时候标红 发现 pymysql下方还是标红,不能正常导入 可以试用一下以下的办法 解决办法: 首先打开 Settings找到P ...
- UEditor中多图上传的bug
多图上传 预览:支持浏览器版本 IE8以上 在线管理:由于存在bug,显示不了 ueditor-1.1.1.jar解压后找到FileManager 1.修改com.baidu.ueditor.hun ...
- 【转】Java重构-策略模式、状态模式、卫语句
前言 当代码中出现多重if-else语句或者switch语句时.弊端之一:如果这样的代码出现在多处,那么一旦出现需求变更,就需要把所有地方的if-else或者switch代码进行更改,要是遗漏了某一处 ...
- 高德定位腾讯定位在APP上无法开启定位权限的解决方案
[备注]公司项目中遇到的问题,如果你在团队工作其中定有不少配合方面的问题,其中的思路是可以借鉴的,因为这也许正是你们现在遇到的问题,总结的不好的地方还请多多指教 因为项目需求的确定,定位成了必不可少的 ...
- WebAssembly MDN简单使用
MDN 就是通过编译器编译完成c后生成的胶水代码 引入js 就能直接调用定义在c或者c++中的函数了 c代码如下: #include <stdio.h> #include <stdl ...
- React初识整理(四)--React Router(路由)
官网:https://reacttraining.com/react-router 后端路由:主要做路径和方法的匹配,从而从后台获取相应的数据 前端路由:用于路径和组件的匹配,从而实现组件的切换. 如 ...
- Luogu P1080国王游戏(贪心)
国王游戏 题目链接:国王游戏 ps:题目数据说明了要写高精度. 这个题的答案是\(a.l * a.r < b.l * b.r\)按照这个进行排序 题解中大部分只是如何证明排序是: \(a.l * ...
- DP入门练习
T1 题目:codevs4815江哥的dp题a codevs4815 一个简单的DP,注意开long long(不然会全WA),以及初始条件(这题有负数,所以要把f设成极小值.还要保证转移正确). # ...
- centos 安装 yum apt
以下地址 http://download.csdn.NET/detail/mimi00x/8081263 执行安装命令 rpm -i rpmforge-release-0.5.3-1.el7.rf.x ...
- pep-8要求归纳
代码布局缩进每个缩进级别使用4个空格. 连续行应使用Python的隐式行连接括号,括号和大括号,或使用悬挂缩进 来垂直对齐包装元素.当使用悬挂式缩进时,应考虑以下内容:第一行应该没有任何争论,应该使用 ...