线程
一、pthread 线程概述
pthread(POSIX threads)是一种用于在程序中实现多线程的编程接口。它与进程一样,可以用于实现并发执行任务,但与进程相比有一些不同的特点。
二、优点
1. 比多进程节省资源:进程在创建时需要分配独立的内存空间(0 - 3G),而线程在启动时只需要较小的空间(8M)。此外,线程之间可以共享进程的资源,如内存、文件描述符等,减少了资源的重复分配和占用。
2. 可以共享变量:线程之间可以直接访问共享的内存区域,这使得它们之间的数据共享更加高效和方便。相比之下,进程之间的数据共享需要通过复杂的进程间通信机制。
三、概念和特征
1. 概念:线程被称为轻量级进程,通常是一个进程中的多个任务。进程是系统中最小的资源分配单位,而线程是系统中最小的执行单位。一个进程可以包含多个线程,默认情况下每个进程至少有一个主线程。
2. 特征:
◦ 共享资源:线程可以共享进程的内存空间和其他资源,这使得线程之间的数据交换更加高效。
◦ 效率高:相比多进程,线程的创建和切换开销较小,可以提高程序的执行效率,通常可以提高约 30% 的效率。
◦ 三方库支持:pthread 提供了一套跨平台的线程编程接口,包括clone等系统调用和 POSIX 标准支持,便于移植。在编写代码时需要包含头文件pthread.h,编译时需要加载-lpthread库,线程函数的实现通常在libpthread.so库中。例如,可以使用gcc 1.c -lpthread命令编译包含线程的程序。
四、缺点
1. 稳定性稍差:与进程相比,线程的稳定性稍微差一些。由于线程共享进程的地址空间,如果一个线程出现错误,可能会影响到其他线程甚至整个进程的稳定性。
2. 调试相对麻烦:使用 GDB 调试多线程程序相对复杂,GDB 只能跟踪其中一条线程分支。可以使用info thread命令查看线程信息,然后使用thread命令切换到特定的线程进行调试。
在实际编程中,需要根据任务的特点和需求来选择使用进程还是线程。如果任务复杂,需要独立的资源和更高的稳定性,可以选择使用进程;如果任务相对简单,需要高效的数据共享和并发执行,可以选择使用线程。
线程与进程区别:
一、资源方面
1. 线程:
◦ 相比进程,线程多了共享资源的特性。线程可以共享所属进程的大部分资源,如内存空间、文件描述符等,这使得线程之间的数据交换和通信更加高效,类似于进程间通信(IPC)但更加便捷。
◦ 同时,线程又具有部分私有资源,其中最主要的是私有栈区。每个线程都有自己独立的栈空间,用于存储函数调用的栈帧、局部变量等,保证了线程在执行过程中的独立性。
2. 进程:
◦ 进程间只有私有资源,没有共享资源。每个进程都有独立的内存空间、文件描述符表、打开的文件列表等资源,进程之间的资源相互隔离,不能直接访问对方的资源。
二、空间方面
1. 进程:
◦ 进程空间独立,不同的进程拥有各自独立的地址空间。这意味着一个进程不能直接访问另一个进程的内存空间,它们之间的通信需要通过特定的机制,如管道、消息队列、共享内存等方式进行。
2. 线程:
◦ 线程可以共享空间,因为它们都在所属进程的地址空间内运行。线程之间可以直接通过共享的内存进行通信,无需像进程间通信那样使用复杂的机制。这种直接通信的方式使得线程之间的数据交换更加快速和高效,但也需要注意同步和互斥问题,以避免数据竞争和不一致性。
线程设计框架概述
POSIX 线程提供了一套用于多线程编程的标准接口。其设计框架主要包括创建多线程、线程空间操作以及线程资源回收等步骤。
创建多线程(pthread_create函数)
1.
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
这个函数用于创建一个新的线程。
◦ thread:是一个输出参数,用于存储新创建线程的 ID。在调用之前需要先定义并传入一个pthread_t类型的变量,函数执行成功后,该变量将被赋予新线程的 ID。
◦ attr:用于指定线程的属性,一般设置为NULL表示使用默认属性。
◦ start_routine:是一个函数指针,指向新线程要执行的函数。这个函数接收一个void*类型的参数,并返回一个void*类型的值。通常被称为回调函数,它定义了线程的执行空间。
◦ arg:是传递给回调函数的参数。可以通过这个参数向新线程传递数据。
2. 注意事项:
◦ 一次pthread_create执行只能创建一个线程。
◦ 每个进程至少有一个线程称为主线程。如果主线程退出,那么所有创建的子线程也会退出。
◦ 主线程必须有子线程同时运行才算多线程程序。
◦ 线程 ID 是线程的唯一标识,由 CPU 维护的一组数字。可以使用pstree命令查看系统中多线程的对应关系。多个子线程可以执行同一回调函数。
◦ ps -eLf命令可以查看线程相关信息(Low Weight Process)。ps -eLo pid,ppid,lwp,stat,comm也可以展示一些线程相关的信息。
获取当前线程 ID(pthread_self函数)
1.
pthread_t pthread_self(void);
这个函数用于获取当前线程的线程 ID。
◦ 功能:获取当前正在执行的线程的 ID。
◦ 参数:无。
◦ 返回值:成功时返回当前线程的 ID,类型为pthread_t(通常是一个无符号长整型,可使用%lu格式输出)。失败时返回 -1。
◦ 另一种获取线程 ID 的方式是通过系统调用syscall(SYS_gettid)。
创建多线程、打印线程id、验证变量共享:
这段代码创建了两个线程和一个主线程,通过共享全局变量a展示了线程之间的数据共享和执行顺序的不确定性。线程 1 先执行并改变了全局变量a的值,线程 2 在延迟 1 秒后输出改变后的a的值,主线程在创建两个子线程后进入无限循环以保持程序运行。
线程的退出
一、自行退出(自杀)
void pthread_exit(void *retval);
这个函数用于子线程自行退出。
◦ 功能:当子线程调用这个函数时,子线程会立即退出。它允许子线程在完成任务后或者出现错误时主动退出执行。
◦ 参数:retval是线程退出时候的返回状态,类似于 “临死遗言”。其他线程可以通过特定的方式获取这个返回值,以了解子线程退出的原因或状态。例如,可以在另一个线程中使用pthread_join函数来获取子线程的退出状态。
◦ 返回值:无。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
void *th(void* arg)
{
// 输出子线程的线程 ID
printf("th tid:%lu\n", pthread_self());
// 子线程通过调用 pthread_exit(NULL) 退出,等效于 return NULL;
// 这表示子线程正常结束,没有特定的返回值
pthread_exit(NULL);
}
int main(int argc, const char *argv[])
{
// 输出主线程的线程 ID
printf("main tid:%lu\n", pthread_self());
pthread_t tid;
// 创建子线程
pthread_create(&tid, NULL, th, NULL);
while (1)
sleep(1);
// 主线程进入无限循环,防止主线程过早退出,否则子线程也会跟着退出
return 0;
}
二、强制退出(他杀)
int pthread_cancel(pthread_t thread);
这个函数用于主线程强制结束子线程。
◦ 功能:主线程可以调用这个函数来请求结束一个特定的子线程。这类似于一种 “他杀” 行为,主线程主动干预子线程的执行,要求其终止。
◦ 参数:thread是要请求结束的子线程的线程 ID(tid)。
◦ 返回值:成功时返回 0;失败时返回 -1。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
// 线程函数 1,不断输出“接受控制”
void *th1(void* arg)
{
while (1)
{
printf("接受控制\n");
sleep(1);
}
return NULL;
}
// 线程函数 2,不断输出“发送视频”
void *th2(void* arg)
{
while (1)
{
printf("发送视频\n");
sleep(1);
}
return NULL;
}
int main(int argc, const char *argv[])
{
pthread_t tid1, tid2;
// 创建第一个线程,执行 th1 函数
pthread_create(&tid1, NULL, th1, NULL);
// 创建第二个线程,执行 th2 函数
pthread_create(&tid2, NULL, th2, NULL);
int i = 0;
while (1)
{
// 每循环一次,i 加 1
if (i == 3)
// 当 i 等于 3 时,强制取消 tid1 对应的线程(即第一个线程)
pthread_cancel(tid1);
if (i == 5)
// 当 i 等于 5 时,强制取消 tid2 对应的线程(即第二个线程)
pthread_cancel(tid2);
sleep(1);
i++;
}
return 0;
}
线程的回收
一、线程回收机制
1. 与进程不同,线程没有孤儿线程和僵尸线程的概念。在进程中,如果父进程先于子进程结束,子进程可能成为孤儿进程,由系统的 init 进程收养;如果子进程结束但父进程未及时回收其资源,子进程会成为僵尸进程。而在线程中,主线程结束时,任意生成的子线程都会结束;同时,子线程的结束不会影响主线程的运行。
二、pthread_join函数
int pthread_join(pthread_t thread, void **retval);
这个函数用于回收指定线程的资源。
◦ 功能:它可以将指定的线程资源进行回收,并且具有阻塞等待功能。如果指定的线程没有结束,回收线程的调用者(通常是主线程或其他线程)会被阻塞,直到目标线程结束。这样可以确保线程资源被正确回收,避免资源泄漏。
◦ 参数:
◦ thread是要回收的子线程的线程 ID(tid)。
◦ retval是一个二级指针,用于接收要回收的子线程的返回值或状态。通过传递二级指针,可以在函数内部修改指针所指向的内容,从而获取子线程的返回状态。例如,如果子线程通过pthread_exit(值)退出,可以在回收线程中通过retval获取这个退出值。
◦ 返回值:成功时返回 0;失败时返回 -1。
三、子线程的回收策略
1. 如果预估子线程可以在有限范围内结束,则正常使用pthread_join等待回收。这种情况下,回收线程可以确定子线程会在可预期的时间内结束,因此可以通过阻塞等待的方式确保资源被正确回收。
2. 如果预估子线程可能休眠或者阻塞,则可以等待一定时间后强制回收。例如,可以使用pthread_timedjoin_np函数在指定的时间内等待子线程结束,如果超时则强制回收资源。这种策略适用于子线程可能进入长时间休眠或阻塞状态,而回收线程不能无限期等待的情况。
3. 如果子线程已知必须长时间运行,则不再回收其资源。在某些情况下,子线程可能需要一直运行,例如作为守护线程提供持续的服务。这种情况下,可以不回收子线程的资源,让其在后台持续运行。但需要注意,不回收资源可能会导致资源泄漏,因此需要谨慎使用这种策略,并确保子线程在不再需要时能够正确退出。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
void *th(void* arg)
{
// 在子线程中动态分配 20 个字节的内存空间
char *p = (char*)malloc(20);
// 将字符串"hello"复制到动态分配的内存空间中
strcpy(p, "hello");
// 子线程休眠 3 秒,模拟子线程执行一些耗时操作
sleep(3);
// 返回动态分配的内存地址,作为子线程的返回值
return p;
}
int main(int argc, const char *argv[])
{
pthread_t tid;
// 创建一个子线程
pthread_create(&tid, NULL, th, NULL);
void *ret = NULL;
// 等待子线程 tid 结束,并获取子线程的返回值,存储在 ret 中
pthread_join(tid, &ret);
// 输出子线程返回的字符串内容
printf("ret = %s\n", (char*)ret);
// 注意,这里需要强制转换 ret 为 char* 类型才能正确输出字符串
// 释放子线程动态分配的内存空间
free(ret);
return 0;
}
这段代码创建了一个子线程,子线程在堆上动态分配内存并存储一个字符串,然后主线程等待子线程结束并获取其返回值(即动态分配的内存地址),最后输出子线程的字符串内容并释放动态分配的内存空间。通过这种方式,主线程可以获取子线程的执行结果并正确管理资源。
线程的参数、返回值
一、传参数
1. 传整数:
◦ 普通函数传参示例:int add(int a, int b);中a和b是形参,在调用时如add(x, y);中x和y是实参。
◦ 线程传参:pthread_create(&tid, NULL, fun, x);中最后一个参数可以用来向线程函数传递一个整数参数。在线程函数void * fun(void * arg);中,通过将arg强制转换为合适的类型来获取传入的参数。
2. 传字符串:
◦ 栈区字符数组:在函数内部定义的字符数组,如char buf[] = "";,这种方式定义的字符数组在函数执行完毕后其内存空间可能会被回收,不适合在线程间传递。
◦ 字符串常量:char *p = "hello";,字符串常量存储在静态存储区,其地址在程序运行期间一直有效,但由于其内容不可修改,也不太适合作为线程间传递的参数。
◦ 堆区字符串:char *pc = (char *)malloc(128);,通过动态分配内存的方式创建的字符串,可以作为参数传递给线程函数。在主线程中创建子线程时使用pthread_create(&tid, NULL, fun, pc);,在子线程函数fun(void *arg)中,将arg强制转换为char*类型,即可访问传入的字符串。在主线程中需要在合适的时候使用free(pc);释放动态分配的内存。
3. 传结构体:
◦ 定义结构体类型,例如typedef struct { int a; char b[20]; } MyStruct;。
◦ 用结构体定义变量,如MyStruct myStruct;。
◦ 向pthread_create传结构体变量,pthread_create(&tid, NULL, fun, &myStruct);。
◦ 从子线程中获取结构体数据,在子线程函数fun(void *arg)中,将arg强制转换为结构体指针类型,如MyStruct *myStructPtr = (MyStruct *)arg;,然后就可以访问结构体中的成员。
二、返回值
1. pthread_exit(0)可以改为pthread_exit(9);等,这里传递的参数是一个void*类型的指针,可以指向任何数据类型。例如,pthread_exit可以返回一个整数的地址,如int * p = malloc(4); *p = -10; pthread_exit(p);。
2. pthread_join(tid, NULL);可以改为pthread_join(tid,?);,其中第二个参数是一个二级指针,用于接收子线程的返回值。例如,void **retval; pthread_join(tid, retval);。
子线程退出时可以返回一个内存地址,这个地址所在的内存中可以存储任何数据。但是要注意地址的有效性:
• 栈区变量:错误,子线程结束后栈区变量的地址失效,不能作为返回值。
• 全局变量:失去意义,因为主线程可以直接访问全局变量,不需要通过子线程返回。
• 静态变量和堆区变量:可以作为子线程的返回值,因为它们的内存空间在子线程结束后仍然有效。
主线程通过一个地址形式的变量来接受子线程返回的地址变量,就可以将该地址中的数据取到。
设置线程分离属性
int pthread_detach(pthread_t thread);
这个函数用于设置指定线程的分离属性。
1. 功能:设置指定线程为分离状态。当一个线程被设置为分离状态后,一旦该线程结束执行,它的资源会被自动回收,而不需要其他线程通过调用pthread_join来回收资源。这对于那些不需要被其他线程等待或管理的线程非常有用,可以避免资源泄漏和不必要的等待。
2. 参数:thread是要设置分离属性的线程 ID。通常是在创建线程后,将新创建的线程 ID 传入这个函数来设置分离属性。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
void* th(void* arg)
{
// 将当前线程设置为分离状态,这样当线程结束时,系统会自动回收资源
pthread_detach(pthread_self());
// 输出当前线程的 ID
printf("tid : %lu\n", pthread_self());
return NULL;
}
int main(int argc, const char *argv[])
{
pthread_t tid;
int i;
for (i = 0; i < 55000; i++)
{
// 创建新线程
int ret = pthread_create(&tid, NULL, th, NULL);
if (ret!= 0)
{
// 如果创建线程失败,跳出循环
break;
}
}
// 输出成功创建的线程数量
printf("i = %d\n", i);
return 0;
}
在这段代码中,main函数创建了多个线程,每个线程在执行时都会将自身设置为分离状态。这样,当这些线程结束执行时,系统会自动回收它们的资源,无需主线程通过pthread_join来回收。循环的目的是创建尽可能多的线程,直到创建线程失败为止,然后输出成功创建的线程数量。这种方式可以测试系统在一定资源条件下能够创建的线程数量上限。