多核開發入門指南

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代碼的方法以下:
  1. int fib(int n) 
  2. {
  3.   if (n < 2) return n;
  4.  else {
  5.       int x = fib(n-1);
  6.       int y = fib(n-2);
  7.       return x + y;
  8.      }
  9. }       
  10.                 
  11. int main(int argc, char *argv[])
  12. {       
  13.   int n = atoi(argv[1]);
  14.  int result = fib(n);
  15.  printf("Fibonacci of %d is %d.\n", n, result);
  16.  return 0;
  17. }
       這個算法的核心就是f(n) = f(n-1) + f(n-2),當n很大時,咱們但願計算f(n-1)和f(n-2)這兩個任務可否分攤在一個雙核處理器上同時執行。
 
       若是直接使用WinAPI-threaded操做的代碼以下:

  
  1.       int fib(int n)
  2.  {
  3.    if (n < 2) return n;
  4.    else {
  5.     int x = fib(n-1);
  6.     int y = fib(n-2);
  7.      return x + y;
  8.      }
  9.  }
  10.      
  11.  typedef struct {
  12.        int input;
  13.        int output;
  14.      } thread_args;
  15.      
  16.   void *thread_func ( void *ptr )
  17.   {
  18.     int i = ((thread_args *) ptr)->input;
  19.     ((thread_args *) ptr)->output = fib(i);
  20.     return NULL;
  21.   }
  22.       
  23.  int main(int argc, char *argv[])
  24.      {
  25.        pthread_t thread;
  26.        thread_args args;
  27.        int status;
  28.        int result;
  29.        int thread_result;
  30.        if (argc < 2) return 1;
  31.        int n = atoi(argv[1]);
  32.        if (n < 30) result = fib(n);
  33.        else {
  34.          args.input = n-1;
  35.          status = pthread_create(thread,
  36.      NULL, thread_func,
  37.      (void*) &args );
  38.         // main can continue executing while the thread executes.
  39.          result = fib(n-2);
  40.         // Wait for the thread to terminate.
  41.          pthread_join(thread, NULL);
  42.          result += args.output;
  43.        }
  44.  printf("Fibonacci of %d is %d.\n", n, result);
  45.  return 0;
  46.  }
       注意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
 
  1. int fib(int n) {
  2.   int i, j;
  3.   if (n<2)
  4.      return n;
  5.   else {
  6.     #pragma omp task shared(i)
  7.     i=fib(n-1);
  8.     #pragma omp task shared(j)
  9.     j=fib(n-2);
  10.     #pragma omp taskwait
  11.     return i+j;
  12.   }
  13. }
使用Cilk++
  1. int fib(int n) 
  2. {
  3.   if (n < 2) return n;
  4.  else {
  5.       int x = cilk_spawn fib(n-1);
  6.       int y = fib(n-2);
  7.       cilk_sync;
  8.       return x + y;
  9.      }
  10. }       
  11.                 
  12. int main(int argc, char *argv[])
  13. {       
  14.   int n = atoi(argv[1]);
  15.  int result = fib(n);
  16.  printf("Fibonacci of %d is %d.\n", n, result);
  17.  return 0;
  18. }
.NET Task Parallel Library中相應的例子
  1.   Private Function FiboFullParallel(ByVal N As LongAs Long
  2.        If N <= 0 Then Return 0
  3.        If N = 1 Then Return 1
  4.    
  5.        Dim t1 As Tasks.Future(Of Long) = Tasks.Future(Of Long).Create( Function() FiboFullParallel(N - 1))
  6.        Dim t2 As Tasks.Future(Of Long) = Tasks.Future(Of Long).Create( Function() FiboFullParallel(N - 2))
  7.    
  8.        Return t1.Value + t2.Value
  9.    End Function
     能夠看到不管使用哪一種併發平臺,代碼都很是簡潔,沒有破壞原有的算法封裝,僅僅經過簡單的改造就能夠實現自動任務的分派。
 

5、什麼狀況下該使用多核編程呢?
        若是一個任務的執行時間在10-100毫秒,那麼就無需使用多核,由於將任務經過多線程分解到多核上計算,而後再將結果集合起來的開銷大體須要100毫秒(固然具體多少依據機器的性能以及你所使用的編譯器的性能),並且還須要消耗內存的空間。
        在OpenMP裏面咱們可使用"if clause"來給雙核配置增長條件,例以下面的代碼很明顯,當n小於100000的時候,不使用多核,當n大於的時候再使用
  1. #pragma omp parallel for if(n > 100000)
  2. for (i = 0; i < n;, i++) {
  3. ...
  4. }
6、後記
       本文旨在告訴你爲什麼要進行多核開發,以及簡單展現了多核開發平臺的使用。實際的多核開發要複雜的多,並且咱們知道目前的PC機的多核系統都是基於共享內存的,雖然每一個核都有本身的一級緩存。所以不一樣核上的線程在運行時就涉及到對資源競爭使用的問題。除此之外若是應用須要用到IO(硬盤,網絡)的時候,也存在一樣的問題。所以多核的設計的難點就在於須要具體狀況具體分析,找出多核應用的瓶頸,經過改進數據結構或算法,消除或優化這個瓶頸。
相關文章
相關標籤/搜索