【Linux】线程同步与互斥
目录
线程相关问题
线程安全
常见的线程安全的情况
常见的线程不安全的情况
可重入函数与不可重入函数
常见不可重入的情况
常见可重入的情况
可重入与线程安全的关系
联系
区别
线程同步与互斥
互斥锁
使用
死锁
死锁的四个必要条件
如何避免死锁
条件变量
同步概念与竞态条件
使用
Posix 信号量
使用
生产者消费者模型
线程池
读者写者问题
线程相关问题
线程安全
在线安全定义为多线程环境中特定函数、代码块或数据结构在受多个线程并发访问时所具有的特性,在这种情况下系统将确保执行过程无误并维持数据的一致性和完整性,并有效避免出现数据被污染、资源竞争以及死锁等潜在问题。
常见的线程安全的情况
- 每个线程仅对全局变量或静态变量具有读取权限而无写入权限,在一般情况下这些线程是安全的。
- 类或接口在运行于线程环境中时被视为原子操作。
- 多个线程之间的切换操作不会导致该接口的执行结果出现二义性。
常见的线程不安全的情况
- 未对共享资源进行保护的函数。
- 被调用时状态发生改变的函数。
- 返回指向静态对象指针的函数。
- 存在线程安全问题的函数。
可重入函数与不可重入函数
当一个函数在其执行过程中被其他执行路径触发时,在首次触发后尚未完成的情况下再次被同一或不同路径触发的情况是可以接受的,并且不会引发错误或异常,则称其为重入口可执行函数。

该图表展示了链表进行连续 insert 操作的过程。当在插入node1之后,在执行更新 head 之前调用信号处理函数时注入 node2,则会导致 node2 信息的丢失。因此说明该 insert 操作不具备可重入性。
常见不可重入的情况
- 使用了malloc和free函数, 因为malloc函数基于内存地址块列表来管理堆空间。
- 标准I/O库采用了不可重入式的方式以全局数据结构为基础进行实现。
- 具备重入能力的函数体内采用了固定内存区域作为数据存储空间。
常见可重入的情况
- 未使用全局变量或静态变量
- 未使用动态内存分配函数(malloc)或new关键字开辟空间
- 未调用不可重入函数
- 返回值只能是局部变量或由调用者提供的数据
- 允许本地存储数据或者通过复制全局数据到本地副本来保护其安全性
可重入与线程安全的关系
联系
- 当函数支持重入时,则意味着该函数是线程安全的。
- 由于该函数不具备重入能力,则无法被多个线程同时调用,并可能导致潜在的线程安全性问题。
- 若某个函数包含全局变量,则该函数既不具备线程安全性也不具备可重入性。
区别
- 该类reentrant functions fall under the category of thread-safe functions.
- thread-safe特性并非必具可重新-entry属性;然而拥有该属性的功能必定属于 thread-safe范畴。
- 当对关键资源实施加锁管理时,则该功能具备 thread-safe特性能确保系统的安全性;然而在 while the lock is held的情况下发生死锁问题会导致功能无法实现 reentrant行为。
线程同步与互斥
Linux
#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <pthread.h>
#include "Thread.hpp"
using namespace zyh;
const int num = 4;
int ticket = 10000;
void print(std::string name)
{
while (ticket > 0)
{
usleep(1001);
td::cout << "I am " << name << ", ticket: " << --ticket << std::endl;
// sleep(1);
}
}
int main()
{
std::vector<Thread<int>> threads;
int cnt = 10;
// 1. 创建线程
for (int i = 0; i < num; i++)
{
std::string name = "thread-" + std::to_string(i + 1);
threads.emplace_back(print, name, name);
}
// 2. 启动线程
for (auto& thread : threads)
{
thread.start();
}
sleep(3);
// 3. 等待线程
for (auto& thread : threads)
{
thread.join();
std::cout << "wait thread done, thread is: " << thread.getThreadName() << std::endl;
}
return 0;
}
cpp

结果:

观察到的是,在我们的预期下, 当原始票价设为 original\_tickets = 0 的时候, 应该不会有任何人能够购买这张票价; 然而, 在实际情况中发现, 即使将 original\_tickets 设置为了 -1, 系统依然会启动并开始售票
原因:当变量 ticket 的值为 0,在完成 while 循环后(此时 ticket 值仍为 0),发生调度切换至线程 2。该线程打印剩余票的数量并将其减少一个单位。执行完毕后 ticket 值变为 -1(即小于零),随后切换回原计划中的线程 1,在其继续执行剩余的打印代码时会输出负数值。
解决方案:为了解决上述问题根本原因在于 ticket 作为关键资源多线程对关键资源的操作并非原子性因此需要采取相应的措施以确保系统的安全性通过互斥锁机制则能够有效解决这个问题
#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <pthread.h>
#include "Thread.hpp"
using namespace zyh;
const int num = 4;
int ticket = 10000;
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
void print(std::string name)
{
while (ticket > 0)
{
pthread_mutex_lock(&mtx);
//usleep(1001);
if (ticket > 0)
{
std::cout << "I am " << name << ", ticket: " << --ticket << std::endl;
// sleep(1);
}
pthread_mutex_unlock(&mtx);
}
}
int main()
{
std::vector<Thread<std::string>> threads;
int cnt = 10;
// 1. 创建线程
for (int i = 0; i < num; i++)
{
std::string name = "thread-" + std::to_string(i + 1);
threads.emplace_back(print, name, name);
}
// 2. 启动线程
for (auto& thread : threads)
{
thread.start();
}
sleep(3);
// 3. 等待线程
for (auto& thread : threads)
{
thread.join();
std::cout << "wait thread done, thread is: " << thread.getThreadName() << std::endl;
}
return 0;
}
cpp

互斥锁
Linux
伪代码如下:

使用
ubuntu 下可能会出现 man 手册查不到 pthread 相关库函数的问题
原因:因为man手册中默认没有安装关于 posix 标准的文档。
解决办法:bash 输入以下内容
sudo apt-get install manpages-posix-dev
bash
创建与销毁
原型
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);int pthread_mutex_destroy(pthread_mutex_t *mutex);
若 mutex 为静态或全局变量,则可以用宏来初始化,后续不用 destroy
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
加锁与解锁
通过pthread库实现锁操作的函数定义
死锁
deadlock occurs when multiple processes within a group hold onto resources that cannot be released, resulting in each process being unable to proceed despite mutually requesting resources already held by other processes.
一个最简单的死锁代码:
#include <iostream>
#include <pthread.h>
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
int main()
{
pthread_mutex_lock(&mtx);
std::cout << "Lock once..." << std::endl;
pthread_mutex_lock(&mtx);
std::cout << "Deadlock generated." << std::endl;
pthread_mutex_unlock(&mtx);
return 0;
}
cpp

另一份死锁代码:
#include <iostream>
#include <pthread.h>
pthread_mutex_t mtx1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mtx2 = PTHREAD_MUTEX_INITIALIZER;
void* handler1(void* arg)
{
pthread_mutex_lock(&mtx1);
std::cout << "Lock mtx1." << std::endl;
pthread_mutex_lock(&mtx2);
std::cout << "Lock mtx1 and mtx2." << std::endl;
pthread_mutex_unlock(&mtx2);
std::cout << "Unlock mtx2." << std::endl;
pthread_mutex_unlock(&mtx1);
std::cout << "Unlock mtx1." << std::endl;
return nullptr;
}
void* handler2(void* arg)
{
pthread_mutex_lock(&mtx2);
std::cout << "Lock mtx2." << std::endl;
pthread_mutex_lock(&mtx1);
std::cout << "Lock mtx1 and mtx2." << std::endl;
pthread_mutex_unlock(&mtx1);
std::cout << "Unlock mtx1." << std::endl;
pthread_mutex_unlock(&mtx2);
std::cout << "Unlock mtx2." << std::endl;
return nullptr;
}
int main()
{
pthread_t tid1, tid2;
pthread_create(&tid1, nullptr, handler1, nullptr);
pthread_create(&tid2, nullptr, handler2, nullptr);
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
return 0;
}
cpp

死锁的四个必要条件
- 互斥原则:每个资源仅能被单个流程占用一次。
- 请求权保持原则:当流程因获取资源而被阻塞时需继续持有已获取的资源。
- 不得强行剥夺原则:流程不得在未耗尽现有资源前强行剥夺已有资源。
- 循环依赖机制:多个流程之间形成一种首尾相连的循环依赖关系。
如何避免死锁
- 破坏死锁的关键因素之一
- 加锁操作具有可预测性
- 确保资源使用后能够被正确释放
- 资源分配过程能够确保一次性的获取
条件变量
当一个线程以互斥的方式访问某个变量时,它可能会意识到在其他线程更改状态之前它无法进行任何操作。
例如在一个线程访问队列时 发现该队列为空 则该线程必须等待直到其他线程将一个节点添加到该队列中 这种情况下就需要使用条件变量
同步概念与竞态条件
互斥访问:为了确保数据安全,在线程允许按照特定顺序访问临界资源时进行互斥访问以防止饥饿问题称为互斥访问。
竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。
使用
创建与销毁
原型
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);int pthread_cond_destroy(pthread_cond_t *cond);
若 cond 为静态或全局变量,则可以用宏来初始化,后续不用 destroy
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
唤醒与等待
该接口用于实现条件信号量的基本操作。
其中:
pThreadCondSignal用于发送条件信号。pThreadCondBroadcast用于广播条件信号。pThreadCondWait用于等待当前线程满足指定条件后继续执行。
这些函数均需通过引用参数传递相关状态信息,并依赖于互斥锁机制来保证线程安全性。
Posix 信号量
Posix信号量与System V信号量功能相仿,在同步操作方面均具有无冲突的特点,并旨在实现无冲突地访问共享资源;然而Posix也可应用于线程间的同步操作
Linux
Linux
使用
创建与销毁
原型
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
参数:
pshared: 0 表示线程间共享,非 0 表示进程间共享
value:信号量初始值
P 操作
原型
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
V 操作
原型
int sem_post(sem_t *sem);
生产者消费者模型
这里会放一个超链接,待更新。
线程池
这里会放一个超链接,待更新。
读者写者问题
这里会放一个超链接,待更新。
