线程
概念
线程是轻量级进程,一般是一个进程中的多个任务。
进程是系统中最小的资源分配单位
进程 是操作系统中资源分配的最小单位。每个进程都有自己的地址空间,并且拥有独立的资源(如内存、文件句柄等)
进程之间通常是相互独立的,彼此不能直接访问对方的内存空间
线程是系统中最小的执行单位
线程 是操作系统中程序执行的最小单位,也称为“轻量级进程”。一个进程可以包含一个或多个线程,这些线程共享该进程的资源(如内存和文件句柄),但每个线程有自己的栈、寄存器和程序计数器
进程是资源分配的基本单元,而线程是调度和执行的基本单元。线程能够比进程更轻量级地执行任务,因为它们共享进程的资源。
特征
共享资源
线程共享进程的资源,如内存、文件句柄等。多个线程可以同时访问相同的资源,因此它们能够轻松地进行数据交换和通信,而不需要像进程之间那样复杂的IPC(进程间通信)机制。
效率高
30%(并发度)
上下文切换开销小:线程之间的上下文切换(如切换栈、寄存器)比进程之间的上下文切换要轻量得多,因为线程共享进程的资源。
并发执行:在多核处理器上,多个线程可以真正并发地运行,利用系统资源更加充分。一般来说,通过多线程可以提高系统的并发度,并提升性能。
三方库
pthread clone posix
在C语言中,常用的线程库包括 pthread
、clone
系统调用和 POSIX 标准库(其中 pthread
是最常用的)
3.1 编写代码头文件: pthread.h
3.2 编译代码加载库: -lpthread library
在编译使用 pthread
的程序时,需要链接 pthread
库
libpthread.so
gcc 1.c -lpthread
设置gcc的别名
-
临时别名设置
使用 alias
命令来设置 GCC 的别名
alias gcc='gcc -g -pthread '
这个设置会立即生效,但只在当前的终端会话中有效。如果关闭终端或重启系统,别名设置会消失。
-
永久别名设置
为了使别名设置在每次终端启动时都有效,将别名添加到用户的 .bashrc
文件中,这样每次登录时,别名都会自动加载:
cd ~ # 切换到家目录
vim .bashrc # 编辑 .bashrc 文件
在 .bashrc
文件末尾添加以下内容:
alias gcc='gcc -g -pthread '
保存并退出 Vim:
:wq
然后,执行以下命令以重新加载 .bashrc
,使别名立即生效:
source ~/.bashrc
这样设置后,每次新打开的终端都会自动应用设置的别名。
线程与进程对比
优点
比多进程节省资源,可以共享变量。
缺点
- 稳定性
线程和进程相比,稳定性,稍微差些
线程共享进程的地址空间,如果一个线程发生错误(例如访问非法内存),可能会导致整个进程崩溃。这是因为线程之间没有内存隔离,因此相对于多进程模式,线程的稳定性稍差。
线程的调度依赖于操作系统的线程调度器,当系统负载过高时,线程之间的竞争可能导致线程饥饿或优先级反转等问题。
- 调试难度
线程的调试gdb,相对麻烦些
info thread 查看当前程序中所有线程的状态
可以使用 thread <thread_id>
命令切换到指定的线程。例如,要调试线程 3,可以输入thread 3
线程与进程区别
资源
-
共享资源
线程:线程在同一个进程内运行,所有线程共享进程的全局资源,如地址空间、文件描述符、信号处理程序等。这种共享资源带来了一定的性能优势,但也引入了资源竞争的问题。例如,如果多个线程同时访问共享数据而没有正确的同步机制,就可能会导致数据不一致或竞态条件。
进程:进程有自己独立的资源,包括独立的内存空间、文件描述符等。进程之间没有直接的资源共享,如果需要通信,必须通过进程间通信(IPC)机制(如管道、消息队列、共享内存等)来进行。
-
私有资源
线程:虽然线程共享大部分资源,但每个线程仍然有自己的私有资源,例如栈(用于函数调用、局部变量等)和寄存器(如程序计数器、栈指针等)。通常,每个线程的栈空间较小,默认大小在 8 MB 左右(这可以在系统配置或程序代码中调整)。
进程:进程的所有资源都是私有的,其他进程无法直接访问。每个进程都有自己的地址空间,典型的用户空间为 3 GB(在 32 位系统上),剩余 1 GB 保留给内核空间。
空间和通信
- 地址空间
线程:线程共享同一个进程的地址空间,这意味着一个线程可以直接访问另一个线程的数据。这种共享空间允许线程之间进行快速通信和数据交换,但也要求开发者小心管理,以避免数据竞争和死锁等问题。
进程:进程有各自独立的地址空间,这意味着一个进程无法直接访问另一个进程的内存数据。这种隔离增强了系统的稳定性和安全性,但也使得进程间通信变得更为复杂,通常需要使用 IPC 机制来交换数据。
- 通信
线程:由于线程共享同一个进程的地址空间,所以它们可以通过共享内存进行直接通信。没有额外的开销,可以非常高效地共享数据。
进程:进程之间不能直接共享数据,因此必须依赖操作系统提供的 IPC 机制进行通信。这些机制通常包括管道(pipe)、消息队列(message queue)、共享内存(shared memory)、信号(signal)等。虽然这些方法也可以实现数据交换,但相比线程间的直接通信,效率会稍低一些。
线程的设计框架 posix
创建多线程 >>线程空间操作 >>线程资源回收(栈区)
errno strerror(errno) perror();
创建多线程
int pthread_create
(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
- 功能
该函数可以创建指定的一个线程。
- 参数
thread 线程id,需要实现定义并由该函数返回。
attr 线程属性,一般是NULL,表示默认属性。
start_routine 指向指针函数的函数指针。
本质上是一个函数的名称即可。称为th 回调函数,是线程的执行空间。
arg 回调函数的参数,即参数3的指针函数参数。
- 返回值
成功 0 失败 错误码
注意:一次pthread_create执行只能创建一个线程。
每个进程至少有一个线程称为主线程。(主线程不需要创建)
主线程退出则所有创建的子线程都退出。
主线程必须有子线程同时运行才算多线程程序。
线程id是线程的唯一标识,是CPU维护的一组数字。
pstree 查看系统中多线程的对应关系。
多个子线程可以执行同一回调函数。
获得当前线程的线程号
pthread_t pthread_self(void);
返回值的类型unsigned long int; 可用%lu打印
- 功能
获取当前线程的线程id
- 参数
无
- 返回值
成功 返回当前线程的线程id 失败 -1;
线程的退出
pthread_exit
自行退出>>自杀 >>子线程自己退出
exit(1);
void pthread_exit(void *retval); exit return p;
- 功能
子线程自行退出
- 参数
retval 线程退出时候的返回状态,临死遗言。
- 返回值
无
pthread_cancel
强制退出 >>他杀 >>主线程结束子线程
int pthread_cancel(pthread_t thread);
- 功能
请求结束一个线程
- 参数
thread 请求结束一个线程tid
- 返回值
成功 0 失败 -1
线程的回收
线程的回收机制
不同与进程没有孤儿线程和僵尸线程
>>主线程结束任意生成的子线程都会结束
>>子线程的结束不会影响主线程的运行
pthread_join
int pthread_join(pthread_t thread, void **retval);
**改变指针的指向
- 功能
通过该函数可以将指定的线程资源回收,该函数具有阻塞等待功能,如果指定的线程没有结束,回收线程会阻塞。
- 参数
thread 要回收的子线程tid
retval 要回收的子线程返回值/状态。——ptread_exit(值);
- 返回值
成功 0 失败 -1;
子线程的回收策略
1、如果预估子线程可以有限范围内结束则正常用pthread_join等待回收。
2、如果预估子线程可能休眠或者阻塞则等待一定时间后强制回收。
3、如果子线程已知必须长时间运行则,不再回收其资源。
线程的参数与返回值
#include <pthread.h>
#include <stdio.h>
void *fun(void *arg) {
int x = *(int *)arg; // 解引用指针,获取传递的整数值
printf("Thread received: %d\n", x);
return NULL;
}
int main() {
pthread_t tid;
int x = 10;
pthread_create(&tid, NULL, fun, &x); // 将 x 的地址传递给线程函数
pthread_join(tid, NULL); // 等待线程执行完成
return 0;
}
pthread_create
传递参数
pthread_create(&tid, NULL, fun, &x);
中的&x
是传递给线程函数fun
的参数。由于线程函数的参数类型是void *
,所以必须传递指针。
线程函数 fun
void *fun(void *arg)
中的arg
是一个通用指针,可以指向任何类型的数据在函数内部,可将
arg
转换为具体的数据类型,这里是int
类型的指针,然后通过解引用来获取实际的整数值:int x = *(int *)arg;
- 由于线程函数的参数类型必须是
void *
,所以任何参数都需要通过指针传递,甚至是简单的整数。 - 在线程函数内部,通常需要将
void *
类型的参数转换回实际类型,如将void *arg
转换为int *
。
线程的分离
设置线程分离属性的主要目的是让线程在结束后,系统能够自动回收其资源,而不需要主线程通过 pthread_join
来等待它的结束。对于那些不需要与主线程同步或者不需要获取其返回值的线程,设置分离属性可以简化资源管理。
pthread_detach
int pthread_detach(pthread_t thread);
- 功能
pthread_detach
函数用于将一个已经创建的线程设置为分离状态。一旦线程被设置为分离状态,当它终止时,其所有资源(如栈、线程控制块等)会自动被系统回收,而不需要调用 pthread_join
来等待它的结束。
- 参数
thread
:线程ID,表示要设置为分离状态的线程。通常传入的是当前线程的ID或其他需要分离的线程的ID。
- 返回值
0
:成功。
非0:失败,返回错误码。
pthread_detach
通常在以下情况下使用:
- 不需要同步:如果主线程或其他线程不需要等待某个线程的结束(即不需要获取其返回值),可以将该线程设置为分离状态,以便其终止后自动释放资源。
- 动态设置:可以在线程创建后,动态决定是否将该线程设置为分离状态。
线程的清理
pthread_cleanup_push
void pthread_cleanup_push(void (*routine)(void *), void *arg);
- 功能
注册一个线程清理函数。当线程被取消或以其他方式退出时,会自动调用这个清理函数,确保资源被正确释放。
- 参数
routine
:清理函数的入口,即一个函数指针。该函数将在清理时被调用。
arg
:传递给清理函数的参数。这个参数可以是任何需要在清理时使用的数据或资源句柄。
- 返回值
无返回值。
pthread_cleanup_pop
void pthread_cleanup_pop(int execute);
- 功能
调用先前由 pthread_cleanup_push
注册的清理函数。
这个函数通常与 pthread_cleanup_push
成对出现。
- 参数
execute
:
如果 execute
非0,则会立即执行清理函数。
如果 execute
为0,则不会执行清理函数。
- 返回值
无返回值。