1、为何须要多核开发?
答案很简单,目前的芯片制造技术对CPU主频的提高已经达到一个极限了,也就是说性能的垂直伸缩已经不太可能了。所以经过多核的方法,可让程序横向的伸缩,这就相似于用多台服务器实现负载均衡(水平伸缩),而不是简单的靠将服务器升级成小型机来提供处理能力(垂直伸缩)。
虽然多核并行计算的概念已经存在了几十年了,但直到最近多核CPU在PC上的普及,多核开发才不得不提引发程序员的重视。
多核开发的本质就是使用多线程进行程序开发,咱们在学数据结构和算法的时候,写的全部的算法都是面向单线程的。而多核开发的目的就是将这些算法改形成多线程的支持,而后系统运行时将这些多线程平均分配到多核处理器上,以实现运行的加速。
2、如何进行多核开发
若是你很熟悉POSIX threads (pthreads) 或者 WinAPI threads,你就能够本身进行开发。
若是你不想设计过多底层的线程操做,那就选择一个并发开发平台,由平台来自动协调,调度和管理多核资源。并发开发平台包括各类线程池的库,例如
.NET的ThreadPool类
Java的Concurrent类
消息传递环境,例如MPI
data-parallel编程环境,例如NESL, RapidMind, ZPL
task-parallel编程环境, 例如Intel的Threading Building Blocks (TBB) 和 Microsoft的Task Parallel Library (TPL)
动态编程环境,例如Cilk or Cilk++或者业界标准OpenMP.
这些并发平台经过提供语言抽象,扩充注释或者提供库函数的方式来支持多核开发。
3、使用并发开发平台具体有哪些好处
咱们从下面几个方面来看:
软件开发中最重要的三个考虑的要素就是
程序的性能 (使用多核就是为了提高程序的性能的)
开发的时间
程序的可靠性
而其中影响开发时间的三个要素是
伸缩性:若是你本身编写线程,你必须考虑用户是双核,四核仍是八核。如何将线程自动适应用户的核数,而且在多核上将线程均衡的负载。
代码简洁:直接使用底层线程库操做代码是十分复杂的
模块化:直接使用底层线程库操做还会破坏代码的模块化
4、具体实例
下面以Fibonacci的例子来演示:它的递归算法常常被用来做为多核开发的例子。
单核时代,咱们写Fibonacci代码的方法以下:
- int fib(int n)
- {
- if (n < 2) return n;
- else {
- int x = fib(n-1);
- int y = fib(n-2);
- return x + y;
- }
- }
-
- int main(int argc, char *argv[])
- {
- int n = atoi(argv[1]);
- int result = fib(n);
- printf("Fibonacci of %d is %d.\n", n, result);
- return 0;
- }
这个算法的核心就是f(n) = f(n-1) + f(n-2),当n很大时,咱们但愿计算f(n-1)和f(n-2)这两个任务可否分摊在一个双核处理器上同时执行。
若是直接使用WinAPI-threaded操做的代码以下:
- int fib(int n)
- {
- if (n < 2) return n;
- else {
- int x = fib(n-1);
- int y = fib(n-2);
- return x + y;
- }
- }
-
- typedef struct {
- int input;
- int output;
- } thread_args;
-
- void *thread_func ( void *ptr )
- {
- int i = ((thread_args *) ptr)->input;
- ((thread_args *) ptr)->output = fib(i);
- return NULL;
- }
-
- int main(int argc, char *argv[])
- {
- pthread_t thread;
- thread_args args;
- int status;
- int result;
- int thread_result;
- if (argc < 2) return 1;
- int n = atoi(argv[1]);
- if (n < 30) result = fib(n);
- else {
- args.input = n-1;
- status = pthread_create(thread,
- NULL, thread_func,
- (void*) &args );
-
- result = fib(n-2);
-
- pthread_join(thread, NULL);
- result += args.output;
- }
- printf("Fibonacci of %d is %d.\n", n, result);
- return 0;
- }
注意main里面的if(n<30),当n在30之内时,计算很是快,就不须要使用多线程,当n大于30以后,咱们生成一个线程用来计算f(n-1),而main的主线程将继续计算f(n-2),这样等两个线程都结束之后(pthread_join(thread, NULL);),咱们将他们的结果相加。
从这个例子就能够看出,本身实现线程的缺点:
1 这个例子正好能够用两个线程分配在两个核上来实现,可若是一个任务须要16个线程同时执行,咱们又不知道客户端究竟是几核的CPU时,这个任务如何分配就成为一个问题。
2 这段代码很是不简洁
3 额外的结构和函数也破坏了算法自己的完整性。
下面咱们使用多核支持库来实现该代码:
使用OpenMP
- int fib(int n) {
- int i, j;
- if (n<2)
- return n;
- else {
- #pragma omp task shared(i)
- i=fib(n-1);
- #pragma omp task shared(j)
- j=fib(n-2);
- #pragma omp taskwait
- return i+j;
- }
-
- }
-
使用Cilk++
- int fib(int n)
- {
- if (n < 2) return n;
- else {
- int x = cilk_spawn fib(n-1);
- int y = fib(n-2);
- cilk_sync;
- return x + y;
- }
- }
-
- int main(int argc, char *argv[])
- {
- int n = atoi(argv[1]);
- int result = fib(n);
- printf("Fibonacci of %d is %d.\n", n, result);
- return 0;
- }
.NET Task Parallel Library中相应的例子
- Private Function FiboFullParallel(ByVal N As Long) As Long
- If N <= 0 Then Return 0
- If N = 1 Then Return 1
-
- Dim t1 As Tasks.Future(Of Long) = Tasks.Future(Of Long).Create( Function() FiboFullParallel(N - 1))
- Dim t2 As Tasks.Future(Of Long) = Tasks.Future(Of Long).Create( Function() FiboFullParallel(N - 2))
-
- Return t1.Value + t2.Value
- End Function
能够看到不管使用哪一种并发平台,代码都很是简洁,没有破坏原有的算法封装,仅仅经过简单的改造就能够实现自动任务的分派。
5、什么状况下该使用多核编程呢?
若是一个任务的执行时间在10-100毫秒,那么就无需使用多核,由于将任务经过多线程分解到多核上计算,而后再将结果集合起来的开销大体须要100毫秒(固然具体多少依据机器的性能以及你所使用的编译器的性能),并且还须要消耗内存的空间。
在OpenMP里面咱们可使用"if clause"来给双核配置增长条件,例以下面的代码很明显,当n小于100000的时候,不使用多核,当n大于的时候再使用
- #pragma omp parallel for if(n > 100000)
- for (i = 0; i < n;, i++) {
- ...
- }
6、后记
本文旨在告诉你为什么要进行多核开发,以及简单展现了多核开发平台的使用。实际的多核开发要复杂的多,并且咱们知道目前的PC机的多核系统都是基于共享内存的,虽然每一个核都有本身的一级缓存。所以不一样核上的线程在运行时就涉及到对资源竞争使用的问题。除此之外若是应用须要用到IO(硬盘,网络)的时候,也存在一样的问题。所以多核的设计的难点就在于须要具体状况具体分析,找出多核应用的瓶颈,经过改进数据结构或算法,消除或优化这个瓶颈。