- 在并行程度中,當兩個并行的線程,在沒有任何限制的情況下,通路一個共享變量或者共享對象的一個域,而且至少要有一個操作是寫操作,就可能發生資料競争錯誤。
- 原語Compare-and-swap(CAS)是實作無鎖資料結構的通用原語。
- 獲得内部鎖的唯一途徑是:進入這個内部鎖保護的同步塊或方法。
- volatile變量具有synchronized的可見性特性,但是不具備原子特性。
- 減小競争發生可能性的有效方式是盡可能縮短把持鎖的時間
1 基本概念
計算機程序:
在計算機作業系統中,程序是指當可執行檔案運作時,系統所建立的核心對象。
計算機線程:
線程是計算機中最小的執行單元。
同步:
不同程序間的若幹程式段,它們的運作必須嚴格按照規定的某種次序來運作,這種先後次序依賴于要完成的任務。如果用對資源的通路來定義的話,同步是指在互斥的基礎上,通過其它機制實作通路者對資源的有序通路。
互斥:
散布在不同程序之間的若幹程式片段,當某個程序運作其中一個程式片時,其它程序就不能運作它們之中的任一程式片段,隻能等到該程序運作完這個程式片段後才能運作。如果用對資源的通路來定義的話,互斥某一資源隻能允許一個通路者對其進行通路,具有唯一性和拍它性。互斥無法限制通路者對資源的通路順序,即通路是無序的。
2 線程間的同步方法:
使用者模式和核心模式:
核心模式是指利用系統核心對象的單一性來進行同步,使用時需要切換核心态和使用者态,而使用者模式就是不用切換到核心态,隻在使用者态完成的操作。
使用者态模式下的方法有:原子操作(例如一個單一的全局變量),臨界區。
核心模式下的方法有:事件、信号量、互斥量。
1 臨界區:
通過對多線程的串行化來通路公共資源或一段代碼,速度快,适合控制資料通路。
2 互斥量:
為協調共同對一個共享資源的單獨通路而設計的。
3 信号量:
為控制一個具有有限數量使用者資源而設計
4 事件:
用來通知線程有一些事件已經發生,進而啟動後繼任務。
3 程序間的通信方式:
1 管道及有名管道:
管道可用于具有親緣關系的父子程序間的通信,有名管道除了具有管道所具有的功能外,它還允許無親緣關系的程序間通信。
2 信号:
信号是在軟體層次上對中斷機制的一種模拟,它是比較複雜的通信方式,用于通知程序某事發生了,一個程序收到一個信号與處理器收到一個中斷請求效果上可以說是一緻的。
3 消息隊列:
消息隊列是消息的連結表,它克服了上兩種通信方式中信号量有限的缺點,具有寫權限的程序可以按照一定的規則向消息隊列添加新資訊,對消息隊列具有讀權限的程序則可以從消息隊列中讀資訊。
4 共享記憶體:
可以說這是最有用的程序間通信方式。它使得多個程序可以通路同一塊記憶體空間,不同程序可以及時看到對象程序中對共享記憶體中的資料更新情況。這種方法需要依靠某種同步操作,如互斥鎖和信号量等。
5 信号量
主要作為程序之間以及同一程序的不同線程之間的同步或互斥手段。
6 套接字
這是一種更為一般的程序間通信方式,它可以用于網絡中的不同機器之間的程序間通信,應用非常廣泛。
4 線程同步方法代碼執行個體:
1 使用C++标準庫的thread,mutex頭檔案
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
void Fun_1(int &iCounter)
{
while (true)
{
std::lock_guard<std::mutex>mtx_locker(mtx);
iCounter++;
if (iCounter < )
{
std::cout << "Fun_1()->" << iCounter << std::endl;
}
else
{
break;
}
}
}
void Fun_2(int &iCounter)
{
while (true)
{
std::lock_guard<std::mutex>mtx_locker(mtx);
iCounter++;
if (iCounter < )
{
std::cout << "Fun_2()->" << iCounter << std::endl;
}
else
{
break;
}
}
}
int main()
{
int iCounter = ;
std::thread thread1(Fun_1, std::ref(iCounter));
std::thread thread2(Fun_2, std::ref(iCounter));
thread1.join();
thread2.join();
system("pause");
return ;
}
以上代碼通過構造std::mutex的執行個體來建立互斥元,标準庫提供了std::lock_guard類模闆,實作了互斥元的RALL慣用法(資源擷取及初始化)。該對象在構造時鎖定所給的互斥元,析構時解鎖該互斥元,進而保證被鎖定的互斥元始終被正确解鎖。
2 使用WindowsAPI的臨界區對象:
// MultiThread.h
#ifndef _MultiThread_H_
#define _MultiThread_H_
#include <windows.h>
class RAII_CrtcSec
{
private:
CRITICAL_SECTION crtc_sec;
public:
RAII_CrtcSec()
{
::InitializeCriticalSection(&crtc_sec);
}
~RAII_CrtcSec()
{
::DeleteCriticalSection(&crtc_sec);
}
RAII_CrtcSec(const RAII_CrtcSec&) = delete;
RAII_CrtcSec& operator=(const RAII_CrtcSec&) = delete;
void Lock()
{
::EnterCriticalSection(&crtc_sec);
}
void Unlock()
{
::LeaveCriticalSection(&crtc_sec);
}
};
#endif // _MultiThread_H_
// main.cpp
#include <windows.h>
#include <iostream>
#include "MultiThread.h"
DWORD WINAPI Fun_1(LPVOID p);
DWORD WINAPI Fun_2(LPVOID p);
int iCounter = ;
RAII_CrtcSec cs;
int main()
{
HANDLE h1 = CreateThread(nullptr, , Fun_1, nullptr, , );
HANDLE h2 = CreateThread(nullptr, , Fun_2, nullptr, , );
CloseHandle(h1);
CloseHandle(h2);
system("pause");
return ;
}
DWORD WINAPI Fun_1(LPVOID p)
{
while (true)
{
cs.Lock();
++iCounter;
if (iCounter < )
{
std::cout << "Fun_1()->" << iCounter << std::endl;
}
else
{
break;
}
cs.Unlock();
}
return ;
}
DWORD WINAPI Fun_2(LPVOID p)
{
while (true)
{
cs.Lock();
++iCounter;
if (iCounter < )
{
std::cout << "Fun_2()->" << iCounter << std::endl;
}
else
{
break;
}
cs.Unlock();
}
return ;
}
上面的代碼使用了Windows API中的臨界區對象來實作線程同步。臨界區是指一個通路共享資源的代碼段,臨界區對象則是指當使用者使用某個線程通路共享資源時,必須使代碼段獨占資源,不允許其它線程通路該資源。在該線程通路資源後,其它線程才能通路該資源。Windows API提供了臨界區對象的結構體CRITICAL_SECTION,對該對象的使用可以總結為如下幾步:
- InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection),該函數的作用是初始化臨界區,唯一的參數是指向結構體CRITICAL_SECTION的指針變量。
- EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection),該函數的作用是使調用該函數的線程進入已經初始話的臨界區,并擁有該臨界區的所有權。這是一個阻塞函數,如果線程獲得臨界區的所有權成功,則該函數将傳回,調用線程繼續執行,否則該函數将一直等待,這樣會導緻該函數的線程也一直等待。如果不想要調用線程等待(非阻塞),則應該使用TryEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection)。
- LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection),該函數的作用是使調用該函數的線程離開臨界區并釋放該臨界區的所有權,以便讓其它線程也獲得通路該共享資源的機會。一定要在程式不适用臨界區的時候調用該函數,釋放臨界區所有權,否則程式将一直等待造成程式假死。
- DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection),該函數的作用是删除程式中已經初始化的臨界區。如果函數調用成功,則程式會将記憶體中的臨界區删除,防止出現記憶體錯誤。
3 使用WIndows API的事件對象
#include <windows.h>
#include <iostream>
DWORD WINAPI Fun_1(LPVOID p);
DWORD WINAPI Fun_2(LPVOID p);
int iCounter = ;
HANDLE h_Event;
int main()
{
h_Event = CreateEvent(nullptr, false, true, nullptr);
SetEvent(h_Event);
HANDLE h1 = CreateThread(nullptr, , Fun_1, nullptr, , nullptr);
HANDLE h2 = CreateThread(nullptr, , Fun_2, nullptr, , nullptr);
CloseHandle(h1);
CloseHandle(h2);
system("pause");
return ;
}
DWORD WINAPI Fun_1(LPVOID p)
{
while (true)
{
WaitForSingleObject(h_Event, INFINITE);
ResetEvent(h_Event);
if (iCounter < )
{
++iCounter;
std::cout << "Fun_1()->" << iCounter << std::endl;
SetEvent(h_Event);
}
else
{
break;
SetEvent(h_Event);
}
}
return ;
}
DWORD WINAPI Fun_2(LPVOID p)
{
while (true)
{
WaitForSingleObject(h_Event, INFINITE);
ResetEvent(h_Event);
if (iCounter < )
{
++iCounter;
std::cout << "Fun_2()->" << iCounter << std::endl;
SetEvent(h_Event);
}
else
{
break;
SetEvent(h_Event);
}
}
return ;
}
事件對象是一種核心對象,使用者在程式中使用核心對象的有無信号狀态來實作線程同步。使用事件對象的步驟可包括如下:
- 建立事件對象,函數原型為:
HANDLE WINAPI CreateEvent( _In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes, _In_ BOOL bManualReset, _In_ BOOL bInitialState, _In_opt_ LPCSTR lpName);
如果函數調用成功,則傳回新建立的事件對象,否則傳回NULL。函數參數的含義如下:
-lpEventAttributes:表示建立的事件對象的安全屬性,若設為NULL,則表示該程式使用的是預設的安全屬性。
-bManualReset:表示所建立的事件對象是人工重置還是自動重置。若為true則表示使用人工重置,在調用線程獲得事件對象所有權後使用者顯示地調用ResetEvent()将對象設定為無信号狀态。
-bInitialState:表示事件對象的初始狀态。若為true,則表示該對象初始時有信号狀态,則線程可以使用事件對象。
-lpName:表示事件對象的名稱,若為NULL,則表示建立的是匿名事件對象。
2. 若事件對象初始狀态設定為無信号,則需調用SetEvent(HANDLE hEvent)将其設定為無信号狀态。ResetEvent(HANDLE hEvent)則用于将事件對象設定為無信号狀态。
3. 線程通過調用WaitForSingleObject()主動請求事件對象,該函數原型如下:
該函數将在使用者指定的事件對象上等待。如果事件對象處于有信号狀态,函數将傳回。否則函數将一直等待,知道使用者所制定的事件到達。DWORD WINAPI WaitForSingleObject( _In_ HANDLE hHandle, _In_ DWORD dwMilliseconds);
4 使用Windows API的互斥對象:
#include <windows.h>
#include <iostream>
DWORD WINAPI Fun_1(LPVOID p);
DWORD WINAPI Fun_2(LPVOID p);
HANDLE h_Mutex;
int iCounter = ;
int main()
{
h_Mutex = CreateMutex(nullptr, false, nullptr);
HANDLE h1 = CreateThread(nullptr, , Fun_1, nullptr, , nullptr);
HANDLE h2 = CreateThread(nullptr, , Fun_2, nullptr, , nullptr);
CloseHandle(h1);
CloseHandle(h2);
system("pause");
return ;
}
DWORD WINAPI Fun_1(LPVOID p)
{
while (true)
{
WaitForSingleObject(h_Mutex, INFINITE);
if (iCounter < )
{
++iCounter;
std::cout << "Fun_1()->" << iCounter << std::endl;
ReleaseMutex(h_Mutex);
}
else
{
ReleaseMutex(h_Mutex);
break;
}
}
return ;
}
DWORD WINAPI Fun_2(LPVOID p)
{
while (true)
{
WaitForSingleObject(h_Mutex, INFINITE);
if (iCounter < )
{
++iCounter;
std::cout << "Fun_2()->" << iCounter << std::endl;
ReleaseMutex(h_Mutex);
}
else
{
ReleaseMutex(h_Mutex);
break;
}
}
return ;
}
互斥對象的使用方法和C++标準庫的mutex類似,互斥對象使用完記得釋放。