【Linux】线程与同步互斥相关知识详细梳理
目录
1. 线程概念
2. 线程优势
3. 线程劣势
4. 线程控制
4.1 POSIX线程库
4.2 线程操作
5. 线程互斥
5.1 互斥相关概念
5.2 互斥量mutex
5.3 互斥量实现原理
6. 线程同步
6.1 同步概念与竞态条件
6.2 条件变量
6.3 条件变量使用规范及细节
1. 线程概念
什么是线程:
在⼀个程序里的⼀个执行路线就叫做线程(thread)。
更准确的定义是:**线程是'一个进程内部的控制序列'。任何进程中都必然拥有至少一个执行线程。
该系统采用基于优先级的任务轮转调度算法,在内存层面上实现了对物理资源的高效共享与分配;通过虚拟地址空间映射机制,在CPU层面实现了对物理地址空间的直接访问与管理;同时通过任务轮转与时间片切换等多级调度策略优化了系统的资源利用率。
在线程层面的设计中,“作业”这一概念被引入并得到充分应用,“作业”作为粒度最小的基本执行单位能够最大限度地提高程序运行效率;这种设计使得程序能够在同一时刻处理多个独立的任务。
综上所述,在现代操作系统中,默认情况下每一个运行中的应用程序都由多个独立的作业(即细粒度的任务单元)共同完成其功能;而这些作业则被统称为线程或逻辑并行实体。
2. 线程优势
- 创建新线程所需的开销远低于创建新进程。
- 相较于进程间的转换操作,在完成线程转换时操作系统所需的工作量显著减少。
- 主要区别在于线程转换过程中虚拟内存空间保持不变;而进程转换则需加载新的内存空间。
- 通过内核将寄存器中的内容进行转换的过程伴随明显的性能损失。
- 此外,在完成上下文转换时会破坏处理器缓存机制。
- 当你改变虚拟内存空间时……这会导致页表缓冲TLB(快表)全部失效……
- 因此,在执行基于虚拟地址的操作时会发生页面置换……
- 在这种模式下……会导致大量无效缓存命中率出现下降
线程所消耗的资源较少,从而能够最大限度地利用多处理器系统的可并行处理能力.
当慢速I/O操作完成时,程序便立即开始执行其他计算任务.
线程所消耗的资源较少,从而能够最大限度地利用多处理器系统的可并行处理能力.
当慢速I/O操作完成时,程序便立即开始执行其他计算任务.
5.采用并行计算策略以适应多处理器系统的运行需求。
6.I/O密集型应用为了提高性能需将所有I/O操作重叠进行线程可同时处理不同的I/O请求。
3. 线程劣势
- 性能开销
系统健壮性有所下降
实现多线程功能时需要更加注重细节安排,在一个多线程程序运行过程中因时间资源分配不均或者未采取适当的保护机制(如未对共享变量进行适当管理)而引发潜在问题的可能性显著增加。
缺少相应的访问权限机制
从进程层面出发,在单个线程内部调用某些操作系统函数可能会影响到整个系统的运行状态。
开发难度相应提升
相比单线程程序设计而言,在多线程环境下实现相同功能通常会面临更高的技术门槛。
**** 编写与调试⼀个多线程程序比单线程程序困难得多
4. 线程异常
单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。
在计算机操作系统中, 线程被视为一个进程中能够独立执行的任务实体, 线程一旦出现故障, 就会类似于进程中出现故障所引发的一系列响应措施, 从而触发系统中的信号处理机制来终止其运行. 当一个进程中出现故障时, 其内存空间内的所有相关子线程也会随之退出.
4. 线程控制
说完了概念,下面来实操一下把。
4.1 POSIX线程库
与线程相关的功能组形成了一个完整的系列,在大多数情况下这些函数的名字都会以"pthread_"作为前缀来标识。
在使用这些函数库时需包含头文件 <pthread.h>。
需采用编译器指令参数"-lpthread"来连接这些线程相关的动态链接库。
4.2 线程操作
pthread_create
- 用于创建一个新线程。
- 原型 :
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
- 字段 :
- thread:用于存储当前 thread 标识符的一个指针变量,在 thread 创建完成后会存储该 thread 的唯一 thread ID。
- attr:用于存储 thread 的属性信息,默认情况下初始化为 NULL 值(表示使用默认属性)。
- start_routine:一个函数名或者方法名,在调用时会被作为 entry point 执行。
- arg:传递给 start_routine 函数作为入参的数据项或者引用对象。
- 返回值 :若调用成功则返回 0;若调用失败则返回非零错误代码。
当 p threaded s 函数出现错误时,并没有在全局变量 errno 中进行设置(然而大多数其他 POSIX 函数都会采取这种方法)。而是将错误信息通过返回值传递给调用者,并且同时在每个线程内部也会有一个对应的 errno 变量供其使用。对于 p threaded s 函数的错误情况而言,在调用者处检查返回码更为高效
示例:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
void *rout(void *arg)
{
int i;
for (;;)
{
printf("I'am thread 1\n");
sleep(1);
}
}
int main(void)
{
pthread_t tid;
int ret;
if ((ret = pthread_create(&tid, NULL, rout, NULL)) != 0)
{
fprintf(stderr, "pthread_create : %s\n", strerror(ret));
exit(EXIT_FAILURE);
}
int i;
for (;;)
{
printf("I'am main thread\n");
sleep(1);
}
}
pthread_exit
- 使当前线程终止执行,并返回一个值给其他线程。
- 原型 :
void pthread_exit(void *retval);
- 参数 :
retval:线程的退出状态。
pthread_join
- 等待指定线程终止,并回收线程资源。
- 原型 :
int pthread_join(pthread_t thread, void **retval);
- 参数 :
thread:要等待的线程 ID。retval:指向线程退出状态的指针。
(若不想得到退出状态,传入NULL/nullptr即可)
示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread1(void *arg)
{
printf("thread 1 returning ... \n");
int *p = (int *)malloc(sizeof(int));
*p = 1;
return (void *)p;
}
void *thread2(void *arg)
{
printf("thread 2 exiting ...\n");
int *p = (int *)malloc(sizeof(int));
*p = 2;
pthread_exit((void *)p);
}
void *thread3(void *arg)
{
while (1)
{ //
printf("thread 3 is running ...\n");
sleep(1);
}
return NULL;
}
int main(void)
{
pthread_t tid;
void *ret;
// thread 1 return
pthread_create(&tid, NULL, thread1, NULL);
pthread_join(tid, &ret);
printf("thread return, thread id %X, return code:%d\n", tid, *(int *)ret);
free(ret);
// thread 2 exit
pthread_create(&tid, NULL, thread2, NULL);
pthread_join(tid, &ret);
printf("thread return, thread id %X, return code:%d\n", tid, *(int *)ret);
free(ret);
// thread 3 cancel by other
pthread_create(&tid, NULL, thread3, NULL);
sleep(3);
pthread_cancel(tid);
pthread_join(tid, &ret);
if (ret == PTHREAD_CANCELED)
printf("thread return, thread id %X, return code:PTHREAD_CANCELED\n",
tid);
else
printf("thread return, thread id %X, return code:NULL\n", tid);
}
运行结果:

pthread_detach
- 通过将线程从主线程中分离,能够有效地清理剩余资源,并确保主线程无需等待该线程完成才能继续运行。 *
int pthread_detach(pthread_t thread);
- 参数 :
thread:要分离的线程 ID。
默认情况下,默认新创建的线程采用joinable属性。当这些线程退出时,默认情况下必须执行pthread_join操作以确保资源能够被正确地释放,并避免可能导致资源泄漏的问题。如果不需要关注线程返回值的情况,则此时可以采取措施告诉系统,在这些线程退出之后自动释放相关资源。
分离的线程无法join:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread_run(void *arg)
{
pthread_detach(pthread_self());//自己分离自己
printf("%s\n", (char *)arg);
return NULL;
}
int main(void)
{
pthread_t tid;
if (pthread_create(&tid, NULL, thread_run, "thread1 run...") != 0)
{
printf("create thread error\n");
return 1;
}
int ret = 0;
sleep(1); // 等一下,不然线程可能来不及分离
if (pthread_join(tid, NULL) == 0)
{
printf("pthread wait success\n");
ret = 0;
}
else
{
printf("pthread wait failed\n");
ret = 1;
}
return ret;
}
5. 线程互斥
5.1 互斥相关概念
关键资源:在多线程环境中相互竞争使用的资源即被称为关键资源。
关键区段:每个线程内部用于执行与关键资源相关操作的代码块即称为关键区段。
互斥机制:为了确保并发安全任何一个时刻只能有一个执行流程能够进入关键区段进行操作以保证关键资源的安全性。
不可分割的操作:在调度机制无法中断的情况下该操作只能处于两种状态:已完成或未完成。
5.2 互斥量mutex
通常情况下,在大多数场景中
示例:
// 操作共享变量会有问题的售票系统代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;
void *route(void *arg)
{
char *id = (char *)arg;
while (1)
{
//临界区 ticket是临界资源
if (ticket > 0)
{
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
}
else
{
break;
}
}
}
int main(void)
{
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, route, (void*)"thread 1");
pthread_create(&t2, NULL, route, (void*)"thread 2");
pthread_create(&t3, NULL, route, (void*)"thread 3");
pthread_create(&t4, NULL, route, (void*)"thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
}

可以观察到,在正常情况下票数归零后会立即停止递减;然而,在某些情况下这一预期并未实现-票数反而降为负值-这是因为线程对共享资源的操作顺序并不确定:在刚进入自减逻辑(ticket>0)的过程中其他线程可能已经修改了当前票数值;因此导致即使满足if条件程序仍继续执行自减操作从而产生了错误的结果
要解决这种问题,就需要互斥量mutex,也就是锁。
pthread_mutex_init
- 初始化互斥锁(mutex)。
- 原型 :
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
- 参数 :
mutex:指向互斥锁的指针。attr:互斥锁属性(通常设置为NULL)。
也可以直接定义全局锁,静态分配。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
pthread_mutex_lock
- 锁定互斥锁,确保线程在访问共享资源时不会发生竞争。
- 原型 :
int pthread_mutex_lock(pthread_mutex_t *mutex);
pthread_mutex_unlock
- 解锁互斥锁,允许其他线程访问被锁定的资源。
- 原型:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
pthread_mutex_destroy
- 销毁互斥锁,释放资源。
- 原型 :
int pthread_mutex_destroy(pthread_mutex_t *mutex);
使用示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>
int ticket = 100;
pthread_mutex_t mutex; //定义一个全局的锁
void *route(void *arg)
{
char *id = (char *)arg;
while (1)
{
pthread_mutex_lock(&mutex); //加锁
if (ticket > 0)
{
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
pthread_mutex_unlock(&mutex); //解锁
}
else
{
pthread_mutex_unlock(&mutex);
break;
}
}
}
int main(void)
{
pthread_t t1, t2, t3, t4;
pthread_mutex_init(&mutex, NULL); //初始化锁
pthread_create(&t1, NULL, route, (void*)"thread 1");
pthread_create(&t2, NULL, route, (void*)"thread 2");
pthread_create(&t3, NULL, route, (void*)"thread 3");
pthread_create(&t4, NULL, route, (void*)"thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
pthread_mutex_destroy(&mutex); //释放锁
}
本质上来说,在一个临界区中加一把锁能够确保只有一个线程能够对该资源进行访问,在这种情况下临界区内部的操作流程也就只有一条连续的路径可循。与此同时,在释放了该资源之后必须记得将相应的锁定机制解除以便于其他线程进入该区域进行操作。值得注意的是如果一个拥有当前锁定状态的线程试图再次对同一资源获取锁定就会导致系统发生死锁现象
使用互斥量锁时一定要注意图示规范:

5.3 互斥量实现原理
下面是互斥量的逻辑:

在一次执行中, 最多只有一个线程能够获取到mutex的编号'1', 从而实现对资源的加锁操作. 其中movb和xchgb均为完全无 interleaving 的原子性操作.
6. 线程同步
6.1 同步概念与竞态条件
同步 :在保证数据安全的前提下,让线程能够
int pthread_cond_signal(pthread_cond_t *cond);
按照一种确定的访问顺序遵循关键资源的行为 ,从而有效地防止资源竞争称为同步机制
6.2 条件变量
pthread_cond_init
- 初始化条件变量。
- 原型 :
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
或定义全局的条件变量,静态分配。
pthread_cond_t PTHREAD_COND_INITIALIZER
pthread_cond_wait
- 使线程等待条件变量,直到条件成立。
- 原型 :
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
pthread_cond_signal
- 唤醒等待条件变量的一个线程。
- 原型 :
int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_broadcast
- 唤醒所有等待条件变量的线程。
- 原型 :
int pthread_cond_broadcast(pthread_cond_t *cond);
示例:
1. #include <iostream>
2. #include <string.h>
3. #include <unistd.h>
4. #include <pthread.h>
5. pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
6. pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
7. void *active(void *arg)
8. {
9. std::string name = static_cast<const char *>(arg);
10. while (true)
11. {
12. pthread_mutex_lock(&mutex);
13. pthread_cond_wait(&cond, &mutex);
14. std::cout << name << " 活动..." << std::endl;
15. pthread_mutex_unlock(&mutex);
16. }
17. }
18. int main(void)
19. {
20. pthread_t t1, t2;
21. pthread_create(&t1, NULL, active, (void *)"thread-1");
22. pthread_create(&t2, NULL, active, (void *)"thread-2");
23. sleep(3); // 可有可⽆,这⾥确保两个线程已经在运⾏
24. while (true)
25. {
26. // 对⽐测试
27. // pthread_cond_signal(&cond); // 唤醒⼀个线程
28. pthread_cond_broadcast(&cond); // 唤醒所有线程
29. sleep(1);
30. }
31. pthread_join(t1, NULL);
32. pthread_join(t2, NULL);
33. }
基于具有特定特性的条件变量对象,我们能够建立一个基于阻塞队列的简单生产者-消费者模型。此外,在未来的文章中,我将详细探讨这一技术的具体实现方法。
6.3 条件变量使用规范及细节
我们可以明确看到,在**phtread_cond_wait()**函数中设置一个参数为mutex的原因及其必要性。其原因在于,在条件等待状态下实现多线程同步通信时必须遵守严格的锁管理原则。具体而言,在这种机制下若没有正确的锁管理,则会导致严重的死锁问题:即如果一个线程在其条件等待过程中先获得了锁(lock),随后又进行了条件等待(wait),那么这种情况就很可能导致死锁的发生。此外需要注意的是,在主线程被唤醒并重新获得锁之后才能继续执行后续操作。
条件等待示例:
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
说明:条件判断必须采用while循环机制以确保在 wake-up 事件触发后仍能有效执行相关的操作流程;若采用其他类型机制可能导致 wake-up 前已经满足特定条件而 wake-up 时却不再满足这一情况发生(即所谓的"伪唤醒"现象);而 while 循环设计则能确保 wake-up 事件发生后只有在确实满足所有前提条件下才会退出循环体执行后续操作流程;这种设计方式能够有效避免"伪唤醒"问题的发生
给条件发送信号代码:
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);
