線程及 進程間的通信問題!

一個很好的編程隨想的博客http://program-think.blogspot.com/2009/03/producer-consumer-pattern-0-overview.html

架構設計:生產者/消費者模式[0]:概述

 1、如何確定數據單元
2、隊列緩衝區
3、環形緩衝區
4、雙緩衝區

生產 消費
2010-06-01 10:13

#include <boost/thread/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/condition_variable.hpp>
#include <queue>

template<typename T,size_t BUF_SIZE>
class Buffer
{
public:
       void put(T const& element)
       {
         boost::unique_lock<boost::mutex> lock(mQueue_);                
         while(queue_.size() == BUF_SIZE) //Buffer is full.Waiting...
           not_full.wait(lock);    
         queue_.push(element);
         not_empty.notify_all();
       }
      
       T get()
       {
         boost::unique_lock<boost::mutex> lock(mQueue_);       
         while(queue_.empty())            //Buffer is empty.Waiting...
            not_empty.wait(lock);   
         T element = queue_.front();
         queue_.pop();      
         not_full.notify_all();
         return element;
       }  
private:
      std::queue<T> queue_;
      boost::mutex   mQueue_;
      boost::condition_variable not_full;
      boost::condition_variable not_empty;    
};

Buffer<int,10> buf;
void producer()
{
for(int n=0;n<100;++n)
{
    buf.put(n);
}
}
void consumer()
{
for(int x=0;x<100;++x)
{
    int n=buf.get();
}
}

int main()
{
boost::thread thrd1(&producer);
boost::thread thrd2(&consumer);
thrd1.join();
thrd2.join();
}

論1:線程和 進程間的通信問題。

以前一直想找個機會總結一下進程和線程的通信機制,但由於技術和平臺的侷限性,一直沒有找準切入點。由於馬上要畢業了,對自己技術的總結和梳理的前提下寫了本篇文章,如有錯誤之處,敬請拍磚和指教。

         操作系統的主要任務是管理計算機的軟件、硬件資源。現代操作系統的主要特點是多用戶和多任務,也就是程序的並行執行,windows如此linux也是如此。所以操作系統就藉助於進程來管理計算機的軟、硬件資源,支持多任務的並行執行。要並行執行就需要多進程、多線程。因此多進程和多線程間爲了完成一定的任務,就需要進行一定的通信。而線程間通信又和進程間的通信不同。由於進程的數據空間相對獨立而線程是共享數據空間的,彼此通信機制也很不同。

         線程間通信由於多線程共享地址空間和數據空間,所以多個線程間的通信是一個線程的數據可以直接提供給其他線程使用,而不必通過操作系統(也就是內核的調度)。

         進程間的通信則不同,它的數據空間的獨立性決定了它的通信相對比較複雜,需要通過操作系統。以前進程間的通信只能是單機版的,現在操作系統都繼承了基於套接字(socket)的進程間的通信機制。這樣進程間的通信就不侷限於單臺計算機了,實現了網絡通信。

        進程的通信機制主要有:管道、有名管道、消息隊列、信號量、共享空間、信號、套接字。

        管道:它傳遞數據是單向性的,只能從一方流向另一方,也就是一種半雙工的通信方式;只用於有親緣關係的進程間的通信,親緣關係也就是父子進程或兄弟進程;沒有名字並且大小受限,傳輸的是無格式的流,所以兩進程通信時必須約定好數據通信的格式。管道它就像一個特殊的文件,但這個文件之存在於內存中,在創建管道時,系統爲管道分配了一個頁面作爲數據緩衝區,進程對這個數據緩衝區進行讀寫,以此來完成通信。其中一個進程只能讀一個只能寫,所以叫半雙工通信,爲什麼一個只能讀一個只能寫呢?因爲寫進程是在緩衝區的末尾寫入,讀進程是在緩衝區的頭部讀取,他們各自 的數據結構不同,所以功能不同。

        有名管道:看見這個名字就能知道個大概了,它於管道的不同的是它有名字了。這就不同與管道只能在具有親緣關係的進程間通信了。它提供了一個路徑名與之關聯,有了自己的傳輸格式。有名管道和管道的不同之處還有一點是,有名管道是個設備文件,存儲在文件系統中,沒有親緣關係的進程也可以訪問,但是它要按照先進先出的原則讀取數據。同樣也是單雙工的。

        消息隊列:是存放在內核中的消息鏈表,每個消息隊列由消息隊列標識符標識,於管道不同的是,消息隊列存放在內核中,只有在內核重啓時才能刪除一個消息隊列,內核重啓也就是系統重啓,同樣消息隊列的大小也是受限制的。

        信號量:也可以說是一個計數器,常用來處理進程或線程同步的問題,特別是對臨界資源的訪問同步問題。臨界資源:爲某一時刻只能由一個進程或線程操作的資源,當信號量的值大於或等於0時,表示可以供併發進程訪問的臨界資源數,當小於0時,表示正在等待使用臨界資源的進程數。更重要的是,信號量的值僅能由PV操作來改變。

        共享內存:就是分配一塊能被其他進程訪問的內存。共享內存可以說是最有用的進程間通信方式,也是最快的IPC形式。首先說下在使用共享內存區前,必須通過系統函數將其附加到進程的地址空間或說爲映射到進程空間。兩個不同進程A、B共享內存的意思是,同一塊物理內存被映射到進程A、B各自的進程地址空間。進程A可以即時看到進程B對共享內存中數據的更新,反之亦然。由於多個進程共享同一塊內存區域,必然需要某種同步機制,互斥鎖和信號量都可以。採用共享內存通信的一個顯而易見的好處是效率高,因爲進程可以直接讀寫內存,而不需要任何數據的拷貝。對於像管道和消息隊列等通信方式,則需要在內核和用戶空間進行四次的數據拷貝,而共享內存則只拷貝兩次數據[1]:一次從輸入文件到共享內存區,另一次從共享內存區到輸出文件。實際上,進程之間在共享內存時,並不總是讀寫少量數據後就解除映射,有新的通信時,再重新建立共享內存區域。而是保持共享區域,直到通信完畢爲止,這樣,數據內容一直保存在共享內存中,並沒有寫回文件。共享內存中的內容往往是在解除映射時才寫回文件的。因此,採用共享內存的通信方式效率是非常高的

         信號:信號是在軟件層次上中斷機制的一種模擬,在原理上,一個進程收到一個信號與處理器收到一箇中斷請求可以說是一樣的。信號是異步的,一個進程不必通過任何操作來等待信號的到達,事實上,進程也不知道信號到底什麼時候到達。信號是進程間通信機制中唯一的異步通信機制,可以看作是異步通知,通知接收信號的進程有哪些事情發生了。信號機制經過POSIX實時擴展後,功能更加強大,除了基本通知功能外,還可以傳遞附加信息。信號事件的發生有兩個來源:硬件來源(比如我們按下了鍵盤或者其它硬件故障);軟件來源。信號分爲可靠信號和不可靠信號,實時信號和非實時信號。進程有三種方式響應信號1.忽略信號2.捕捉信號 3.執行缺省操作。

        套接字:這一塊在網絡編程那一塊講的 很多,在此就不在說拉。

論2:VC 線程間通信

1.使用全局變量(窗體不適用)
     實現線程間通信的方法有很多,常用的主要是通過全局變量自定義消息事件對象等來實現
的。其中又以對全局變量的使用最爲簡潔。該方法將全局變量作爲線程監視的對象,並通過在主線
程對此變量值的改變而實現對子線程的控制。
     由於這裏的全局變量需要在使用它的線程之外對其值進行改變,這就需要通過volatile關鍵字對
此變量進行說明。對於標準類型的全局變量,我們建議使用volatile 修飾符,它告訴編譯器無需對該變量作任何的優化,即無需將它放到一個寄存器中,並且該值可被外部改變。如果線程間所需傳遞的信息較複雜,我們可以定義一個結構,通過傳遞指向該結構的指針進行傳遞信息。
使用全局變量進行線程通信的方法非常簡單,通過下面給出的示例代碼能夠對其
有一個基本的認識。
 
2.利用自定義消息(可適用於窗體)
     全局變量在線程通信中的應用多用在主線程對子線程的控制上,而從子線程向主線程的信息反饋
則多采用自定義消息的方式來進行。一個線程向另外一個線程發送消息是通過操作系統實現的。利用Windows操作系統的消息驅動機制,當一個線程發出一條消息時,操作系統首先接收到該消息,然後把該消息轉發給目標線程,接收消息的線程必須已經建立了消息循環。這裏對自定義消息的使用同使用普通自定義消息非常相似,只
不過消息的發送是在子線程函數中進行的。該方法的主體是自定義消息,應首先定義自定義消息並
添加對消息的響應代碼。
      用PostMessage()或SendMessage()消息傳遞函數.
 
3.使用事件內核對象(相當好用)
     利用事件(Event)內核對象對線程的通信要複雜些,主要通過對事件對象的監視來實現線程間的
通信。事件對象由CreateEvent()函數來創建,具有兩種存在狀態:置位與復位,分別由SetEvent()
和ResetEvent()來產生。事件的置位將通過 WaitForSingleObject()或WaitForMultipleObjects()之類
的通知等待函數繼續執行。
// 事件句柄
HANDLE hEvent = NULL;
UINT ThreadProc7(LPVOID pParam)
{
                 while(true)
                 {
                               // 等待事件發生
                               DWORD dwRet = WaitForSingleObject(hEvent, 0);
                               // 如果事件置位則退出線程,否則將繼續執行
                               if (dwRet == WAIT_OBJECT_0)
                                            break;
                               else
                               {
                                            Sleep(2000);

                                            AfxMessageBox("線程正在運行!");
                               }
                 }
               
                 AfxMessageBox("線程終止運行!");
                 return 0;
}
……
void CSample06View::OnEventStart() 
{
                 // 創建事件   
                 hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
               
                 // 啓動線程
                 AfxBeginThread(ThreadProc7, NULL);
}
void CSample06View::OnEventEnd() 
{
                 // 事件置位
                 SetEvent(hEvent);             
}
     上面這段代碼展示了事件對象在線程通信中的作用。在創建線程前首先創建一個事件對象hEven
t,這裏CreateEvent()函數所採用的四個參數分別表示句柄不能被繼承、事件在置位後將由系統自動
進行復位、事件對象初始狀態爲復位狀態和不指定事件名。在創建的子線程中使用 WaitForSingle
Object()對hEvent進行監視。WaitForSingleObject()的函數原型爲:
                 DWORD WaitForSingleObject(
                               HANDLE hHandle,                             //等待對象的句柄
                               DWORD dwMilliseconds           //超過時間間隔
                 );
     函數將在hHandle對象有信號時或是在等待時間超出由dwMilliseconds設定的超時時間間隔返
回。其返回值可以爲 WAIT_ABANDONED、WAIT_OBJECT_0和WAIT_TIMEOUT,分別表示被
等待的互斥量(Mutex)對象沒有被釋放、等待的對象信號置位和超時。通過對返回值的判斷可以區分
出引起WaitForSingleObject()函數返回的原因。在本例中只關心 WAIT_OBJECT_0的返回值,當
通過SetEvent()將hEvent置位後即可使WaitForSingleObject()立即返回並通過跳出循環而結束線
程。

 

論3:線程之間的通信

  通常情況下,一個次級線程要爲主線程完成某種特定類型的任務,這就隱含着表示在主線程和次級線程之間需要建立一個通信的通道。一般情況下,有下面的幾種方法實現這種通信任務:使用全局變量(上一節的例子其實使用的就是這種方法)、使用事件對象、使用消息。這裏我們主要介紹後兩種方法。

  (一) 利用用戶定義的消息通信

  在Windows程序設計中,應用程序的每一個線程都擁有自己的消息隊列,甚至工作線程也不例外,這樣一來,就使得線程之間利用消息來傳遞信息就變的非常簡單。首先用戶要定義一個用戶消息,如下所示:#define WM_USERMSG WMUSER+100;在需要的時候,在一個線程中調用::PostMessage((HWND)param,WM_USERMSG,0,0)或CwinThread::PostThradMessage()來向另外一個線程發送這個消息,上述函數的四個參數分別是消息將要發送到的目的窗口的句柄、要發送的消息標誌符、消息的參數WPARAM和LPARAM。下面的代碼是對上節代碼的修改,修改後的結果是在線程結束時顯示一個對話框,提示線程結束:

UINT ThreadFunction(LPVOID pParam)
{
 while(!bend)
 {
  Beep(100,100);
  Sleep(1000);
 }
 ::PostMessage(hWnd,WM_USERMSG,0,0);
 return 0;
}
////////WM_USERMSG消息的響應函數爲OnThreadended(WPARAM wParam,
LPARAM lParam)
LONG CTestView::OnThreadended(WPARAM wParam,LPARAM lParam)
{
 AfxMessageBox("Thread ended.");
 Retrun 0;
}
  上面的例子是工作者線程向用戶界面線程發送消息,對於工作者線程,如果它的設計模式也是消息驅動的,那麼調用者可以向它發送初始化、退出、執行某種特定的處理等消息,讓它在後臺完成。在控制函數中可以直接使用::GetMessage()這個SDK函數進行消息分檢和處理,自己實現一個消息循環。GetMessage()函數在判斷該線程的消息隊列爲空時,線程將系統分配給它的時間片讓給其它線程,不無效的佔用CPU的時間,如果消息隊列不爲空,就獲取這個消息,判斷這個消息的內容並進行相應的處理。

  (二)用事件對象實現通信

  在線程之間傳遞信號進行通信比較複雜的方法是使用事件對象,用MFC的Cevent類的對象來表示。事件對象處於兩種狀態之一:有信號和無信號,線程可以監視處於有信號狀態的事件,以便在適當的時候執行對事件的操作。上述例子代碼修改如下:

////////////////////////////////////////////////////////////////////
Cevent threadStart ,threadEnd;
UINT ThreadFunction(LPVOID pParam)
{
 ::WaitForSingleObject(threadStart.m_hObject,INFINITE);
 AfxMessageBox("Thread start.");
 while(!bend)
 {
  Beep(100,100);
  Sleep(1000);
  Int result=::WaitforSingleObject(threadEnd.m_hObject,0);
  //等待threadEnd事件有信號,無信號時線程在這裏懸停
  If(result==Wait_OBJECT_0)
   Bend=TRUE;
 }
 ::PostMessage(hWnd,WM_USERMSG,0,0);
 return 0;
}
/////////////////////////////////////////////////////////////
Void CtestView::OninitialUpdate()
{
 hWnd=GetSafeHwnd();
 threadStart.SetEvent();//threadStart事件有信號
 pThread=AfxBeginThread(ThreadFunction,hWnd);//啓動線程
 pThread->m_bAutoDelete=FALSE;
 Cview::OnInitialUpdate();
}
////////////////////////////////////////////////////////////////
Void CtestView::OnDestroy()
{
 threadEnd.SetEvent();
 WaitForSingleObject(pThread->m_hThread,INFINITE);
 delete pThread;
 Cview::OnDestroy();
}
  運行這個程序,當關閉程序時,才顯示提示框,顯示"Thread ended"。
論4:線程之間的同步

  前面我們講過,各個線程可以訪問進程中的公共變量,所以使用多線程的過程中需要注意的問題是如何防止兩個或兩個以上的線程同時訪問同一個數據,以免破壞數據的完整性。保證各個線程可以在一起適當的協調工作稱爲線程之間的同步。前面一節介紹的事件對象實際上就是一種同步形式。Visual C++中使用同步類來解決操作系統的並行性而引起的數據不安全的問題,MFC支持的個多線程的同步類可以分成兩大類:同步對象(CsyncObject、Csemaphore、Cmutex、CcriticalSection和Cevent)和同步訪問對象(CmultiLock和CsingleLock)。本節主要介紹臨界區(critical section)、互斥(mutexe)、信號量(semaphore),這些同步對象使各個線程協調工作,程序運行起來更安全。

  (一) 臨界區

  臨界區是保證在某一個時間只有一個線程可以訪問數據的方法。使用它的過程中,需要給各個線程提供一個共享的臨界區對象,無論哪個線程佔有臨界區對象,都可以訪問受到保護的數據,這時候其它的線程需要等待,直到該線程釋放臨界區對象爲止,臨界區被釋放後,另外的線程可以強佔這個臨界區,以便訪問共享的數據。臨界區對應着一個CcriticalSection對象,當線程需要訪問保護數據時,調用臨界區對象的Lock()成員函數;當對保護數據的操作完成之後,調用臨界區對象的Unlock()成員函數釋放對臨界區對象的擁有權,以使另一個線程可以奪取臨界區對象並訪問受保護的數據。同時啓動兩個線程,它們對應的函數分別爲WriteThread()和ReadThread(),用以對公共數組組array[]操作,下面的代碼說明了如何使用臨界區對象:

#include "afxmt.h"
int array[10],destarray[10];
CCriticalSection Section;
UINT WriteThread(LPVOID param)
{
 Section.Lock();
 for(int x=0;x<10;x++)
  array[x]=x;
 Section.Unlock();
}
UINT ReadThread(LPVOID param)
{
 Section.Lock();
 For(int x=0;x<10;x++)
  Destarray[x]=array[x];
  Section.Unlock();
}
  上述代碼運行的結果應該是Destarray數組中的元素分別爲1-9,而不是雜亂無章的數,如果不使用同步,則不是這個結果,有興趣的讀者可以實驗一下。

  (二)互斥

  互斥與臨界區很相似,但是使用時相對複雜一些,它不僅可以在同一應用程序的線程間實現同步,還可以在不同的進程間實現同步,從而實現資源的安全共享。互斥與Cmutex類的對象相對應,使用互斥對象時,必須創建一個CSingleLock或CMultiLock對象,用於實際的訪問控制,因爲這裏的例子只處理單個互斥,所以我們可以使用CSingleLock對象,該對象的Lock()函數用於佔有互斥,Unlock()用於釋放互斥。實現代碼如下:

#include "afxmt.h"
int array[10],destarray[10];
CMutex Section;

UINT WriteThread(LPVOID param)
{
 CsingleLock singlelock;
 singlelock (&Section);
 singlelock.Lock();
 for(int x=0;x<10;x++)
  array[x]=x;
 singlelock.Unlock();
}

UINT ReadThread(LPVOID param)
{
 CsingleLock singlelock;
 singlelock (&Section);
 singlelock.Lock();
 For(int x=0;x<10;x++)
  Destarray[x]=array[x];
  singlelock.Unlock();
}
  (三)信號量

  信號量的用法和互斥的用法很相似,不同的是它可以同一時刻允許多個線程訪問同一個資源,創建一個信號量需要用Csemaphore類聲明一個對象,一旦創建了一個信號量對象,就可以用它來對資源的訪問技術。要實現計數處理,先創建一個CsingleLock或CmltiLock對象,然後用該對象的Lock()函數減少這個信號量的計數值,Unlock()反之。下面的代碼分別啓動三個線程,執行時同時顯示二個消息框,然後10秒後第三個消息框才得以顯示。

/////////////////////////////////////////////////////////////////////////
Csemaphore *semaphore;
Semaphore=new Csemaphore(2,2);
HWND hWnd=GetSafeHwnd();
AfxBeginThread(threadProc1,hWnd);
AfxBeginThread(threadProc2,hWnd);
AfxBeginThread(threadProc3,hWnd);
UINT ThreadProc1(LPVOID param)
{
 CsingleLock singelLock(semaphore);
 singleLock.Lock();
 Sleep(10000);
 ::MessageBox((HWND)param,"Thread1 had access","Thread1",MB_OK);
 return 0;
}
UINT ThreadProc2(LPVOID param)
{
 CSingleLock singelLock(semaphore);
 singleLock.Lock();
 Sleep(10000);
 ::MessageBox((HWND)param,"Thread2 had access","Thread2",MB_OK);
 return 0;
}

UINT ThreadProc3(LPVOID param)
{
 CsingleLock singelLock(semaphore);
 singleLock.Lock();
 Sleep(10000);
 ::MessageBox((HWND)param,"Thread3 had access","Thread3",MB_OK);
 return 0;
}

 

VC多線程之線程間的通信 http://www.ibiancheng.cn/Article/VCWin32Article/200802/110.html

論5:線程之間的同步

 相比於進程間通信來說,線程間通信無疑是相對比較簡單的。

   首先我們來看看最簡單的方法,那就是使用全局變量(靜態變量也可以)來進行通信,由於屬於同一個進程的各個線程是處於同一個進程空間中的,並且它們共享這個進程的各種資源,因此它們都可以毫無障礙的訪問這個進程中的全局變量。當需要有多個線程來訪問一個全局變量時,通常我們會在這個全局變量前加上volatile聲明,來告訴編譯器這個全局變量是易變的,讓編譯器不要對這個變量進行優化(至於編譯器到底有沒有按照你的要求來對volatile進行處理這個暫且不理)。

   下面貼出一段簡單的示例代碼:

#include "stdafx.h"
#include 
"windows.h"
#include 
"stdio.h"

volatile int ThreadData = 0;

void ThreadProcess()
{
    
for(int i=0; i<6; i++)
    
{
        ThreadData 
+= 1000;
        Sleep(
1000);
        printf(
"Sub  Thread Tick %5d! %5d\n",(i+1)*1000, ThreadData);
    }

    printf(
"Exit Sub Thread!\n");
    
}


int _tmain(int argc, _TCHAR* argv[])
{
    HANDLE hThread;
    DWORD ThreadID;
    hThread
=CreateThread(NULL,
                     
0,
                     (LPTHREAD_START_ROUTINE)ThreadProcess,
                     NULL,
                     
0,
                     
&ThreadID);
    
    
for(int i=0; i<10; i++)
    
{
        ThreadData 
-= 600;
        Sleep(
600);
        printf(
"Main Thread Tick %5d! %5d\n", (i+1)*600, ThreadData);
    }

    printf(
"Main Thread Loop Finished! \n");
    system(
"pause");
    
return 0;
}

   除了全局變量之外,還有其他的方法,比如利用消息機制等來實現線程間通信。這個就不詳細解釋了,關於消息機制,詳情請看Windows消息機制概述 。 

   下面,關於多線程中的全局變量,我來介紹點有點偏題的東西:
線程局部存儲(TLS)
    進程中的全局變量與函數內定義的靜態(static)變量,是各個線程都可以訪問的共享變量。在一個線程修改的內存內容,對所有線程都生效。這是一個優點也是一個缺點。說它是優點,線程的數據交換變得非常快捷。說它是缺點,一個線程死掉了,其它線程也性命不保; 多個線程訪問共享數據,需要昂貴的同步開銷,也容易造成同步相關的BUG。
  如果需要在一個線程內部的各個函數調用都能訪問、但其它線程不能訪問的變量(被稱爲static memory local to a thread 線程局部靜態變量),就需要新的機制來實現。這就是TLS。
  線程局部存儲在不同的平臺有不同的實現,可移植性不太好。 
  方法一:每個線程創建時系統給它分配一個LPVOID指針的數組(叫做TLS數組),這個數組從C編程角度是隱藏着的不能直接訪問,需要通過一些C API函數調用訪問。首先定義一些DWORD線程全局變量或函數靜態變量,準備作爲各個線程訪問自己的TLS數組的索引變量。一個線程使用TLS時,第一步在線程內調用TlsAlloc()函數,爲一個TLS數組索引變量與這個線程的TLS數組的某個槽(slot)關聯起來,例如獲得一個索引變量:
  global_dwTLSindex=TLSAlloc();
  注意,此步之後,當前線程實際上訪問的是這個TLS數組索引變量的線程內的拷貝版本。也就說,不同線程雖然看起來用的是同名的TLS數組索引變量,但實際上各個線程得到的可能是不同DWORD值。其意義在於,每個使用TLS的線程獲得了一個DWORD類型的線程局部靜態變量作爲TLS數組的索引變量。C/C++原本沒有直接定義線程局部靜態變量的機制,所以在如此大費周折。
  第二步,爲當前線程動態分配一塊內存區域(使用LocalAlloc()函數調用),然後把指向這塊內存區域的指針放入TLS數組相應的槽中(使用TlsValue()函數調用)。
  第三步,在當前線程的任何函數內,都可以通過TLS數組的索引變量,使用TlsGetValue()函數得到上一步的那塊內存區域的指針,然後就可以進行內存區域的讀寫操作了。這就實現了在一個線程內部這個範圍處處可訪問的變量。
  最後,如果不再需要上述線程局部靜態變量,要動態釋放掉這塊內存區域(使用LocalFree()函數),然後從TLS數組中放棄對應的槽(使用TlsFree()函數)。
  方法二:
  直接聲明這個變量是各個線程有自己拷貝的線程局部靜態變量:
  __declspec( thread ) int var_name;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章