在编程的世界里,多线程技术就像是一把神奇的钥匙,能够开启高效处理和资源利用的新大门。而今天我们要深入探讨的 Pthread,便是多线程编程领域中一颗璀璨的明珠。
无论是 Linux、Unix 还是其他遵循POSIX标准的操作系统,Pthread 都能大显身手。它如同一位技艺高超的指挥家,能让程序中的多个线程有条不紊地协同工作,充分发挥多核处理器的潜力,实现资源的高效利用和程序性能的大幅提升。对于每一位渴望深入探索多线程编程奥秘的开发者来说,了解 Pthread 就像是踏上了一段充满挑战与惊喜的奇妙旅程。现在,就让我们一同走进 Pthread 的世界,揭开它神秘的面纱吧!
在编程的世界里,多线程技术就像是一把神奇的钥匙,能够开启高效处理和资源利用的新大门。而今天我们要深入探讨的 Pthread,便是多线程编程领域中一颗璀璨的明珠。
无论是 Linux、Unix 还是其他遵循 POSIX 标准的操作系统,Pthread 都能大显身手。它如同一位技艺高超的指挥家,能让程序中的多个线程有条不紊地协同工作,充分发挥多核处理器的潜力,实现资源的高效利用和程序性能的大幅提升。对于每一位渴望深入探索多线程编程奥秘的开发者来说,了解 Pthread 就像是踏上了一段充满挑战与惊喜的奇妙旅程。现在,就让我们一同走进 Pthread 的世界,揭开它神秘的面纱吧!
一、Pthread简介
Pthread,即 POSIX threads,作为 POSIX 的线程标准,为开发者提供了一套强大且规范的线程编程接口。它在多种操作系统中广泛适用,包括 Unix、Linux、macOS 等类 Unix 系统,甚至在 Windows 系统中也有移植版本。
Pthread 的 API 命名方式与一般 C/C++ 代码相同,这使得编程过程更加易于理解和上手。例如,创建线程使用pthread_create函数,该函数有多个参数,包括指向线程标识符的指针、线程属性、线程执行函数的起始地址以及运行函数的参数。通过这些参数,可以灵活地控制线程的创建过程。
在类 Unix 系统中,Pthread 是多线程编程的基础。以 Linux 系统为例,它广泛支持 Pthreads,开发者可以利用 Pthread 库在 Linux 环境下创建、管理和同步多个线程。而对于 macOS,虽然是 Apple 公司的操作系统,但也遵循 POSIX 标准,同样支持 Pthreads。
对于 Windows 系统,虽然不是类 Unix 系统,但可以通过一些工具和库来实现 POSIX 兼容性,从而使用 Pthreads 进行多线程编程。比如使用pthreads-win32,这是一个开源项目,为 Windows 操作系统提供了 Posix 线程接口,使得开发者可以在 Windows 平台上编写跨平台的多线程程序。
Pthread 的出现,为多线程编程带来了诸多优势。它允许程序同时运行多个任务,通过并发执行来加速处理过程,提高应用程序的性能和响应速度。同时,由于其遵循 POSIX 标准,具有良好的可移植性,代码可以轻松地在多个平台上进行移植,为开发者提供了极大的便利。
pthread 的设计思想主要是为了提供用户态的轻量级线程管理机制,比task_struct结构要简单的多,结构如下:
pthread
├── 基本信息
│ ├── tid: 线程ID
│ ├── list: 线程链表节点
│ ├── user_stack: 是否用户提供栈
│ ├── stackblock: 栈指针,用户栈
│ ├── stackblock_size: 栈大小(含保护区)
│ ├── guardsize: 保护区大小
│ ├── start_routine: 线程函数
│ └── arg: 线程函数参数
├── 线程同步与锁
│ ├── lock: 同步锁
│ ├── setxid_futex: setxid调用锁
│ ├── cleanup: 清理函数列表
│ └── exit_lock: 线程退出锁
├── 线程状态与标志
│ ├── cancelhandling: 取消处理状态
│ ├── flags: 标志位(线程属性)
│ ├── exiting: 是否正在退出
│ ├── stopped_start: 是否启动时挂起
│ └── setup_failed: 线程创建是否失败
├── 调度信息
│ ├── schedparam: 调度参数(优先级)
│ ├── schedpolicy: 调度策略
│ └── result: 线程函数返回值
├── 信号处理
│ ├── sigmask: 信号掩码
│ └── tls_state: 内部TLS状态
├── 线程局部存储(TLS)
│ ├── specific_1stblock: TLS第一级数据
│ ├── specific: TLS第二级数据指针
│ └── specific_used: 是否使用TLS
├── 互斥锁与条件变量
│ ├── robust_prev: 上一个持有的健壮性互斥锁
│ └── robust_head: 健壮性互斥锁列表头,健壮性互斥锁用于检测持有锁的线程是否已经死亡,防止死锁
├── 异常处理与栈展开
│ ├── cleanup_jmp_buf: 栈展开缓存
│ └── exc: 异常处理信息
├── 系统调用信息
│ ├── multiple_threads: 是否启用多线程
│ └── gscope_flag: 全局范围标志
├── 调试信息
│ ├── eventbuf: 事件缓冲区
│ └── nextevent: 下一个有事件的线程
└── 其他功能
├── c11: 是否为C11线程
├── rtld_catch: 动态链接器异常处理
└── rseq_area: 重启顺序区域
对于pthread的属性,有__pthread_attr来描述:
struct __pthread_attr
├── struct __sched_param __schedparam
│ └── 表示线程的调度参数,通常包括调度优先级。调度参数在实时调度策略(如 SCHED_FIFO 和 SCHED_RR)中使用。
├── void *__stackaddr
│ └── 指定线程的栈内存起始地址。可以通过设置自定义栈空间来代替系统默认分配的栈。
├── size_t __stacksize
│ └── 指定线程栈的大小。默认栈大小通常较小,可以根据需要设置更大的栈空间。
├── size_t __guardsize
│ └── 指定栈末尾的保护区大小。保护区用于防止栈溢出,通常设置为一页大小的内存不可读写。
├── enum __pthread_detachstate __detachstate
│ └── 指定线程的分离状态。可能的值为:
│ ├── PTHREAD_CREATE_JOINABLE(可连接状态)
│ └── PTHREAD_CREATE_DETACHED(分离状态)
│ 分离状态的线程在结束时会自动释放资源,不需要调用 `pthread_join()`。
├── enum __pthread_inheritsched __inheritsched
│ └── 指定线程的调度属性继承方式。可能的值为:
│ ├── PTHREAD_INHERIT_SCHED(继承调用者的调度属性)
│ └── PTHREAD_EXPLICIT_SCHED(使用显式设置的调度属性)
├── enum __pthread_contentionscope __contentionscope
│ └── 指定线程的竞争范围。可能的值为:
│ ├── PTHREAD_SCOPE_SYSTEM(系统范围竞争,线程与所有进程竞争CPU)
│ └── PTHREAD_SCOPE_PROCESS(进程范围竞争,线程仅与同一进程内的其他线程竞争)
├── int __schedpolicy
└── 指定线程的调度策略。可能的值为:
├── SCHED_OTHER(常规分时调度)
├── SCHED_FIFO(实时调度,先入先出)
└── SCHED_RR(实时调度,时间片轮转)
二、Pthread的具体使用
2.1创建线程
pthread_create函数是创建线程的主要方式。它的函数声明为int pthread_create(pthread_t* restrict tidp,const pthread_attr_t *restrict_attr,void*(*start_rtn)(void*),void *restrict arg)。第一个参数为指向线程标识符的指针,线程创建成功后,该指针会被填充上新线程的标识符。第二个参数用于设置线程属性,如果传入NULL,则使用默认的线程属性。第三个参数是线程运行函数的起始地址,该函数的返回值类型为void *,并接受一个void *类型的参数。最后一个参数是运行函数的参数。
例如,以下代码展示了如何使用pthread_create函数:
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
pthread_t ntid;
void printids(const char *s){
pid_t pid;
pthread_t tid;
pid = getpid();
tid = pthread_self();
printf("%s pid %u tid %u (0x%x)\n", s,(unsigned int)pid, (unsigned int)tid, (unsigned int)tid);
}
void *thr_fn(void *arg){
printids("new thread: ");
return((void *)0);
}
int main(void){
int err;
err = pthread_create(&ntid, NULL, thr_fn, NULL);
if (err!=0)
printf("can't create thread: %s\n", strerror(err));
printids("main thread:");
sleep(1);
exit(0);
}
如果编译时出现错误,如MSB3073,查看日志显示undefined reference to pthread_create’,这是因为Pthread` 是第三方库,需要动态链接。解决步骤如下:
-
确定虚拟机是否安装Pthread库,若没有先进行安装sudo apt-get install glibc-doc sudo apt-get install manpages-posix manpages-posix-dev。
-
定位Pthread动态库的位置locate libpthread.so。
-
在visualGDB里添加动态库,打开VisualGDB项目属性,添加远程库。
2.2线程阻塞与退出
pthread_join等待线程结束:pthread_join函数用于等待一个指定的线程结束。其函数原型为int pthread_join(pthread_t thread, void **retval)。其中,thread参数是要等待的线程的标识符,retval参数是一个指向指针的指针,用来获取线程函数的返回值。该函数在调用时会阻塞当前线程,直到指定的线程结束为止。例如:
#include<stdio.h>
#include<pthread.h>
void* thread_main(void* pmax){
int i;
int max=*((int*)pmax);
for(i=0;i<max;i++){
puts("child thread called...");
sleep(1);
}
return"Tread ended...";
}
int main(){
pthread_t pid;
void* rst;
int max=10;
pthread_create(&pid,NULL,(void*)thread_main,(void*)&max);
pthread_join(pid,(void*)&rst);
printf("the child return: %s\n",(char*)rst);
return0;
}
pthread_exit显式退出线程:pthread_exit函数用于当前运行的线程运行时退出,并且pthread_exit的参数作为返回值提供给pthread_join函数获取。如果在main函数中调用pthread_exit,只有main线程自己退出,已经创建的线程并不会随之一起退出。
2.3获取线程标识
pthread_self函数用于获取当前线程的标识符。在多线程编程环境下,有时候需要区分不同的线程,这时候就可以使用pthread_self函数。例如:
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
pthread_t ntid;
void printids(const char *s){
pid_t pid;
pthread_t tid;
pid = getpid();
tid = pthread_self();
printf("%s pid %u tid %u (0x%x)\n", s,(unsigned int)pid, (unsigned int)tid, (unsigned int)tid);
}
int main(void){
printids("main thread:");
return0;
}
三、Pthread的线程属性
3.1分离状态设置
在 Pthread 中,线程的分离状态决定了一个线程以何种方式来终止自己。默认情况下,线程处于非分离状态,这意味着原有的线程会等待创建的线程结束,只有当 pthread_join() 函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。
而分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。可以通过两种方式设置线程为分离状态。一种是在创建线程时,利用 pthread_create 函数的第二个参数即线程属性来设置分离状态。具体做法是先初始化一个线程属性变量,然后设置其为分离状态,最后将它作为参数传入线程创建函数 pthread_create()。例如:
pthread_t tid;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, THREAD_FUNCTION, arg);
另一种方法是在线程内部使用 pthread_detach 函数将线程分离。比如:
pthread_t thread_id;
// 创建线程
pthread_create(&thread_id, NULL, thread_function, NULL);
// 分离线程
pthread_detach(thread_id);
3.2线程优先级调整
在 Pthread 中,可以获取和设置新线程的优先级。新线程不继承父线程调度优先级(PTHREAD_EXPLICIT_SCHED),仅当调度策略为实时(即 SCHED_RR 或 SCHED_FIFO)时才有效,并可以在运行时通过 pthread_setschedparam() 函数来改变。新线程的优先级为默认为 0。
获取线程可以设置的最高和最低优先级,可以使用 sched_get_priority_max(int policy) 和 sched_get_priority_min(int policy) 函数。对于 SCHED_OTHER 策略,sched_priority 只能为 0。对于 SCHED_FIFO 和 SCHED_RR 策略,sched_priority 从 1 到 99。
设置和获取优先级通过 pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param) 和 pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param *param) 函数。例如:
pthread_attr_t attr;
struct sched_param param;
pthread_attr_init(&attr);
// 设置调度策略为 SCHED_FIFO
pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
// 获取优先级范围
int rr_min_priority = sched_get_priority_min(SCHED_RR);
int rr_max_priority = sched_get_priority_max(SCHED_RR);
param.sched_priority = (rr_min_priority + rr_max_priority) / 2;
// 设置优先级
pthread_attr_setschedparam(&attr, ¶m);
3.3调度策略选择
Pthread 有三种调度策略:SCHED_OTHER、SCHED_RR 和 SCHED_FIFO。
SCHED_OTHER 是 Linux 默认的分时调度策略,所有线程的优先级别都是 0,线程的调度是通过分时来完成的。这种调度策略也是抢占式的,当高优先级的线程准备运行的时候,当前线程将被抢占并进入等待队列。
SCHED_FIFO 是一种实时的先进先出调用策略,且只能在超级用户下运行。这种调用策略仅仅被使用于优先级大于 0 的线程。当一个线程变成可运行状态,它将被追加到对应优先级队列的尾部。对于相同优先级别的线程,按照先进先出的规则运行。
SCHED_RR 对 SCHED_FIFO 做出了一些增强功能。它使用最大运行时间来限制当前进程的运行,当运行时间大于等于最大运行时间的时候,当前线程将被切换并放置于相同优先级队列的最后。这样其他具有相同级别的线程能在当前线程下执行。
可以通过 pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy) 函数来设置线程的调度策略。例如:
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setschedpolicy(&attr, SCHED_RR);
四、Pthread的优缺点
4.1优点分析
跨平台性与可移植性:Pthread 作为 POSIX 标准的一部分,具有良好的跨平台性和可移植性。它不仅可以在类 Unix 系统如 Linux、macOS 上使用,还可以通过一些工具和库在 Windows 系统上实现 POSIX 兼容性,从而进行多线程编程。这使得开发者可以轻松地将基于 Pthread 的代码在不同的操作系统之间进行移植,大大提高了开发效率和代码的复用性。据统计,几乎所有现代类 Unix 系统都支持 Pthread,这为开发者在这些平台上进行多线程编程提供了稳定可靠的基础。
与 C/C++ 特性集成:Pthread 最初是为 C 语言设计的,但它也可以在 C++ 程序中使用。这使得 C 和 C++ 开发者可以在不同的项目中共享相同的多线程编程接口,提高了代码的可维护性和可扩展性。此外,Pthread 提供了较低级别的控制,允许程序员直接管理线程资源,如线程属性、线程栈大小等。这种细粒度的控制使得开发者可以根据具体的应用需求进行优化,提高程序的性能。
4.2缺点探讨
缺乏读写锁支持:Pthread 没有提供读写锁(RWlock)的支持,这在一些需要同时进行读操作和写操作的多线程应用中可能会带来一些不便。例如,在一个多线程的数据库应用中,如果没有读写锁,可能需要使用更复杂的同步机制来保证数据的一致性,这会增加开发的难度和代码的复杂性。虽然有一个类似的 shared_mutex,但它属于 C++14,很多编译器可能不支持,这进一步限制了开发者的选择。
操作线程和 Mutex 等 API 较少:为了实现跨平台性,Pthread 只能选取各原生实现的子集,这导致操作线程和互斥锁(Mutex)等的 API 较少。如果开发者需要设置某些属性,需要通过 API 调用返回原生平台上的对应对象,再对返回的对象进行操作。这与一些高级的多线程库相比,增加了开发的难度和工作量。例如,在设置线程优先级时,需要使用多个函数进行复杂的操作,而不像一些高级库那样提供简洁的接口。此外,由于 API 较少,一些高级的功能可能无法直接实现,需要开发者自己编写更多的代码来实现相同的功能。
五、Pthread的应用场景
5.1互斥锁应用
在卖票窗口的场景中,假设有多个售票窗口同时售票,而票数是有限的共享资源。如果没有互斥锁的保护,多个线程同时对票数进行操作,可能会导致票数出现错误。
使用 Pthread 的互斥锁可以很好地解决这个问题。首先定义一个全局变量表示票数,比如 int tickets = 100。然后创建多个线程模拟售票窗口,每个线程在卖票时先获取互斥锁,再进行票数的减少操作,操作完成后释放互斥锁。以下是示例代码:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
int tickets = 100;
pthread_mutex_t mutex;
void* sellTickets(void* arg) {
while (tickets > 0) {
pthread_mutex_lock(&mutex);
if (tickets > 0) {
printf("窗口 %ld 卖出一张票,剩余票数:%d\n", pthread_self(), --tickets);
}
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main() {
pthread_mutex_init(&mutex, NULL);
pthread_t threads[5];
for (int i = 0; i < 5; i++) {
pthread_create(&threads[i], NULL, sellTickets, NULL);
}
for (int i = 0; i < 5; i++) {
pthread_join(threads[i], NULL);
}
pthread_mutex_destroy(&mutex);
return 0;
}
通过这种方式,使用 Pthread 的互斥锁可以保证多线程操作共享资源时的一致性,避免出现数据错误。
5.2栅栏同步方式
使用 pthread_barrier 可以实现多线程同步,在某些特定场景下非常有用。例如在一个数据处理程序中,需要多个线程分别处理不同部分的数据,然后在所有线程都完成处理后进行下一步的汇总操作。
首先初始化栅栏,指定等待的线程数量。比如有三个线程处理数据,就初始化 pthread_barrier_t barrier; pthread_barrier_init(&barrier, NULL, 3);。然后在每个线程的处理函数中,在完成数据处理后调用 pthread_barrier_wait(&barrier);。这样,所有线程都会在这个点等待,直到所有线程都到达这个点,然后一起继续执行下一步操作。以下是一个简单的示例代码:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
pthread_barrier_t barrier;
void* processData(void* arg) {
int id = *(int*)arg;
printf("线程 %d 正在处理数据...\n", id);
// 模拟数据处理时间
sleep(id + 1);
printf("线程 %d 完成数据处理,等待其他线程...\n", id);
pthread_barrier_wait(&barrier);
if (id == 0) {
printf("所有线程完成数据处理,开始汇总操作...\n");
}
return NULL;
}
int main() {
pthread_t threads[3];
int ids[3] = {0, 1, 2};
pthread_barrier_init(&barrier, NULL, 3);
for (int i = 0; i < 3; i++) {
pthread_create(&threads[i], NULL, processData, &ids[i]);
}
for (int i = 0; i < 3; i++) {
pthread_join(threads[i], NULL);
}
pthread_barrier_destroy(&barrier);
return 0;
}
使用 pthread_barrier 可以确保多线程在特定的点进行同步,提高程序的协调性和正确性。
5.3实际应用案例
以在 Windows 和 Linux 系统下的一个网络服务器程序为例,说明 Pthread 在不同操作系统下的应用配置和多线程问题的解决。
在 Linux 系统下,由于其本身对 Pthread 的广泛支持,使用起来相对简单。可以直接使用系统提供的编译器进行编译,链接 Pthread 库即可。例如,在编译一个使用 Pthread 的服务器程序时,可以使用 gcc -pthread server.c -o server 命令进行编译。在程序中,可以使用 Pthread 创建多个线程来处理客户端的连接请求,每个线程独立处理一个连接,提高服务器的并发处理能力。
在 Windows 系统下,虽然不是类 Unix 系统,但可以通过 pthreads-win32 库来实现 Pthread 的功能。首先需要下载并安装 pthreads-win32 库,然后按照特定的配置步骤将头文件和库文件添加到项目中。在代码中,可以使用与 Linux 下类似的方式创建线程来处理网络连接请求。
例如,以下是一个简单的网络服务器程序的框架:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#ifdef _WIN32
#include <winsock2.h>
#else
#include <sys/socket.h>
#include <netinet/in.h>
#endif
void* handleClient(void* arg) {
// 处理客户端连接的代码
return NULL;
}
int main() {
int serverSocket;
struct sockaddr_in serverAddress, clientAddress;
socklen_t clientAddressLength;
pthread_t thread;
#ifdef _WIN32
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData)!= 0) {
perror("WSAStartup failed");
return -1;
}
#endif
serverSocket = socket(AF_INET, SOCK_STREAM, 0);
if (serverSocket == -1) {
perror("socket creation failed");
return -1;
}
serverAddress.sin_family = AF_INET;
serverAddress.sin_addr.s_addr = INADDR_ANY;
serverAddress.sin_port = htons(8080);
if (bind(serverSocket, (struct sockaddr*)&serverAddress, sizeof(serverAddress)) == -1) {
perror("bind failed");
return -1;
}
if (listen(serverSocket, 5) == -1) {
perror("listen failed");
return -1;
}
while (1) {
clientAddressLength = sizeof(clientAddress);
int clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddress, &clientAddressLength);
if (clientSocket == -1) {
perror("accept failed");
continue;
}
pthread_create(&thread, NULL, handleClient, (void*)&clientSocket);
}
#ifdef _WIN32
WSACleanup();
#endif
return 0;
}
通过这个例子可以看出,Pthread 在不同操作系统下都可以发挥重要作用,通过合理的配置和编程,可以有效地解决多线程问题,提高程序的性能和可扩展