posix多線程編程簡介(轉載)

引自:http://www.blog.edu.cn/user1/9641/archives/2005/1039230.shtml

POSIX線程編程起步(1)-Hello World
       在UNIX主機上,線程常常又被稱爲“輕量級進程”,這種稱呼很簡單同時也便於理解,事實上,UNIX線程是從進程演變而來的。與進程相比,線程相當小,創建線程引起的CPU開銷也相對較小。不僅如此,由於線程可以共享內存資源,而不像進程那樣擁有獨立的內存空間,所以使用線程也很節省內存。以後的幾篇文章,將重點講述POSIX 線程標準最常用的部分(主要基於其在DEC OSF/1 OS, V3.0上的實現)。

1.Hello World

        創建進程所用的函數是pthread_create()。它的四個參數包括了:一個線程(pthread_t)變量的指針、一個線程屬性(pthread_attr_t)變量的指針、線程啓動時所要運行的函數指針以及傳遞給該函數的一個參數(void *)。


  pthread_t         a_thread;
  pthread_attr_t    a_thread_attribute;
  void *             thread_function(void *argument);
  char              *some_argument;
  
  pthread_create( &a_thread, a_thread_attribute, thread_function, (void *)some_argument);

       很多時候,線程屬性變量僅僅指定線程使用的最小棧。其實它有更豐富的含義,但現在的情況是,大多數應用程序創建新線程時值不過傳遞了一個THREAD_ATTR_DEFAULT,有時甚至只是NULL。用pthread_create()創建出的新線程,從指定的函數入口開始執行,這與創建進程不同:所有的進程都具有相同的執行序列。這樣設計的原因很簡單:如果所有的進程都從同一進程空間的同一處開始執行,那麼就會有多個進程對相同的共享資源執行相同的指令。

       現在我們已經知道如何創建新線程,就讓我們來開始第一個多線程程序:用多個線程在屏幕上輸出“Hello World”。爲了顯示線程的作用,我們將用到兩個線程:一個用來輸出“Hello”、另一個用來輸出“World”。爲此,我們首先需要一個用於屏幕輸出的函數,新線程將從此函數開始執行。此外,我們還需要兩個線程(pthread_t)變量,用來創建新線程。當然,我們需要在pthread_create()的參數中指明每個新線程應該輸出的字符串。請看以下代碼:

  void * print_message_function( void *ptr );
  
  main()
  {
     pthread_t thread1, thread2;
     char *message1 = "Hello";
     char *message2 = "World";
     
     pthread_create( &thread1, pthread_attr_default,
                    print_message_function, (void*) message1);
     pthread_create(&thread2, pthread_attr_default, 
                    print_message_function, (void*) message2);
  
     exit(0);
  }
  
  void * print_message_function( void *ptr )
  {
     char *message;
     message = (char *) ptr;
     printf("%s ", message);
  }

       這裏需要注意的是print_message_function()函數的原型,以及創建新線程時對參數類型的轉換。程序首先創建第一個新線程並將“Hello”作爲參數傳遞,接着創建了另一個線程並傳遞“World”作爲起始參數。我們希望第一個線程從printf_message_function()開始執行,在輸出“Hello”後結束,接着第二個線程在輸出“World”之後也同樣地結束。這樣的過程看起來似乎很合理,然而其中有兩處嚴重缺陷。

       首先,不同的線程是並行運行的,並無先後次序。因此我們無法保證第一個新線程在第二線程之前輸出字符串。其結果是,屏幕輸出可能是“Hello World”,也可能是“World Hello”。其次,與上述原因類似,父線程(姑且如此稱呼)有可能在兩個子線程輸出之前就執行了exit(0),這將導致整個進程結束——當然兩個子進程也就因此而結束了。其後果是屏幕上可能根本沒有輸出。爲了解決第二個問題,我們可以用pthread_exit()來代替exit(),這樣兩個子進程就不會結束(因爲該函數不會終止整個進程的運行)。

       目前我們的小程序有兩個競爭條件,現在讓我們試着用比較笨的辦法來解決它們。首先,爲了讓兩個子線程按照我們需要的順序運行,我們在創建第二個線程之前插入一個延遲。接着,爲了保證在子線程結束之前父線程不退出,我們在父線程的尾部也插入一個延遲。請看下面的代碼:

  void * print_message_function( void *ptr); 
  
  main()
  {
     pthread_t thread1, thread2;
     char *message1 = "Hello";
     char *message2 = "World";
     
     pthread_create( &thread1, pthread_attr_default,
                    print_message_function, (void *) message1);
     sleep(10);
     pthread_create(&thread2, pthread_attr_default, 
                    print_message_function, (void *) message2);
  
     sleep(10);
     exit(0);
  }
  
  void * print_message_function( void *ptr )
  {
     char *message;
     message = (char *) ptr;
     printf("%s", message);
     pthread_exit(0);
  }

       令人遺憾的是,以上代碼仍然不能達到我們的目的。利用延遲來進行線程同步是很不可靠的。我們目前遇到的同步問題本質上與分佈式程序設計中的同步問題相同:我們永遠無法確知某一個線程將會在何時結束。

       以上代碼的缺陷不只是不可靠,事實上sleep()函數執行時,整個進程都在睡覺而不僅僅是父線程,這一點和exit()很像。當sleep()返回時,我們的程序仍然面對着相同的條件競爭。我們的新代碼不僅沒有解決競爭問題,反而讓我們多花了20秒來等待程序結束。順便應該指出,如果想要對某一線程進行延遲,應該調用pthread_delay_np()函數(np意指non portable,不可移植),如下:

     struct timespec delay;
     delay.tv_sec = 2;
     delay.tv_nsec = 0;
     pthread_delay_np( &delay );

本節涉及的函數: 
pthread_create(), pthread_exit(), pthread_delay_np().


POSIX線程編程起步(2)-線程同步
2.線程同步

        POSIX提供了兩種用於線程同步的原語,這兩種操作分別是互斥以及條件變量。互斥是一種簡單的進行鎖定的原語,其主要作用是控制對共享資源的訪問,防止衝突。關於多線程編程,有一點值得大家注意,那就是整個程序的地址空間有所有的線程共享。其結果是幾乎所有的資源都可以被共享——比如全局變量、文件描述符等。另一方面,在每個線程的入口函數(由pthread_create調用)內,以及由該函數調用的其他函數內,我們都會定義一些私有的局部變量。在多線程程序中,全局變量與局部變量總是被混合使用,要想使多線程程序順利的運行,各線程對共享資源的訪問必須得到控制。

       以下是一個生產者/消費者程序。生產者與消費者對共享緩衝區的訪問由互斥進行控制。
  void * reader_function(void *);
  void * writer_function(void *);
  
  char buffer;
  int buffer_has_item = 0;
  pthread_mutex_t mutex;
  struct timespec delay;
  
  main()
  {
     pthread_t reader;
  
     delay.tv_sec = 2;
     delay.tv_nsec = 0;
  
     pthread_mutex_init(&mutex, pthread_mutexattr_default);
     pthread_create( &reader, pthread_attr_default, reader_function,
                    NULL);
     writer_function();
  }
  
  void * writer_function(void *)
  {
     while(1)
     {
          pthread_mutex_lock( &mutex );
          if ( buffer_has_item == 0 )
          {
               buffer = make_new_item();
               buffer_has_item = 1;
          }
          pthread_mutex_unlock( &mutex);
          pthread_delay_np( &delay );
     }
  }
  
  void * reader_function(void *)
  {
     while(1)
     {
          pthread_mutex_lock( &mutex);
          if ( buffer_has_item == 1)
          {
               consume_item( buffer );
               buffer_has_item = 0;
          }
          pthread_mutex_unlock( &mutex );
          pthread_delay_np( &delay );
     }
  }

       上邊這個簡單的例子程序中,共享緩衝區只能保存一個共享數據項。因此該緩衝區只有兩個狀態:“有”/“無”。生產者在向緩衝區寫入數據前,首先會將互斥上鎖,如果該互斥已被鎖定,則生產者將阻塞直到互斥被解鎖。生產者鎖定了互斥以後,將會檢查緩衝區是否爲空(通過標誌變量buffer_has_item)。如果緩衝區沒有數據,生產者就會產生新數據項放入緩衝區,並設置標誌變量以使得消費者可以知道是否能進行消費。接下來生產者解除對互斥的鎖定並等待,這樣消費者應該有充足的時間來訪問緩衝區。

       消費者採取了相似的過程來訪問緩衝區。它首先鎖定互斥,檢查標誌變量,如果可能則消費掉僅有的數據項。接着消費者解鎖互斥並等待一小會兒好讓生產者有時間寫入新的數據項。

       上例中,生產者和消費者將會持續不斷的運行,不斷的生產、消費。事實上,在通常的程序中,如果確定不再使用某個互斥,則應該用pthread_mutex_destroy(&mutex)將其摧毀。順便提一句,在使用某個互斥之前,應該使用pthread_mutex_init()將其初始化。在我們的例子中,初始化時使用了兩個參數,第一個用來指定被初始化的互斥,第二個則是該互斥的屬性。(在DEC OSF/1上,互斥的屬性沒有實際意義,通常使用PTHREAD_MUTEXATTR_DEFAULT)

       對互斥的正確使用可以有效地減少競爭條件。其實互斥本身是非常簡單的,只有兩個狀態:鎖定、未鎖定。它能實現的功能也是有限的。POSIX還提供了條件變量這一有力工具來補充互斥的不足。使用條件變量,一個線程可以在已經鎖定互斥的情況下被阻塞並等待喚醒信號,而其他線程仍能訪問被鎖定的共享資源。當另外的某一個線程發出信號後,被阻塞的線程將被喚醒並依然可以訪問阻塞前自己鎖定的共享資源。由此,互斥和條件變量的聯合使用可以幫助我們避免循環死鎖的情況出現。我們利用互斥和條件變量設計了一個僅有單一整數信號燈的庫。庫的源代碼可以在附錄A中找到,關於條件變量的說明可以在手冊頁(man pages)裏找到
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章