一、前言
線程是比進程更輕量級的執(zhí)行單元,允許在一個進程中并發(fā)執(zhí)行多個控制流。每一個線程都有自己的程序計數(shù)器、寄存器集和棧空間,但它們共享所屬進程的全局數(shù)據(jù)和資源。這種共享內(nèi)存模型使線程間的通信比進程間通信更為高效,同時也帶來了潛在的同步問題,如死鎖和競態(tài)條件,需要通過適當?shù)耐綑C制來解決。
在程序設計中,線程能夠顯著提高程序的響應速度和資源利用率,特別是在處理CPU密集型或IO密集型任務時。例如,一個圖形用戶界面(GUI)應用程序可以使用一個線程處理用戶輸入,而另一個線程執(zhí)行耗時的計算或網(wǎng)絡請求,這樣可以避免UI凍結,保持良好的用戶體驗。線程還常用于實現(xiàn)并行算法,加快大數(shù)據(jù)處理、圖像渲染等任務的執(zhí)行速度。
在Windows環(huán)境下,C語言可以通過調(diào)用Win32 API中的CreateThread
函數(shù)來創(chuàng)建和管理線程。CreateThread
函數(shù)允許你指定線程的入口點(即線程函數(shù))、線程的優(yōu)先級、堆棧大小等參數(shù)。
以下是一個使用CreateThread
函數(shù)創(chuàng)建線程的簡單示例:
#include <windows.h>
#include <stdio.h>
// 線程函數(shù)
DWORD WINAPI ThreadFunction(LPVOID lpParam)
{
int id = *(int *)lpParam;
printf("Hello from thread %dn", id);
return 0;
}
int main()
{
HANDLE hThread;
DWORD threadID;
int threadParameter = 1;
// 創(chuàng)建線程
hThread = CreateThread(
NULL, // 默認的安全屬性
0, // 使用默認堆棧大小
ThreadFunction, // 線程函數(shù)
&threadParameter, // 傳遞給線程函數(shù)的參數(shù)
0, // 創(chuàng)建標志,0表示立即啟動
&threadID); // 返回線程ID
if (hThread == NULL)
{
printf("Error creating thread. Error code: %dn", GetLastError());
return 1;
}
// 等待線程結束
WaitForSingleObject(hThread, INFINITE);
// 關閉線程句柄
CloseHandle(hThread);
return 0;
}
在這個示例中,CreateThread
函數(shù)接收多個參數(shù),包括一個線程函數(shù)指針、一個指向線程參數(shù)的指針、線程的創(chuàng)建標志等。當線程創(chuàng)建成功后,CreateThread
函數(shù)返回一個句柄,這個句柄可以用于后續(xù)的線程控制操作,如等待線程結束、終止線程或查詢線程狀態(tài)。
通過這種方式,C語言程序員可以在Windows平臺上利用多線程編程,有效地提高程序性能和響應能力,同時解決復雜的問題域。多線程編程同時也帶來了同步和死鎖等問題,需要開發(fā)者采用合適的同步機制,如互斥量、信號量、臨界區(qū)等,以確保線程安全和程序的正確性。
二、實操案例
2.1 CreateThread函數(shù)
CreateThread
函數(shù)是Windows API中用于創(chuàng)建新線程的核心函數(shù)。在C或C++語言中,可以從一個現(xiàn)有的進程中啟動一個新的執(zhí)行流。
下面詳細介紹了CreateThread
函數(shù)的原型和每個參數(shù)的意義:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 線程安全性屬性
SIZE_T dwStackSize, // 線程堆棧大小
LPTHREAD_START_ROUTINE lpStartAddress, // 線程函數(shù)的入口點
LPVOID lpParameter, // 傳遞給線程函數(shù)的參數(shù)
DWORD dwCreationFlags, // 創(chuàng)建線程的標志
LPDWORD lpThreadId // 輸出參數(shù),接收線程ID
);
- lpThreadAttributes: 是一個指向
SECURITY_ATTRIBUTES
結構的指針,用于指定線程的安全屬性,比如權限和安全描述符。如果你不需要特別的安全設置,通??梢詡鬟fNULL
。 - dwStackSize: 是一個
SIZE_T
類型的值,用來指定新線程的堆棧大?。ㄒ宰止?jié)為單位)。如果設置為0
,則使用系統(tǒng)的默認堆棧大小。 - lpStartAddress: 是一個
LPTHREAD_START_ROUTINE
類型的指針,指向線程的起始函數(shù)。這是一個回調(diào)函數(shù),當線程開始執(zhí)行時會被調(diào)用。這個函數(shù)的原型通常如下:DWORD WINAPI ThreadFunction(LPVOID lpParameter);
其中
lpParameter
是在CreateThread
調(diào)用中傳遞的參數(shù)。 - lpParameter: 是一個
LPVOID
類型的指針,可以用來向線程函數(shù)傳遞參數(shù)。這個參數(shù)會被直接傳遞給lpStartAddress
所指向的函數(shù)。 - dwCreationFlags: 是一個
DWORD
類型的值,用于指定線程創(chuàng)建的標志。常見的標志包括:0
: 立即開始執(zhí)行線程。CREATE_SUSPENDED
: 創(chuàng)建線程但不立即執(zhí)行它。線程處于掛起狀態(tài),可以通過ResumeThread
函數(shù)恢復執(zhí)行。
- lpThreadId: 是一個指向
DWORD
類型的指針,CreateThread
成功創(chuàng)建線程后,會將線程的唯一標識符(ID)寫入這個指針所指向的位置。這個ID可以用于后續(xù)的線程管理和控制。
CreateThread
函數(shù)的返回值是一個HANDLE
類型的值,這是新創(chuàng)建線程的句柄。這個句柄可以用于后續(xù)的線程控制操作,比如WaitForSingleObject
(等待線程結束)、TerminateThread
(終止線程)或ResumeThread
(恢復掛起的線程)。
一旦線程完成執(zhí)行,或被終止,線程對象仍然存在,直到CloseHandle
函數(shù)被調(diào)用來釋放它。因此,在使用CreateThread
創(chuàng)建線程后,記得在適當?shù)臅r候調(diào)用CloseHandle
來清理資源。
2.2 案例1:創(chuàng)建多個線程同時運行
開發(fā)環(huán)境:在Windows下安裝一個VS即可。我當前采用的版本是VS2020。
在C語言中使用多線程,尤其是使用Windows API進行多線程編程,涉及創(chuàng)建和管理多個線程來并發(fā)執(zhí)行任務。
下面代碼,演示了如何在C語言中創(chuàng)建多個線程,并讓它們同時運行,每個線程執(zhí)行簡單的打印操作。此代碼將創(chuàng)建五個線程,每個線程都會打印一條消息。
#include <windows.h>
#include <stdio.h>
// 線程函數(shù)
DWORD WINAPI PrintMessage(LPVOID lpParam)
{
int id = (int)lpParam;
printf("Hello from thread ID: %dn", id);
return 0;
}
int main()
{
HANDLE hThreads[5]; // 數(shù)組用于保存所有線程的句柄
DWORD threadIDs[5]; // 數(shù)組用于保存所有線程的ID
// 創(chuàng)建五個線程
for (int i = 0; i < 5; i++)
{
hThreads[i] = CreateThread(
NULL, // 默認安全屬性
0, // 使用默認堆棧大小
PrintMessage, // 線程函數(shù)
(LPVOID)(i + 1), // 傳遞給線程函數(shù)的參數(shù)
0, // 創(chuàng)建標志,0表示立即啟動
&threadIDs[i]); // 返回線程ID
if (hThreads[i] == NULL)
{
printf("Failed to create thread %d.n", i);
return 1;
}
}
// 等待所有線程結束
for (int i = 0; i < 5; i++)
{
WaitForSingleObject(hThreads[i], INFINITE);
}
// 關閉所有線程句柄
for (int i = 0; i < 5; i++)
{
CloseHandle(hThreads[i]);
}
return 0;
}
在這段代碼中,PrintMessage
函數(shù)是每個線程將要執(zhí)行的任務。它接收一個LPVOID
類型的參數(shù),這個參數(shù)是在CreateThread
函數(shù)中傳遞的。在這個例子中,我們傳遞了一個整數(shù)i+1
作為參數(shù),這使得每個線程都有一個唯一的ID。
main
函數(shù)中,我們使用一個循環(huán)來創(chuàng)建五個線程。每個線程的句柄被存儲在hThreads
數(shù)組中,而每個線程的ID則存儲在threadIDs
數(shù)組中。CreateThread
函數(shù)的最后一個參數(shù)&threadIDs[i]
是一個指向數(shù)組元素的指針,用于接收新創(chuàng)建線程的ID。
在所有線程創(chuàng)建完畢后,再次使用一個循環(huán)來等待所有線程結束。WaitForSingleObject
函數(shù)用于阻塞當前線程,直到指定的線程結束。由于我們使用INFINITE
作為超時值,這意味著WaitForSingleObject
將一直等待,直到指定的線程確實結束。
最后,使用另一個循環(huán)來關閉所有線程的句柄,這是必要的資源清理步驟,以避免資源泄漏。
2.3 案例2:多線程處理并發(fā)處理網(wǎng)絡請求
開發(fā)環(huán)境:在Windows下安裝一個VS即可。我當前采用的版本是VS2020。
創(chuàng)建一個使用子線程并發(fā)處理客戶端連接的TCP服務器是一個典型的多線程編程場景。以下是一個使用C語言和Windows Socket API(Winsock)的示例代碼,展示了如何創(chuàng)建一個TCP服務器,該服務器在接收到客戶端連接時,為每個客戶端創(chuàng)建一個子線程來處理通信。
以下是一個示例:
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <stdio.h>
#include <string.h>
#pragma comment(lib, "ws2_32.lib")
#define SERVER_PORT 27015
#define BUFFER_SIZE 1024
// 子線程函數(shù),用于處理客戶端連接
DWORD WINAPI ClientHandler(LPVOID clientSocket)
{
SOCKET sock = (SOCKET)clientSocket;
char buffer[BUFFER_SIZE];
int bytesReceived;
while ((bytesReceived = recv(sock, buffer, BUFFER_SIZE, 0)) > 0)
{
buffer[bytesReceived] = '?';
printf("Received from client: %sn", buffer);
send(sock, buffer, bytesReceived, 0);
}
if (bytesReceived == SOCKET_ERROR)
{
printf("recv failed with error: %dn", WSAGetLastError());
}
else if (bytesReceived == 0)
{
printf("Client disconnectedn");
}
closesocket(sock);
return 0;
}
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET serverSocket;
struct addrinfo hints, *result, *ptr;
int iResult;
HANDLE hThread;
// 初始化Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0)
{
printf("WSAStartup failed: %dn", iResult);
return 1;
}
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
hints.ai_flags = AI_PASSIVE;
// 解析服務器地址和端口
iResult = getaddrinfo(NULL, "27015", &hints, &result);
if (iResult != 0)
{
printf("getaddrinfo failed: %dn", iResult);
WSACleanup();
return 1;
}
// 創(chuàng)建服務器套接字
ptr = result;
serverSocket = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);
if (serverSocket == INVALID_SOCKET)
{
printf("socket failed with error: %ldn", WSAGetLastError());
freeaddrinfo(result);
WSACleanup();
return 1;
}
// 綁定套接字
iResult = bind(serverSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
if (iResult == SOCKET_ERROR)
{
printf("bind failed with error: %dn", WSAGetLastError());
freeaddrinfo(result);
closesocket(serverSocket);
WSACleanup();
return 1;
}
freeaddrinfo(result);
// 開始監(jiān)聽
iResult = listen(serverSocket, SOMAXCONN);
if (iResult == SOCKET_ERROR)
{
printf("listen failed with error: %dn", WSAGetLastError());
closesocket(serverSocket);
WSACleanup();
return 1;
}
printf("Server is ready to accept connections...n");
while (1)
{
SOCKET clientSocket = accept(serverSocket, NULL, NULL);
if (clientSocket == INVALID_SOCKET)
{
printf("accept failed: %dn", WSAGetLastError());
break;
}
// 創(chuàng)建子線程來處理客戶端連接
hThread = CreateThread(NULL, 0, ClientHandler, (LPVOID)clientSocket, 0, NULL);
if (hThread == NULL)
{
printf("CreateThread failed with error: %dn", GetLastError());
closesocket(clientSocket);
continue;
}
CloseHandle(hThread);
}
// 清理
closesocket(serverSocket);
WSACleanup();
return 0;
}
這段代碼初始化Winsock,創(chuàng)建一個監(jiān)聽特定端口的TCP服務器。每當有客戶端連接時,服務器就創(chuàng)建一個新的線程來處理該客戶端的通信。在子線程中,ClientHandler
函數(shù)接收來自客戶端的數(shù)據(jù),將其打印出來,并將同樣的數(shù)據(jù)回傳給客戶端。
由于CreateThread
函數(shù)創(chuàng)建的線程默認是守護線程(非前臺線程),因此主線程結束時,子線程也將被終止。在上面的代碼中,CloseHandle
函數(shù)被用來關閉線程句柄,但這并不意味著線程立即結束,它只是釋放了主線程對線程句柄的引用。