Windows編程(多線程)
線程創建函數
CreateThread
CreateThread
是一種微軟在Windows API中提供了建立新的線程的函數,該函數在主線程的基礎上創建一個新線程。線程終止運行後,線程對象仍然在系統中,必須通過CloseHandle
函數來關閉該線程對象。
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
__drv_aliasesMem LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
· 第一個參數 lpThreadAttributes 表示線程內核對象的安全屬性,一般傳入NULL表示使用默認設置。
· 第二個參數 dwStackSize 表示線程棧空間大小。傳入0表示使用默認大小(1MB)。
· 第三個參數 lpStartAddress 表示新線程所執行的線程函數地址,多個線程可以使用同一個函數地址。
· 第四個參數 lpParameter 是傳給線程函數的參數。
· 第五個參數 dwCreationFlags 指定額外的標誌來控制線程的創建,爲0表示線程創建之後立即就可以進行調度,如果爲CREATE_SUSPENDED則表示線程創建後暫停運行,這樣它就無法調度,直到調用ResumeThread()。
· 第六個參數 lpThreadId 將返回線程的ID號,傳入NULL表示不需要返回該線程ID號
#include <Windows.h>
#include <stdio.h>
#include <process.h>
//HANDLE
//WINAPI
//CreateThread(
// _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
// _In_ SIZE_T dwStackSize,
// _In_ LPTHREAD_START_ROUTINE lpStartAddress,
// _In_opt_ __drv_aliasesMem LPVOID lpParameter,
// _In_ DWORD dwCreationFlags,
// _Out_opt_ LPDWORD lpThreadId
//);
DWORD WINAPI ThreadFun(LPVOID p) {
int a = *(int*)p;
printf("我是子線程,PID = %d,iMym = %d\n", GetCurrentThreadId(), a);
return 0;
}
int main() {
HANDLE ZThread;
DWORD dwThreadID;
int a = 100;
ZThread = CreateThread(NULL, 0, &ThreadFun, &a, 0, &dwThreadID);
}
_beginthreadex
_ACRTIMP uintptr_t __cdecl _beginthreadex(
_In_opt_ void* _Security,
_In_ unsigned _StackSize,
_In_ _beginthreadex_proc_type _StartAddress,
_In_opt_ void* _ArgList,
_In_ unsigned _InitFlag,
_Out_opt_ unsigned* _ThrdAddr
);
unsigned WINAPI FUNC(VOID * p) {
int a = *(int*)p;
printf("我是子線程,PID = %d,iMym = %d\n", GetCurrentThreadId(), a);
return 0;
}
int main() {
HANDLE hThread;
unsigned dwThreadID;
int a = 1;
hThread= (HANDLE)_beginthreadex(NULL, 0, FUNC, (void*)&a,0,&dwThreadID);
Sleep(3000);
}
1 理解內核對象
1 定義:
內核對象通過API來創建,每個內核對象是一個數據結構,它對應一塊內存,由操作系統內核分配,並且只能由操作系統內核訪問。在此數據結構中少數成員如安全描述符和使用計數是所有對象都有的,但其他大多數成員都是不同類型的對象特有的。內核對象的數據結構只能由操作系統提供的API訪問,應用程序在內存中不能訪問。調用創建內核對象的函數後,該函數會返回一個句柄,它標識了所創建的對象。它可以由進程的任何線程使用。
CreateProcess
CreateThread
CreateFile
event
Job
Mutex
常見的內核對象 : 進程、線程、文件,存取符號對象、事件對象、文件對象、作業對象、互斥對象、管道對象、等待計時器對象,郵件槽對象,信號對象
內核對象:爲了管理線程/文件等資源而由操作系統創建的數據塊。
其創建的所有者肯定是操作系統。
WaitForSingleObject
等待指定對象處於信號狀態或超時間隔結束。
DWORD WaitForSingleObject(
HANDLE hHandle,//指明一個內核對象的句柄
DWORD dwMilliseconds //等待時間
);
hHandle:
對象的句柄。有關可以指定句柄的對象類型的列表,請參閱以下備註部分。
如果此句柄在等待仍處於掛起狀態時關閉,則函數的行爲未定義。
句柄必須具有SYNCHRONIZE訪問權限。有關更多信息,請參閱 標準訪問權限。
dwMilliseconds:
超時間隔,以毫秒爲單位。如果指定了非零值,則函數會等待,直到對象發出信號或間隔結束。如果dwMilliseconds爲零,如果對象沒有發出信號,函數不會進入等待狀態;它總是立即返回。如果dwMilliseconds是INFINITE
,則該函數將僅在對象收到信號時返回。
int main()
{
printf("main begin\n");
int iParam = 5;
unsigned int dwThreadID;
DWORD wr;
HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFun,
(void*)&iParam, 0, &dwThreadID);
if (hThread == NULL)
{
puts("_beginthreadex() error");
return -1;
}
//
printf("WaitForSingleObject begin\n");
wr = WaitForSingleObject(hThread, INFINITE);
/*if ((wr = WaitForSingleObject(hThread, INFINITE)) == WAIT_FAILED)
{
puts("thread wait error");
return -1;
}*/
printf("WaitForSingleObject end\n");
printf("main end\n");
system("pause");
return 0;
}
線程同步
互斥對象
互斥對象(mutex)屬於內核對象,它能夠確保線程擁有對單個資源的互斥訪問權。
互斥對象包含一個使用數量,一個線程ID和一個計數器。其中線程ID用於標識系統中的哪個線程當前擁有互斥對象,計數器用於指明該線程擁有互斥對象的次數。
創建互斥對象:調用函數CreateMutex。調用成功,該函數返回所創建的互斥對象的句柄。
請求互斥對象所有權:調用函數WaitForSingleObject函數。線程必須主動請求共享對象的所有權才能獲得所有權。
釋放指定互斥對象的所有權:調用ReleaseMutex函數。線程訪問共享資源結束後,線程要主動釋放對互斥對象的所有權,使該對象處於已通知狀態。
CreateMutex
HANDLE
WINAPI
CreateMutexW(
_In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes, //指向安全屬性
_In_ BOOL bInitialOwner, //初始化互斥對象的所有者 TRUE 立即擁有互斥體,false表示創建的這個mutex不屬於任何線程;所以處於激發狀態,也就是有信號狀態
_In_opt_ LPCWSTR lpName //指向互斥對象名的指針 L“Bingo”
);
WaitForMultipleObjects 函數
DWORD WaitForMultipleObjects(
DWORD nCount,
const HANDLE *lpHandles,
BOOL bWaitAll,
DWORD dwMilliseconds
);
參數
nCount:
lpHandles指向的數組中的對象句柄數。對象句柄的最大數量是MAXIMUM_WAIT_OBJECTS。此參數不能爲零。
lpHandles:
對象句柄數組。有關可以指定句柄的對象類型的列表,請參閱以下備註部分。該數組可以包含不同類型對象的句柄。它可能不包含同一句柄的多個副本。
如果這些句柄之一在等待仍然掛起時關閉,則函數的行爲是未定義的。
句柄必須具有SYNCHRONIZE訪問權限。有關更多信息,請參閱 標準訪問權限。
bWaitAll:
如果此參數爲TRUE,則當lpHandles數組中的所有對象的狀態發出信號時,該函數返回。如果爲FALSE,則當任何一個對象的狀態設置爲有信號時,該函數返回。在後一種情況下,返回值指示其狀態導致函數返回的對象。
dwMilliseconds:
超時間隔,以毫秒爲單位。如果指定了非零值,則函數將等待,直到指定的對象發出信號或間隔過去。如果dwMilliseconds爲零,如果指定的對象沒有發出信號,函數不會進入等待狀態;它總是立即返回。如果dwMilliseconds是INFINITE,則該函數將僅在指定對象發出信號時返回。
視窗XP,Windows Server 2003和Windows Vista中,Windows 7和Windows Server 2008和Windows Server 2008 R2 的dwMilliseconds值不包括在低功耗狀態所花費的時間。例如,當計算機處於睡眠狀態時,超時確實會繼續倒計時。
Windows 8中,Windows Server 2012中的Windows 8.1,Windows Server 2012中R2中,Windows 10和Windows Server 2016 的dwMilliseconds值不包括在低功耗狀態所花費的時間。例如,當計算機處於睡眠狀態時,超時不會一直倒計時。
#include <Windows.h>
#include <stdio.h>
#include <process.h>
HANDLE hMutex;
#define NUM_THREAD 50//線程數
long long num = 0;
unsigned WINAPI ThreadFun1(void* arg)
{
int i;
WaitForSingleObject(hMutex, INFINITE);
for (i = 0; i < 500000; i++)
num += 1;
ReleaseMutex(hMutex);
return 0;
}
unsigned WINAPI ThreadFun2(void* arg)
{
int i;
WaitForSingleObject(hMutex, INFINITE);
for (i = 0; i < 500000; i++)
num -= 1;
ReleaseMutex(hMutex);
return 0;
}
int main()
{
printf("main begin\n");
int iParam = 5;
hMutex = CreateMutexW(NULL, FALSE, 0);
HANDLE tHandles[NUM_THREAD];
for (size_t i = 0; i < NUM_THREAD; i++)
{
if (i % 2)
tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, ThreadFun1, NULL, 0, NULL);
else
tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, ThreadFun2, NULL, 0, NULL);
}
printf("WaitForSingleObject begin\n");
WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);
printf("WaitForSingleObject end\n");
printf("result: %lld \n", num);
printf("main end\n");
system("pause");
return 0;
}
這裏建立2個方法,使用50個線程調度。一個方法對num值進行加一,一個方法對num進行減一處理。
來驗證結果線程鎖是否起作用。
事件對象
事件對象也屬於內核對象,它包含以下三個成員:
● 使用計數;
● 用於指明該事件是一個自動重置的事件還是一個人工重置的事件的布爾值;
● 用於指明該事件處於已通知狀態還是未通知狀態的布爾值。
事件對象有兩種類型:人工重置的事件對象和自動重置的事件對象。這兩種事件對象的區別在於當人工重置的事件對象得到通知時,等待該事件對象的所有線程均變爲可調度線程;而當一個自動重置的事件對象得到通知時,等待該事件對象的線程中只有一個線程變爲可調度線程。
- 創建事件對象
調用CreateEvent
函數創建或打開一個命名的或匿名的事件對象。
- 設置事件對象狀態
調用SetEvent
函數把指定的事件對象設置爲有信號狀態。
- 重置事件對象狀態
調用ResetEvent
函數把指定的事件對象設置爲無信號狀態。
- 請求事件對象
線程通過調用WaitForSingleObject
函數請求事件對象。
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全屬性
BOOL bManualReset, // 復位方式 TRUE 必須用ResetEvent手動復原 FALSE 自動還原爲無信號狀態
BOOL bInitialState, // 初始狀態 TRUE 初始狀態爲有信號狀態 FALSE 無信號狀態
LPCTSTR lpName //對象名稱 NULL 無名的事件對象
);
#include <Windows.h>
#include <stdio.h>
#include <process.h>
#define STR_LEN 100
static char str[STR_LEN];
static HANDLE hEvent;
unsigned WINAPI NumberOfA(void* arg);
unsigned WINAPI NumberOfOthers(void* arg);
unsigned WINAPI NumberOfA(void* arg)
{
int i, cnt = 0;
//再沒有執行fputs("Input string: ", stdout);
//fgets(str, STR_LEN, stdin);SetEvent(hEvent);之前,卡在
//WaitForSingleObject
WaitForSingleObject(hEvent, INFINITE);
for (i = 0; str[i] != 0; i++)
{
if (str[i] == 'A')
cnt++;
}
printf("Num of A: %d \n", cnt);
return 0;
}
unsigned WINAPI NumberOfOthers(void* arg)
{
int i, cnt = 0;
for (i = 0; str[i] != 0; i++)
{
if (str[i] != 'A')
cnt++;
}
printf("Num of others: %d \n", cnt - 1);
//把事件對象設置爲有信號狀態
SetEvent(hEvent);
return 0;
}
int main() {
HANDLE hThread1, hThread2;
fputs("Input string: ", stdout);
fgets(str, STR_LEN, stdin);
hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
hThread1 = (HANDLE)_beginthreadex(NULL, 0, NumberOfA, NULL,0 ,NULL);
hThread2 = (HANDLE)_beginthreadex(NULL, 0, NumberOfOthers, NULL,0 ,NULL);
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
ResetEvent(hEvent);
CloseHandle(hEvent);
return 0;
}
關鍵代碼段
調用InitializeCriticalSection函數初始化一個關鍵代碼段。
InitializeCriticalSection(
_Out_ LPCRITICAL_SECTION lpCriticalSection
);
該函數只有一個指向CRITICAL_SECTION結構體的指針。在調用InitializeCriticalSection函數之前,首先需要構造一個CRITICAL_SECTION結構體類型的對象,然後將該對象的地址傳遞給InitializeCriticalSection函數。
- 初始化關鍵代碼段
調用InitializeCriticalSection函數初始化一個關鍵代碼段。
InitializeCriticalSection(
_Out_ LPCRITICAL_SECTION** lpCriticalSection
);
該函數只有一個指向CRITICAL_SECTION
結構體的指針。在調用InitializeCriticalSection
函數之前,首先需要構造一個CRITICAL_SECTION結構體類型的對象,然後將該對象的地址傳遞給InitializeCriticalSection
函數。
進入關鍵代碼段
VOID
WINAPI
EnterCriticalSection(
_Inout_ LPCRITICAL_SECTION lpCriticalSection
);
調用EnterCriticalSection函數,以獲得指定的臨界區對象的所有權,該函數等待指定的臨界區對象的所有權,如果該所有權賦予了調用線程,則該函數就返回;否則該函數會一直等待,從而導致線程等待。
- 退出關鍵代碼段
VOID
WINAPI
LeaveCriticalSection(
_Inout_ LPCRITICAL_SECTION lpCriticalSection);
線程使用完臨界區所保護的資源之後,需要調用LeaveCriticalSection函數,釋放指定的臨界區對象的所有權。之後,其他想要獲得該臨界區對象所有權的線程就可以獲得該所有權,從而進入關鍵代碼段,訪問保護的資源。
刪除臨界區
WINBASEAPI
VOID
WINAPI
DeleteCriticalSection(
_Inout_ LPCRITICAL_SECTION lpCriticalSection
);
當臨界區不再需要時,可以調用DeleteCriticalSection
函數釋放該對象,該函數將釋放一個沒有被任何線程所擁有的臨界區對象的所有資源。
#include <stdio.h>
#include <windows.h>
#include <process.h>
int iTickets = 5000;
CRITICAL_SECTION g_cs;
// A窗口 B窗口
DWORD WINAPI SellTicketA(void* lpParam)
{
while (1)
{
EnterCriticalSection(&g_cs);//進入臨界區
if (iTickets > 0)
{
Sleep(1);
iTickets--;
printf("A remain %d\n", iTickets);
LeaveCriticalSection(&g_cs);//離開臨界區
}
else
{
LeaveCriticalSection(&g_cs);//離開臨界區
break;
}
}
return 0;
}
DWORD WINAPI SellTicketB(void* lpParam)
{
while (1)
{
EnterCriticalSection(&g_cs);//進入臨界區
if (iTickets > 0)
{
Sleep(1);
iTickets--;
printf("B remain %d\n", iTickets);
LeaveCriticalSection(&g_cs);//離開臨界區
}
else
{
LeaveCriticalSection(&g_cs);//離開臨界區
break;
}
}
return 0;
}
int main()
{
HANDLE hThreadA, hThreadB;
InitializeCriticalSection(&g_cs); //初始化關鍵代碼段
hThreadA = CreateThread(NULL, 0, SellTicketA, NULL, 0, NULL); //2
hThreadB = CreateThread(NULL, 0, SellTicketB, NULL, 0, NULL); //2
CloseHandle(hThreadA); //1
CloseHandle(hThreadB); //1
Sleep(40000);
DeleteCriticalSection(&g_cs);//刪除臨界區
system("pause");
return 0;
}