C#中的多线程小试牛刀
前言
昨天在上班时浏览博问,发现了一个问题,虽然自己在 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
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对的支持。
上一篇: Python中创建二维数组