今天领导给我们发了一篇文章文章,让我们学习一下。

文章链接:TAM - Threaded Array Manipulator

这是codeproject上的一篇文章,花了一番时间阅读了一下。文章主要是介绍当单线程处理大量数组遇到性能瓶颈时,使用多线程的方式进行处理,可以缩短数组的处理时间。

看了这篇文章后,感觉似曾相识,很多次,当我想要处理大数组时,我就会进行构思,然后想出的解决方案,与此文章中介绍的方案非常的相似。但是说来惭愧,此文章的作者有了构思后便动手写出了实现代码,然后还进行了性能测试,而我每次只是构思,觉得我能想出来就可以了,等到真正用的时候再把它写出来就行了。事实上,我大概已经构思过很多次了,但是还从来没有写过,直到看到这篇文章,我才下定决心,一定要将这个思路整理一遍。

当单线程处理大数组遇到性能瓶颈时应该怎样处理

虽然科技一直在进步,CPU的处理能力也一直在提高,但是当我们进入大数据时代后,CPU每秒钟都会面临着大量的数据需要处理,这个时候CPU的处理能力可能就会成为性能瓶颈。这是我们就要选择多核多CPU了,编程中也就是使用多线程进行处理。

首先看下单线程处理的例子

  1. static void Main(string[] args)
  2. {
  3. int count = 100000000;
  4. double[] arrayForTest = new double[count];
  5. Stopwatch watch = new Stopwatch();
  6. watch.Start();
  7. for (int i = 0; i < arrayForTest.Length; i++)
  8. {
  9. arrayForTest[i] = MathOperationFunc(arrayForTest[i]);
  10. }
  11. watch.Stop();
  12. Console.WriteLine("经过 " + arrayForTest.Length + " 次循环共消耗时间 " + (watch.ElapsedMilliseconds / 1000.0) + " s");
  13. }
  14.  
  15. static double MathOperationFunc(double value)
  16. {
  17. return Math.Sin(Math.Sqrt(Math.Sqrt((double)value * Math.PI)) * 1.01);
  18. }

单线程处理的耗时

这个单线程的例子中对一个有10000000个元素的数组中的每个元素进行了数学计算,执行完毕共计耗时5.95秒。

然后看两个线程处理的例子

  1. static void Main(string[] args)
  2. {
  3. //四线程测试
  4. int threadCount = 2;
  5. Thread[] threads = new Thread[threadCount];
  6. for (int i = 0; i < threadCount; i++)
  7. {
  8. threads[i] = new Thread(ForTestInThread);
  9. threads[i].Name = threadCount + "线程测试" + (i + 1);
  10. threads[i].Start();
  11. }
  12. }
  13. //工作线程
  14. static void ForTestInThread()
  15. {
  16. int count = 50000000;
  17. double[] arrayForTest = new double[count];
  18. Stopwatch watch = new Stopwatch();
  19. watch.Start();
  20. for (int i = 0; i < arrayForTest.Length; i++)
  21. {
  22. arrayForTest[i] = MathOperationFunc(arrayForTest[i]);
  23. }
  24. watch.Stop();
  25. Console.WriteLine("线程:" + Thread.CurrentThread.Name + ",经过 " + arrayForTest.Length + " 次循环共消耗时间 " + (watch.ElapsedMilliseconds / 1000.0) + " s");
  26. }
  27.  
  28. //数据计算
  29. static double MathOperationFunc(double value)
  30. {
  31. return Math.Sin(Math.Sqrt(Math.Sqrt((double)value * Math.PI)) * 1.01);
  32. }

两个线程测试耗时

我们再来看一下四个线程的例子

  1. static void Main(string[] args)
  2. {
  3. //四线程测试
  4. int threadCount = 4;
  5. Thread[] threads = new Thread[threadCount];
  6. for (int i = 0; i < threadCount; i++)
  7. {
  8. threads[i] = new Thread(ForTestInThread);
  9. threads[i].Name = threadCount + "线程测试" + (i + 1);
  10. threads[i].Start();
  11. }
  12. }
  13. //工作线程
  14. static void ForTestInThread()
  15. {
  16. int count = 25000000;
  17. double[] arrayForTest = new double[count];
  18. Stopwatch watch = new Stopwatch();
  19. watch.Start();
  20. for (int i = 0; i < arrayForTest.Length; i++)
  21. {
  22. arrayForTest[i] = MathOperationFunc(arrayForTest[i]);
  23. }
  24. watch.Stop();
  25. Console.WriteLine("线程:" + Thread.CurrentThread.Name + ",经过 " + arrayForTest.Length + " 次循环共消耗时间 " + (watch.ElapsedMilliseconds / 1000.0) + " s");
  26. }
  27.  
  28. //数据计算
  29. static double MathOperationFunc(double value)
  30. {
  31. return Math.Sin(Math.Sqrt(Math.Sqrt((double)value * Math.PI)) * 1.01);
  32. }

四个线程测试耗时

由上面的测试中可以看到,随着线程数的增多,任务被分解后每个线程执行的任务耗时由原来的 6秒 逐渐降到 2秒 左右,由此我们可以猜想当所有线程同时执行的时候,那么总任务的耗时就会下降,接下来让我们来进行更精确的测试。

Thread.Join方法简介

进行多线程测试时,经常会遇到这样的问题:主线程中如何等待所有线程执行结束后,再执行后续任务。

错误的做法

  1. Thread[] threads = new Thread[threadCount];
  2. for (int i = 0; i < threadCount; i++)
  3. {
  4. int beginIndex = i*arrayForTest.Length/threadCount;
  5. int length = arrayForTest.Length/threadCount;
  6. threads[i] = new Thread(WorkerThread);
  7. var arg = new Tuple<double[], int, int>(arrayForTest, beginIndex, length);
  8. threads[i].Name = threadCount + "线程测试" + (i + 1).ToString();
  9. threads[i].Start(arg);
  10. //等待所有线程结束
  1. threads[i].Join();
  2. }

这么做实际上所有的子线程均是串行执行的,并没有达到并行的效果。

正确的做法

  1. Thread[] threads = new Thread[threadCount];
  2. for (int i = 0; i < threadCount; i++)
  3. {
  4. int beginIndex = i*arrayForTest.Length/threadCount;
  5. int length = arrayForTest.Length/threadCount;
  6. threads[i] = new Thread(WorkerThread);
  7. var arg = new Tuple<double[], int, int>(arrayForTest, beginIndex, length);
  8. threads[i].Name = threadCount + "线程测试" + (i + 1).ToString();
  9. threads[i].Start(arg);
  10. }
  11. //等待所有线程结束
  12. foreach (var thread in threads)
  13. {
  14. thread.Join();
  15. }

多线程处理大数组的实现

了解了Thread.Join后,就可以进行多线程处理大数组的代码编写了:

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. int count = 100000000;
  6. double[] arrayForTest = new double[count];
  7. Stopwatch totalWatch = new Stopwatch();
  8. totalWatch.Start();
  9. ThreadTest(arrayForTest, 2);
  10. totalWatch.Stop();
  11. Console.WriteLine("总任务,经过 " + arrayForTest.Length + " 次循环共消耗时间 " + (totalWatch.ElapsedMilliseconds / 1000.0) + " s");
  12. }
  13.  
  14. //大循环测试
  15. static void ForTest(double[] arrayForTest, int beingIndex, int offset, Func<double, double> func)
  16. {
  17. for (int i = beingIndex; i < beingIndex + offset; i++)
  18. {
  19. arrayForTest[i] = func(arrayForTest[i]);
  20. }
  21. }
  22.  
  23. //数学计算
  24. static double MathOperationFunc(double value)
  25. {
  26. return Math.Sin(Math.Sqrt(Math.Sqrt((double)value * Math.PI)) * 1.01);
  27. }
  28.  
  29. static void ThreadTest(double[] arrayForTest, int threadCount)
  30. {
  31. //启动线程
  32. Thread[] threads = new Thread[threadCount];
  33. for (int i = 0; i < threadCount; i++)
  34. {
  35. //为每个线程分配任务
  36. int beginIndex = i*arrayForTest.Length/threadCount;
  37. int length = arrayForTest.Length/threadCount;
  38. threads[i] = new Thread(WorkerThread);
  39. var arg = new Tuple<double[], int, int>(arrayForTest, beginIndex, length);
  40. threads[i].Name = threadCount + "线程测试" + (i + 1).ToString();
  41. threads[i].Start(arg);
  42. threads[i].Join();
  43. }
  44. //等待所有线程结束
  45. foreach (var thread in threads)
  46. {
  47. thread.Join();
  48. }
  49. }
  50.  
  51. //工作线程
  52. static void WorkerThread(object arg)
  53. {
  54. Stopwatch watch = new Stopwatch();
  55. watch.Start();
  56. var argArray = arg as Tuple<double[], int, int>;
  57. if (argArray == null)
  58. return;
  59. ForTest(argArray.Item1, argArray.Item2, argArray.Item3, MathOperationFunc);
  60. watch.Stop();
  61. Console.WriteLine("线程:" + Thread.CurrentThread.Name + ",经过 " + argArray.Item3 + " 次循环共消耗时间 " + (watch.ElapsedMilliseconds/1000.0) + " s");
  62. }
  63. }

这样多线程处理大数组的功能代码就编写完成了,那么性能是如何呢,用事实说话,效果如下:

由图可以看出,将一个大任务分解到两个线程中去执行后,大任务总体的执行时间会缩短,但是与两个线程中耗时最长的线程的执行时间有关。

同时执行耗时由原来的6秒逐渐降到2秒左右。可见在多核的机器上,多线程是可以提高性能的。

所以当单线程处理大数组遇到性能瓶颈时可以考虑通过多线程来处理。

既然这个多线程处理大数组的功能效果非常好,那么何不把它封装为一个类,添加到自己的类库中,这样就可以随时使用了:

  1. class BigArrayFor
  2. {
  3. /// <summary>
  4. /// 执行任务时,使用的线程数
  5. /// </summary>
  6. public int ThreadCount { get; set; }
  7.  
  8. /// <summary>
  9. /// 处理大数组中每个元素的方法
  10. /// </summary>
  11. public Func<double, double> ForFunc { get; private set; }
  12.  
  13. /// <summary>
  14. /// 需要处理的大数组
  15. /// </summary>
  16. public double[] ArrayForTest { get; private set; }
  17.  
  18. /// <summary>
  19. /// 实例化处理大数组的类
  20. /// </summary>
  21. /// <param name="arrayForTest">需要处理的大数组</param>
  22. /// <param name="forFunc">处理大数组中每个元素的方法</param>
  23. public BigArrayFor(double[] arrayForTest, Func<double, double> forFunc)
  24. {
  25. if (arrayForTest == null || forFunc == null)
  26. {
  27. throw new ArgumentNullException();
  28. }
  29. ThreadCount = 4;
  30. ForFunc = forFunc;
  31. ArrayForTest = arrayForTest;
  32. }
  33.  
  34. /// <summary>
  35. /// 开始处理大数组
  36. /// </summary>
  37. public void Run()
  38. {
  39. //启动线程
  40. Thread[] threads = new Thread[ThreadCount];
  41. for (int i = 0; i < ThreadCount; i++)
  42. {
  43. //为每个线程分配任务
  44. int beginIndex = i * (ArrayForTest.Length / ThreadCount);
  45. int length = ArrayForTest.Length / ThreadCount;
  46. threads[i] = new Thread(WorkerThread);
  47. var arg = new Tuple<double[], int, int>(ArrayForTest, beginIndex, length);
  48. threads[i].Name = ThreadCount + "线程测试" + (i + 1);
  49. threads[i].Start(arg);
  50. }
  51. //等待所有线程结束
  52. foreach (var thread in threads)
  53. {
  54. thread.Join();
  55. }
  56. }
  57.  
  58. private void WorkerThread(object arg)
  59. {
  60. var argArray = arg as Tuple<double[], int, int>;
  61. if (argArray == null)
  62. return;
  63. ForTest(argArray.Item1, argArray.Item2, argArray.Item3, ForFunc);
  64. }
  65. //大循环测试
  66. private void ForTest(double[] arrayForTest, int beingIndex, int offset, Func<double, double> func)
  67. {
  68. for (int i = beingIndex; i < beingIndex + offset; i++)
  69. {
  70. arrayForTest[i] = func(arrayForTest[i]);
  71. }
  72. }
  73.  
  74. }

好了,大数组循环类完成了,到目前为止,最多也只测试过4个线程同时处理大数组的效果,那么线程数继续增多,是不是执行时间会随之缩短呢,万事俱备,让我们开始更详细的测试吧

  1. static void Main(string[] args)
  2. {
  3.  
  4. //多线程操作大数组
  5. int count = 100000000;
  6. double[] arrayForTest = new double[count];
  7. //一个线程
  8. ThreadTest(arrayForTest, 1);
  9. //两个线程
  10. ThreadTest(arrayForTest, 2);
  11. //四个线程
  12. ThreadTest(arrayForTest, 4);
  13. //八个线程
  14. ThreadTest(arrayForTest, 8);
  15. //十六个线程
  16. ThreadTest(arrayForTest, 16);
  17. //二十五个线程
  18. ThreadTest(arrayForTest, 25);
  19. //三十二个线程
  20. ThreadTest(arrayForTest, 32);
  21. }
  22.  
  23. static void ThreadTest(double[] arrayForTest, int threadCount)
  24. {
  25. BigArrayFor bigArrayFor = new BigArrayFor(arrayForTest, MathOperationFunc);
  26. bigArrayFor.ThreadCount = threadCount;
  27. Stopwatch totalWatch = new Stopwatch();
  28. totalWatch.Start();
  29. bigArrayFor.Run();
  30. totalWatch.Stop();
  31. Console.WriteLine(bigArrayFor.ThreadCount + " 个线程,经过 " + arrayForTest.Length + " 次循环,共消耗时间 " + (totalWatch.ElapsedMilliseconds / 1000.0) + " s");
  32. Console.WriteLine();
  33. }

然后看测试效果

我们可以看到,随着线程数量的增多,处理数组所需的总体时间并不是随着线性的缩短,这是因为当线程数量超过CPU的核数后,会增加很多的线程调度的时间,当线程超过一定数量后,性能反而会下降。

总结

在多核机器上,当单线程处理大数组遇到性能瓶颈时,可以考虑使用多线程进行处理,但是线程数量要适量,否则会因为线程调度导致性能下降。

由一篇文章引发的思考——多线程处理大数组的更多相关文章

  1. 一篇文章看懂TPCx-BB(大数据基准测试工具)源码

    TPCx-BB是大数据基准测试工具,它通过模拟零售商的30个应用场景,执行30个查询来衡量基于Hadoop的大数据系统的包括硬件和软件的性能.其中一些场景还用到了机器学习算法(聚类.线性回归等).为了 ...

  2. What number should I guess next ?——由《鹰蛋》一题引发的思考

    What number should I guess next ? 这篇文章的灵感来源于最近技术部的团建与著名的DP优化<鹰蛋>.记得在一个月前,查到鹰蛋的题解前,我在与同学讨论时,一直试 ...

  3. Android:学习AIDL,这一篇文章就够了(下)

    前言 上一篇博文介绍了关于AIDL是什么,为什么我们需要AIDL,AIDL的语法以及如何使用AIDL等方面的知识,这一篇博文将顺着上一篇的思路往下走,接着介绍关于AIDL的一些更加深入的知识.强烈建议 ...

  4. Android:学习AIDL,这一篇文章就够了(上)

    前言 在决定用这个标题之前甚是忐忑,主要是担心自己对AIDL的理解不够深入,到时候大家看了之后说——你这是什么玩意儿,就这么点东西就敢说够了?简直是坐井观天不知所谓——那样就很尴尬了.不过又转念一想, ...

  5. 一个ScheduledExecutorService启动的Java线程无故挂掉引发的思考

    2018年12月12日18:44:53 一个ScheduledExecutorService启动的Java线程无故挂掉引发的思考 案件现场 不久前,在开发改造公司一个端到端监控日志系统的时候,出现了一 ...

  6. (转载)Android:学习AIDL,这一篇文章就够了(下)

    前言 上一篇博文介绍了关于AIDL是什么,为什么我们需要AIDL,AIDL的语法以及如何使用AIDL等方面的知识,这一篇博文将顺着上一篇的思路往下走,接着介绍关于AIDL的一些更加深入的知识.强烈建议 ...

  7. (转载)Android:学习AIDL,这一篇文章就够了(上)

    前言 在决定用这个标题之前甚是忐忑,主要是担心自己对AIDL的理解不够深入,到时候大家看了之后说——你这是什么玩意儿,就这么点东西就敢说够了?简直是坐井观天不知所谓——那样就很尴尬了.不过又转念一想, ...

  8. [转帖]很遗憾,没有一篇文章能讲清楚ZooKeeper

    很遗憾,没有一篇文章能讲清楚ZooKeeper https://os.51cto.com/art/201911/606571.htm [51CTO.com原创稿件]互联网时代是信息爆发的时代,信息的高 ...

  9. 一篇文章让Oracle程序猿学会MySql【未完待续】

    一篇文章让Oracle DB学会MySql[未完待续] 随笔前言: 本篇文章是针对已经能够熟练使用Oracle数据库的DB所写的快速学会MySql,为什么敢这么说,是因为本人认为Oracle在功能性方 ...

随机推荐

  1. 无法删除服务器 'old_server_name',因为该服务器用作复制过程中的发布服务器。 (Microsoft SQL Server,错误: 20582)

    无法删除服务器 'old_server_name',因为该服务器用作复制过程中的发布服务器. (Microsoft SQL Server,错误: 20582) 2013-01-05 15:02 478 ...

  2. css中图片等比例缩放

    li img{ display: inline-block; max-height: 60px; max-width: 60px; vertical-align: middle; }

  3. vue2 上传图片

    <template> <div class="vue-upload-img-multiple"> <div v-if="images.len ...

  4. Oracle数据导入导出

    Oracle数据导入导出imp/exp 在oracle安装目录下有EXP.EXE与IMP.EXE这2个文件,他们分别被用来执行数据库的导入导出.所以Oracle数据导入导出imp/exp就相当与ora ...

  5. 分组背包——sicily 1750

    1750. 运动会 限制条件 时间限制: 1 秒, 内存限制: 32 兆 题目描述 ZEH是一名04级的学生,他除了绩点高,还有运动细胞.有一次学院举办运动会,ZEH发现里面的 项目都是他所向披靡的, ...

  6. Qt 为tableview的item添加网格线

    使用qss可以显示每个item的网格: selection-background-color: rgb(170, 170, 127); gridline-color: rgb(255, 255, 25 ...

  7. while语句(1)

    <?php     for ($i=1; $i<=10  ; $i++) {        echo $i."-".($i*10)."<br>&q ...

  8. JavaScript之周道长浅谈变量使用中的坑

    天空一声巨响,道长闪亮登场,飞花落叶,尘土飞扬,此处不应恐慌,用阅读变量的概念来提升气场. 1)变量的声明,使用一个变量之前应该先声明.变量是使用关键字var来声明的,如下: var number; ...

  9. what is a ear

    http://docs.oracle.com/javaee/6/tutorial/doc/bnaby.html An EAR file (see Figure 1-6) contains Java E ...

  10. 重编译Linux命令源代码

    转: http://blog.csdn.net/endoresu/article/details/6967435 以su命令为例. 查看su命令的路径: # which su /bin/su 查看su ...