27-线程与通信
线程:
什么是线程:
线程是一个轻量化进程,一个任务的创建,调度,消亡的过程
进程与线程的区别?
线程创建:
每个线程拥有属于自己的栈空间(默认8M),其余空间与进程内其余线程共享
局部变量独享
全局变量,静态变量,字符串常量,堆区都是共享的
线程的调度
与进程调度保持一致,宏观并行微观串行
线程的消亡
线程结束必须回收线程空间,否则会产生僵尸线程
===================================================================================================
线程创建
pthread_create
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
功能:创建一个新的执行线程
新线程从 start_routine 函数开始执行
主线程和新线程并发执行
参数:
thread 输出参数,返回线程 ID(用于后续操作)
attr 线程属性(通常传 NULL 使用默认属性)
start_routine 线程入口函数指针(void* (*)(void*))
arg 传递给线程入口函数的参数
返回值:成功创建返回0,失败返回错误码(正数)
分离属性:
线程结束后,系统自动回收线程空间,无需调用pthread_join回收空间
pthread_attr_init
int pthread_attr_init(pthread_attr_t *attr);
对线程属性进行初始化
pthread_attr_destroy
int pthread_attr_destroy(pthread_attr_t *attr);
线程销毁
pthread_attr_setdetachstate
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
设置属性
PTHREAD_CREATE_DETACHED
Threads that are created using attr will be created in a de‐
tached state.
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tip, &attr, function, &arg);
pthread_attr_destory(&attr);
优点是自动回收空间,结束线程,无需人为干预
确点是,不能确定线程什么时候结束,无法针对性控制
编译必须加上 -lpthread: gcc xxx.c -o xxx -lpthread (连接pthread库)
==================================================================================================
pthread_exit
void pthread_exit(void *retval);
功能: 终止当前线程,并返回一个值给等待它的线程
终止调用线程的执行
将 retval 返回给调用 pthread_join() 的线程
线程的资源(栈、寄存器等)会被回收
不会终止整个进程(除非是主线程调用且其他线程都已结束)
参数:线程退出时返回的值(void* 类型)
可以是任意指针
父线程通过 pthread_join() 接收
int num = 42;
// 返回整数(转换为 void*)
pthread_exit((void*)(long)num);
注意:它不是把 num 的地址传出去,而是把 num 的值 42 强行“塞”进了一个指针大小的空间里传出去。
(long)num:把 42 转成 long 类型(保证和指针一样宽)。
(void*):把 42 这个数值伪装成一个地址(指针)。
主线程接收时,再把这个“伪地址”强制转回数字: int result = (int)(long)retval;
返回值:void
pthread_exit() 的参数类型必须是 void *,但可以传入任何类型的指针(因为任何指针都可以隐式转换为 void *)。
如果 pthread_exit() 返回的指针指向局部变量(栈上的变量),当线程函数退出时,这块栈内存会被自动回收。主线程通过 pthread_join() 拿到的指针将指向一个无效的内存区域,访问它会导致未定义行为(程序崩溃、数据错乱等)。
1. 使用malloc(堆)
2.使用静态或全局变量
static int result;
3.字符串常量
pthread_exit("success exit");
==================================================================================================
pthread_join
int pthread_join(pthread_t thread, void **retval);
功能:等待指定线程结束,并回收其资源
阻塞调用线程,直到 thread 线程结束
回收目标线程的资源(栈、寄存器等)
获取目标线程的返回值(如果 retval 不为 NULL)
如果目标线程已经结束,立即返回
参数:thread 要等待的线程 ID(由 pthread_create 返回)
retval 输出参数:存储线程返回值的指针
如果目标线程调用 pthread_exit(ret),则 *retval = ret
如果目标线程 return ret,则 *retval = ret
传 NULL 表示不关心返回值
返回值:成功 0 等待成功
失败 错误码(正数) 如 ESRCH(线程不存在)、EDEADLK(死锁)
注意:接收的pthread_exit返回数据是一个指针,但不是返回值,需要在函数中更改,所以需要传入一个二级指针来进行修改
====================================================================================================
pthread_self() — 获取当前线程 ID 返回值:当前线程的 ID(pthread_t 类型)
pthread_t tid = pthread_self();
打印:
系统 pthread_t 类型 打印格式
Linux unsigned long %lu 无符号(u)long类型(l)10进制
macOS unsigned long %lu
FreeBSD struct pthread * %p
或printf("线程ID %#lx\n", (unsigned long)pthread_self()); #(显示前缀)long类型(l)的x(16进制)
线程可以进行通信,同一进程的线程共用文本段,数据段,堆区,只有栈区独享。但多个线程共享会导致资源竞争。
原子操作:不能被CPU调度打断的一次最小的操作称为原子操作
临界代码:加锁解锁中间的代码,这段代码用户不会被同时执行
互斥锁
防止线程的资源竞争
锁的功能作用
锁的核心功能是实现对共享资源的互斥访问,通过保证同一时刻只有一个线程能进入临界区,来保护共享数据不被并发修改破坏。它能够将非原子的“读-改-写”操作变成一个不可分割的整体,避免竞态条件导致的数据错乱;同时,确保一个线程对共享数据的修改能及时被其他线程看到。简单说,锁解决了多线程编程中最核心的矛盾——如何在并发环境下保证数据的一致性。
锁带来的好处与代价
使用锁最直接的好处是让原本非线程安全的代码变得安全可靠,避免了脏读、数据丢失更新等问题,同时也提供了程序执行顺序的控制能力。但锁并非没有代价:它会带来性能损耗,设计不当还会导致死锁或并发度严重下降。
锁的工作原理:互斥锁的本质是保证“同一时刻,只有一个线程能进入临界区”。因为同一个锁再加上之后无法继续加锁,只有在临界代码执行完毕,解锁之后继续才能再次加锁。这就使得当一个线程加锁后,切换到其他线程时因为无法加锁会切出直到再次回到未解锁的那个线程,一直到该线程执行完毕解锁。
互斥锁通过原子操作保证加锁的排他性,一旦某线程加锁成功,其他线程在解锁前都无法获取同一把锁,因而会被操作系统阻塞并让出CPU,直到持有锁的线程执行完临界区并解锁,才唤醒下一个等待线程。
一个进程并不限制锁的数量
线程自身加锁后,自身也无法再次加锁,只有解锁后才能再次加锁,
加锁; 线程加锁
if (0) 不满足条件无法解锁,同时其他线程也无法加锁,切回到自身线程无法加锁,程序锁死
{
解锁;
}
定义锁
#include <pthread.h>
pthread_mutex_t lock; 全局变量
pthread_mutex_init(&lock, NULL); 初始化
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
// 参数1: 锁变量指针
// 参数2: 锁属性指针(NULL表示默认属性)
// 返回值: 0成功,非0失败
pthread_mutex_lock(&lock); 上锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 参数: 锁变量指针
// 返回值: 0成功,非0失败(如EINVAL表示锁未初始化)
pthread_mutex_unlock(&lock); 解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 参数: 锁变量指针
// 返回值: 0成功,非0失败(如EPERM表示不是锁的持有者)
pthread_mutex_destroy(&lock); 线程结束后销毁锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 参数: 锁变量指针
// 返回值: 0成功,非0失败(如EBUSY表示锁仍被占用)
pthread_mutex_lock() 阻塞直到锁可用 无 大部分常规场景
pthread_mutex_trylock() 非阻塞,立即返回 无 尝试获取锁,失败则做其他事
pthread_mutex_timedlock() 阻塞,但有超时限制 有(绝对时间) 防止死锁,设置最大等待时间
死锁:
使用互斥锁时需要避免的一种情况
由于多个锁的加锁解锁导致程序无法继续执行的一种状态
产生的四个必要条件:
1.互斥条件:只能一个线程使用
2.不可剥夺条件:不能使用一把已经被使用的锁
3.循环请求保持:拿不到锁一直请求
4.循环等待:拿不到锁不能继续执行,只能等待
如何避免:
1.使用trylock
2.加锁解锁顺序各个线程保持一致
二.信号量
1.信号量可以看作一种资源,在程序运行过程中,代码,变量,CPU,内存都可以看成一种资源
2.资源一定是有限的,资源只能被初始化,销毁,申请和释放
让线程程序顺序先后执行
3.函数接口:
#include <semaphore.h>
1.sem_init
int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:初始化信号量
参数:
sem 指向要初始化的信号量变量的指针
pshared 信号量的共享范围。0 表示线程间共享,非0 表示进程间共享(需配合共享内存)
value 信号量的初始值,代表可用资源的数量
返回值: 成功返回 0,失败返回 -1 并设置 errno
。
2.sem_destroy
int sem_destroy(sem_t *sem);
功能:销毁由 sem_init 成功初始化的信号量
参数: sem:指向要销毁的信号量变量的指针
返回值: 0:成功-1; 失败,并设置 errno
3.sem_wait 信号量减1
int sem_wait(sem_t *sem);
功能:POSIX 标准中用于等待信号量的函数,它会尝试将信号量的值减1,如果信号量值为0则阻塞等待,直到信号量变为可用。
参数: sem:指向已初始化信号量的指针
返回值:0:成功
-1:失败,并设置 errno
4.sem_post 信号量加1
int sem_post(sem_t *sem);
功能:POSIX 标准中用于释放信号量的函数,它会将信号量的值原子性地加1,并唤醒一个等待该信号量的线程。
参数: sem:指向已初始化信号量的指针
返回值:0:成功
-1:失败,并设置 errno
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)