随着多核处理器的普及,并发编程成为了提升应用程序性能和响应能力的关键技术。在C语言中,实现并发的主要方式是使用多线程。本节将初步介绍使用 POSIX Threads (Pthreads) 和 Windows API 进行线程创建与管理的基本概念和操作。
1. 什么是线程?
线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程可以拥有多个线程,这些线程共享进程的内存空间(如代码段、数据段、堆),但每个线程拥有自己独立的栈、寄存器和线程局部存储(Thread-Local Storage, TLS)。
进程与线程的区别:
- 资源拥有:进程是资源分配的基本单位,拥有独立的地址空间;线程是CPU调度的基本单位,共享进程资源。
- 开销:创建或销毁进程的开销远大于线程。
- 通信:进程间通信(IPC)相对复杂;线程间通信由于共享内存而更为方便,但也更容易引发同步问题。
2. POSIX Threads (Pthreads)
Pthreads 是一个可移植的操作系统接口标准(IEEE Std 1003.1c-1995),定义了一套创建和操作线程的API。它广泛应用于类Unix系统(如Linux, macOS)。
a. 包含头文件
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // for sleep()
b. 线程函数
线程将执行一个函数,这个函数被称为线程函数。它的原型通常是:
void *thread_function(void *arg);
- 它接受一个 void* 类型的参数,允许传递任意类型的数据给线程。
- 它返回一个 void* 类型的值,允许线程返回任意类型的数据。
c. 创建线程 (pthread_create)
pthread_create 函数用于创建一个新的线程。
语法:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
- thread:指向 pthread_t 类型变量的指针,用于存储新创建线程的ID。
- attr:指向线程属性对象的指针(pthread_attr_t)。通常可以传递 NULL 使用默认属性。
- start_routine:线程函数的指针。
- arg:传递给线程函数的参数。
- 返回值:成功时返回0,失败时返回错误码。
示例:
void *my_thread_func(void *arg) {
int thread_arg = *((int*)arg);
printf("Hello from thread! Argument received: %d\n", thread_arg);
for (int i = 0; i < 3; ++i) {
printf("Thread %d counting: %d\n", thread_arg, i);
sleep(1);
}
// 线程可以返回一个值,例如动态分配的内存
int *return_value = malloc(sizeof(int));
if (return_value) {
*return_value = thread_arg * 10;
}
return (void*)return_value;
}
int main() {
pthread_t tid1, tid2;
int arg1 = 1, arg2 = 2;
int ret;
printf("Main: Creating thread 1\n");
ret = pthread_create(&tid1, NULL, my_thread_func, &arg1);
if (ret != 0) {
fprintf(stderr, "Error creating thread 1: %d\n", ret);
return 1;
}
printf("Main: Creating thread 2\n");
ret = pthread_create(&tid2, NULL, my_thread_func, &arg2);
if (ret != 0) {
fprintf(stderr, "Error creating thread 2: %d\n", ret);
// 考虑是否需要处理已创建的 tid1
return 1;
}
printf("Main: Threads created. Waiting for them to finish...\n");
// ... 等待线程结束 ...
printf("Main: Program finished.\n");
return 0;
}
d. 等待线程结束 (pthread_join)
一个线程可以通过调用 pthread_join 来等待另一个线程结束,并获取其返回值。
语法:
int pthread_join(pthread_t thread, void **retval);
- thread:要等待的线程ID。
- retval:一个指向 void* 的指针,用于存储被等待线程的返回值。如果不需要返回值,可以传递 NULL。
- 返回值:成功时返回0,失败时返回错误码。
示例 (续上):
// ... 在 main 函数中创建线程后 ...
void *thread1_ret_val = NULL;
void *thread2_ret_val = NULL;
ret = pthread_join(tid1, &thread1_ret_val);
if (ret != 0) {
fprintf(stderr, "Error joining thread 1: %d\n", ret);
} else {
if (thread1_ret_val) {
printf("Main: Thread 1 finished and returned: %d\n", *((int*)thread1_ret_val));
free(thread1_ret_val); // 释放线程返回的动态内存
}
}
ret = pthread_join(tid2, &thread2_ret_val);
if (ret != 0) {
fprintf(stderr, "Error joining thread 2: %d\n", ret);
} else {
if (thread2_ret_val) {
printf("Main: Thread 2 finished and returned: %d\n", *((int*)thread2_ret_val));
free(thread2_ret_val);
}
}
printf("Main: Program finished.\n");
// ...
e. 线程退出
线程可以通过以下方式退出:
- 从线程函数中 return。
- 调用 pthread_exit()。
pthread_exit 语法:
void pthread_exit(void *retval);
- retval:线程的返回值,与线程函数返回值的意义相同。
注意:如果在主线程(main函数)中调用 exit() 或者从 main 函数 return,整个进程(包括所有线程)都会终止。如果希望主线程退出但其他线程继续运行(不推荐,通常主线程应等待子线程),主线程应调用 pthread_exit()。
f. 分离线程 (pthread_detach)
默认情况下,线程是“可连接的”(joinable),意味着必须有其他线程调用 pthread_join 来回收其资源。如果一个线程不需要被等待,可以将其设置为“分离的”(detached)。分离线程结束后,其资源会自动被系统回收。
语法:
int pthread_detach(pthread_t thread);
- thread:要分离的线程ID。
- 返回值:成功时返回0,失败时返回错误码。
一旦线程被分离,就不能再对其调用 pthread_join。 可以在创建线程时通过线程属性设置其为分离状态。
g. 编译 Pthreads 程序
在Linux上使用GCC编译Pthreads程序时,通常需要链接Pthreads库:
gcc my_program.c -o my_program -lpthread
3. Windows API
Windows API 提供了自己的一套函数来创建和管理线程。
a. 包含头文件
#include <windows.h>
#include <stdio.h>
// #include <process.h> // 对于 _beginthreadex 和 _endthreadex (更推荐)
b. 线程函数
Windows API 的线程函数原型通常是:
DWORD WINAPI ThreadProc(LPVOID lpParam);
- DWORD:无符号32位整数。
- WINAPI:调用约定。
- LPVOID:等同于 void*。
- 返回值是线程的退出码。
c. 创建线程 (CreateThread或 _beginthreadex)
Windows 提供了 CreateThread 函数。然而,对于C/C++运行时库(CRT)的某些功能(如 errno、信号处理、strtok 等),直接使用 CreateThread 可能会导致问题,因为它不会为新线程正确初始化CRT状态。因此,更推荐使用CRT提供的 _beginthreadex 函数(在 <process.h> 中定义)。
CreateThread 语法:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 安全属性,通常 NULL
SIZE_T dwStackSize, // 栈大小,0 表示默认
LPTHREAD_START_ROUTINE lpStartAddress, // 线程函数指针
LPVOID lpParameter, // 传递给线程的参数
DWORD dwCreationFlags, // 创建标志,0 表示立即运行
LPDWORD lpThreadId // 指向DWORD的指针,接收线程ID,可为NULL
);
- 返回值:成功时返回线程句柄 (HANDLE),失败时返回 NULL。
_beginthreadex 语法 (推荐):
uintptr_t _beginthreadex(
void *security, // 安全属性,通常 NULL
unsigned stack_size, // 栈大小,0 表示默认
unsigned (__stdcall *start_address )(void *), // 线程函数
void *arglist, // 传递给线程的参数
unsigned initflag, // 创建标志,0 表示立即运行
unsigned *thrdaddr // 指向 unsigned 的指针,接收线程ID,可为NULL
);
- 返回值:成功时返回线程句柄 (uintptr_t 可以转换为 HANDLE),失败时返回0。
示例 (_beginthreadex):
#include <windows.h>
#include <process.h> // For _beginthreadex
#include <stdio.h>
unsigned __stdcall MyWindowsThread(void *arg) {
int thread_arg = *((int*)arg);
printf("Hello from Windows thread! Argument: %d\n", thread_arg);
for (int i = 0; i < 3; ++i) {
printf("Windows Thread %d counting: %d\n", thread_arg, i);
Sleep(1000); // Sleep in milliseconds
}
// 线程退出码
return thread_arg * 100;
}
int main() {
HANDLE hThread1, hThread2;
unsigned threadID1, threadID2;
int arg1 = 10, arg2 = 20;
printf("Main: Creating Windows thread 1\n");
hThread1 = (HANDLE)_beginthreadex(NULL, 0, MyWindowsThread, &arg1, 0, &threadID1);
if (hThread1 == 0) { // _beginthreadex 失败返回0
fprintf(stderr, "Error creating thread 1: %lu\n", GetLastError());
return 1;
}
printf("Main: Creating Windows thread 2\n");
hThread2 = (HANDLE)_beginthreadex(NULL, 0, MyWindowsThread, &arg2, 0, &threadID2);
if (hThread2 == 0) {
fprintf(stderr, "Error creating thread 2: %lu\n", GetLastError());
CloseHandle(hThread1); // 清理已创建的句柄
return 1;
}
printf("Main: Windows threads created. Waiting...\n");
// ... 等待线程结束 ...
CloseHandle(hThread1); // 关闭线程句柄
CloseHandle(hThread2);
printf("Main: Program finished.\n");
return 0;
}
d. 等待线程结束 (WaitForSingleObject或 WaitForMultipleObjects)
主线程可以使用 WaitForSingleObject 等待单个线程结束,或使用 WaitForMultipleObjects 等待多个线程结束。
WaitForSingleObject 语法:
DWORD WaitForSingleObject(
HANDLE hHandle, // 要等待的对象的句柄 (这里是线程句柄)
DWORD dwMilliseconds // 超时时间 (毫秒),INFINITE 表示无限等待
);
- 返回值:WAIT_OBJECT_0 表示对象已受信(线程已结束),其他值表示超时或错误。
示例 (续上):
// ... 在 main 函数中创建线程后 ...
printf("Main: Waiting for thread 1 to finish...\n");
WaitForSingleObject(hThread1, INFINITE);
DWORD exitCode1;
GetExitCodeThread(hThread1, &exitCode1); // 获取线程退出码
printf("Main: Thread 1 finished with exit code: %lu\n", exitCode1);
printf("Main: Waiting for thread 2 to finish...\n");
WaitForSingleObject(hThread2, INFINITE);
DWORD exitCode2;
GetExitCodeThread(hThread2, &exitCode2);
printf("Main: Thread 2 finished with exit code: %lu\n", exitCode2);
CloseHandle(hThread1);
CloseHandle(hThread2);
// ...
e. 线程退出
Windows 线程可以通过以下方式退出:
- 从线程函数 return (返回的值即退出码)。
- 调用 ExitThread() (不推荐,因为它不会执行C++对象的析构)。
- 调用 _endthreadex() (推荐,它会正确清理CRT资源并终止线程)。
_endthreadex 语法:
void _endthreadex(unsigned retval);
- retval:线程的退出码。
f. 关闭句柄 (CloseHandle)
线程句柄是内核对象,使用完毕后必须通过 CloseHandle 关闭,以释放资源。关闭句柄不会终止线程,只是表示你不再需要通过该句柄来引用此线程。线程结束后,其内核对象在所有句柄都被关闭且引用计数为0时才会被销毁。
4. 线程管理注意事项
- 共享数据:多个线程访问共享数据时,必须使用同步机制(如互斥锁、信号量、条件变量)来避免竞态条件和数据损坏。这将在后续章节讨论。
- 死锁:不当的锁使用可能导致死锁,即多个线程互相等待对方释放资源。
- 线程数量:创建过多线程会消耗大量系统资源(内存、调度开销),反而可能降低性能。应根据任务特性和硬件能力合理设计线程数量。
- 错误处理:线程API的调用都应检查返回值,并进行适当的错误处理。
- 资源清理:线程分配的资源(如动态内存、文件句柄)需要妥善管理和释放,避免泄漏。
总结
线程是实现并发的强大工具。Pthreads 和 Windows API 提供了各自的接口来创建和管理线程。理解线程的生命周期、如何创建、如何等待以及如何安全地退出,是多线程编程的基础。接下来的挑战在于如何有效地协调多个线程的工作并安全地共享数据。