当前位置: 移动技术网 > IT编程>开发语言>c# > C#中的多线程小试牛刀

C#中的多线程小试牛刀

2019年07月18日  | 移动技术网IT编程  | 我要评论

前言

昨天在上班时浏览博问,发现了一个问题,虽然自己在 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

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对移动技术网的支持。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网