【Linux】Linux线程互斥与同步(接口篇)
文章目录
- 互斥量
- 互斥量的接口
- 互斥量实现原理探究
- 条件变量
- 条件变量的接口
- POSIX信号量
- POSIX信号量的接口
- 自旋锁
- 生产者消费者模型
- 基于BlockingQueue的生产者消费者模型
- (未完成)
- 基于环形队列的生产消费模型
- (未完成)
- 读者写者问题
- 读写锁
互斥量
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。多个线程并发的操作共享变量,会带来一些问题。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <functional>
#include <vector>
// 将线程封装一下
using func = std::function<void *(void *)>;
class mythread
{
public:
mythread(pthread_t tid, func fc, void *argument, std::string name = "none")
: _tid(tid), _fc(fc), _argument(argument), _name(name) {}
~mythread() {}
void Start() { pthread_create(&_tid, nullptr, threadroutine, this); }
const std::string& Name() { return _name; }
void Dubeg() { std::cout << typeid(_fc).name() << std::endl; }
void Detach() { pthread_detach(_tid); }
void Join() { pthread_join(_tid, nullptr); }
private:
// 执行任务
void Excute()
{
if (_argument == nullptr) _argument = this;
this->_fc(_argument);
}
static void *threadroutine(void *args)
{
mythread *ptd = static_cast<mythread *>(args);
ptd->Excute();
return nullptr;
}
private:
pthread_t _tid;
func _fc;
std::string _name;
void *_argument;
};
// 定义一个全局变量
int g_ticket = 10000;
void *headler(void* args)
{
mythread *ptd = reinterpret_cast<mythread *>(args);
// 买票逻辑
while (true)
{
if (g_ticket > 0)
{
usleep(1000);
std::cout << ptd->Name() << " g_ticket = " << g_ticket << std::endl;
--g_ticket;
}
else
{
break;
}
}
return nullptr;
}
int main()
{
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, nullptr);
// 创建5个线程
std::vector<mythread *> tds;
for (int i = 0;i < 5;++i)
{
std::string name = "thread-" + std::to_string(i+1);
pthread_t tid;
mythread *ptd = new mythread(tid, headler, nullptr, name);
tds.emplace_back(ptd);
}
// 让这批进程去工作
for (auto& td : tds)
{
td->Start();
}
sleep(1);
// 回收这批进程
for (auto &td : tds)
{
td->Join();
delete td;
}
return 0;
}
在这段代码中,我生成了 5 个线程去执行抢 10000 张票的逻辑,先看一下程序的运行结果:
这里可以看到,票不光被抢到了负数,而且在显示器上打印出来的信息也重合在一起了。
先来分析一下,为什么可能无法获得竞争结果?
- if 语句判断条件为真以后,代码可以并发的切换到其他线程
- usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段,所以导致打印的时候,可能会导致冲突
--ticket;
操作本身就不是一个原子操作
# 假设 ticket 是一个整型变量,存储在内存中
# 首先将 ticket 的值加载到寄存器 eax 中
movl ticket, %eax
# 将 eax 中的值减 1
subl $1, %eax
# 将减 1 后的值存储回 ticket 所在的内存位置
movl %eax, ticket
所谓原子操作,也可以理解为,翻译成汇编语言,只有一条语句的操作。--ticket;
操作转换成汇编,显然是有3条语句的,所以明显不是一个原子操作。
要解决以上问题,需要做到三点:
- 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
互斥量的接口
初始化互斥量
初始化互斥量有两种方法:
- 静态分配
// 定义一个全局的互斥量, 并使用 PTHREAD_MUTEX_INITIALIZER 来初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- 动态分配
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
参数说明:
- mutex:指向要初始化的 pthread_mutex_t 类型的互斥量的指针。
- attr:指向 pthread_mutexattr_t 类型的互斥量属性的指针,该属性可用于设置互斥量的特殊行为,如是否为递归互斥量、错误检查互斥量等。如果设置为 nullptr,则使用默认属性。(一般都是使用默认属性就好了,所以这里一般将 nullptr 传过去就好,如果需要使用其他属性需要使用
pthread_mutexattr_settype
函数去设置一个pthread_mutexattr_t
类型的变量)
返回值:
- 成功时,返回 0。
- 失败时,返回一个非零的错误码,可以使用 perror 或 strerror 函数打印错误信息。
销毁互斥量
销毁互斥量需要注意:
- 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
- 不要销毁一个已经加锁的互斥量
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数说明:
- mutex:一个指向 pthread_mutex_t 类型互斥量的指针,该互斥量应该是之前通过
- pthread_mutex_init 函数初始化过的。
返回值:
- 成功时,返回 0。
- 失败时,返回一个非零的错误码,你可以使用 perror 或 strerror 函数来查看错误信息。
互斥量加锁和解锁
#include <pthread.h>
// 加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
参数说明:
- mutex:指向 pthread_mutex_t 类型互斥量的指针,该互斥量在使用前应该已经通过 pthread_mutex_init 函数初始化(对于动态分配的互斥量)或者使用 PTHREAD_MUTEX_INITIALIZER 宏初始化(对于静态分配的互斥量)
函数作用:
pthread_mutex_lock
函数的主要作用是锁定一个互斥量。
当互斥量处于未锁定状态时,该函数会立即锁定互斥量并返回 0,表示成功。
当互斥量已经被另一个线程锁定时,调用该函数的线程将被阻塞,直到互斥量被解锁。这种阻塞确保了在任何时刻只有一个线程可以访问受互斥量保护的临界区,从而避免了多线程环境下的数据竞争和不一致性问题。
#include <pthread.h>
// 解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数说明:
- mutex:指向 pthread_mutex_t 类型互斥量的指针,该互斥量应该是之前已经被锁定的。
函数作用:
pthread_mutex_unlock
函数的主要作用是解锁一个互斥量。
当调用此函数时,会释放互斥量的锁定状态,允许其他线程锁定该互斥量。
只有拥有锁的线程才能解锁互斥量,如果一个线程试图解锁一个它没有锁定的互斥量,会导致未定义行为。
以上两个接口的返回值都是:
- 成功时,返回 0。
- 失败时,返回一个非零的错误码。可以使用 perror 或 strerror 函数来打印错误信息。
我们根据上面说的内容,来改写一下之前的代码:
// 定义一个全局变量
int g_ticket = 10000;
// 定义一把静态锁
pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;
void *headler(void* args)
{
mythread *ptd = reinterpret_cast<mythread *>(args);
// 买票逻辑
while (true)
{
// 上锁
pthread_mutex_lock(&g_mutex);
if (g_ticket > 0)
{
usleep(1000);
std::cout << ptd->Name() << " g_ticket = " << g_ticket << std::endl;
--g_ticket;
// 解锁
pthread_mutex_unlock(&g_mutex);
}
else
{
pthread_mutex_unlock(&g_mutex);
break;
}
}
return nullptr;
}
运行结果:
互斥量实现原理探究
- 经过上面的例子,我们已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题
- 为了实现互斥锁操作,大多数体系结构都提供了
swap
或exchange
指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码改一下
lock:
move $0, %al
xchgb %al, mutex
if (al寄存器的内容 > 0) {
return0;
} else
挂起等待;
goto lock; #跳转到 lock 处重新检查
unlcok:
movb $1, mutex
唤醒等待mutex的线程;
return 0;
lock 操作:
unlock 操作
条件变量
条件变量是一种同步机制,通常与互斥量一起使用,用于线程间的通信和同步。它允许一个或多个线程等待某个条件的发生,当该条件满足时,这些等待的线程可以被唤醒继续执行。条件变量主要解决的问题是避免线程不断轮询某个条件是否满足,从而减少 CPU 资源的浪费。
工作原理:
- 等待机制:当一个线程需要等待某个条件满足时,它会先获取互斥锁,然后检查条件是否满足。如果条件不满足,线程会在条件变量上调用等待函数,这个操作会自动释放互斥锁,并将线程置于阻塞状态,使该线程进入等待队列。
- 通知机制:当另一个线程修改了共享资源的状态,使得等待的条件满足时,它会在同一个条件变量上调用通知函数。通知函数会唤醒一个或多个在该条件变量上等待的线程。被唤醒的线程会重新获取互斥锁,然后再次检查条件是否满足,以确保在被唤醒后条件仍然是满足的,之后才会继续执行后续的操作。
条件变量的接口
初始化条件变量
初始化条件变量有两种形式,与互斥量相似:
- 静态初始化
pthread_cond_t my_cond = PTHREAD_COND_INITIALIZER;
- 动态初始化:对于具有自动存储期或动态分配的条件变量,使用 pthread_cond_init 函数进行动态初始化:
函数原型:
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
参数说明:
cond
:指向pthread_cond_t
类型的条件变量的指针,该指针指向的条件变量将被初始化。attr
:指向pthread_condattr_t
类型的属性对象的指针,用于设置条件变量的属性。如果传递nullptr
,则使用默认属性初始化条件变量。
返回值
- 成功时,函数返回 0。
- 失败时,函数返回一个错误代码,该代码表示错误的类型,可以使用 perror 或 strerror 函数来打印错误信息。
销毁条件变量
销毁条件变量需要注意:
- 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的条件变量不需要销毁
- 不要销毁一个正在阻塞中的条件变量
- 已经销毁的条件变量,要确保后面不会有线程再尝试使用
函数原型
int pthread_cond_destroy(pthread_cond_t *cond);
参数说明
cond
:指向要销毁的pthread_cond_t
类型的条件变量的指针。
等待条件变量满足
函数原型
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
参数说明
cond
:指向pthread_cond_t
类型的条件变量的指针,该指针指向线程要等待的条件变量。mutex
:指向pthread_mutex_t
类型的互斥锁的指针,该互斥锁必须在调用pthread_cond_wait
之前已经被锁定。
工作原理
- 当一个线程调用
pthread_cond_wait
时,它会执行以下操作: - 首先,它会自动释放由
mutex
所指向的互斥锁,这允许其他线程可以获取该互斥锁并修改共享数据。 - 然后,该线程会进入阻塞状态,等待
cond
所指向的条件变量被通知。 - 当条件变量被其他线程通过
pthread_cond_signal
或pthread_cond_broadcast
通知时,该线程会被唤醒。 - 被唤醒的线程会重新竞争互斥锁
mutex
,然后继续执行后续的操作。
唤醒等待
这里介绍两种唤醒等待的函数:
pthread_cond_signal
函数原型
int pthread_cond_signal(pthread_cond_t *cond);
参数说明
cond
:指向 pthread_cond_t 类型的条件变量的指针,该指针指向的条件变量将被用来通知等待的线程。
工作原理
pthread_cond_signal
函数会唤醒一个等待在cond
所指向的条件变量上的线程。
如果有多个线程正在等待,具体唤醒哪个线程是不确定的,通常是由操作系统的调度器决定的。
当pthread_cond_signal
被调用时,它会通知一个等待队列中的线程,被通知的线程将从pthread_cond_wait
的阻塞状态中被唤醒,然后重新获取之前释放的互斥锁,并继续执行后续操作。
pthread_cond_broadcast
函数原型
int pthread_cond_broadcast(pthread_cond_t *cond);
参数说明
cond
:指向pthread_cond_t
类型的条件变量的指针,该指针指向的条件变量将被用来通知等待的线程。
工作原理
pthread_cond_broadcast
函数会唤醒所有等待在cond
所指向的条件变量上的线程。- 与
pthread_cond_signal
不同,它会将所有等待的线程从阻塞状态中唤醒,而不是只唤醒一个。 - 当多个线程在
pthread_cond_wait
上等待同一个条件变量时,调用pthread_cond_broadcast
会导致所有这些线程都被唤醒,它们将依次重新获取互斥锁并继续执行后续操作。
两个唤醒等待函数的区别:
pthread_cond_signal
只会唤醒一个等待的线程,而pthread_cond_broadcast
会唤醒所有等待的线程。pthread_cond_signal
通常用于只需要一个线程来响应条件变化的情况,以避免多个线程同时竞争资源。pthread_cond_broadcast
则适用于多个线程都需要对条件变化做出响应的场景。
用上面介绍的接口来写一段测试代码:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int shared_variable = 0;
#define THRESHOLD 5
// 等待线程函数
void* wait_thread(void* arg) {
pthread_mutex_lock(&mutex);
// 循环检查条件
while (shared_variable < THRESHOLD) {
printf("Wait thread: Waiting for shared_variable to reach %d...\n", THRESHOLD);
// 等待条件变量
pthread_cond_wait(&cond, &mutex);
printf("Wait thread: Woke up.\n");
}
printf("Wait thread: shared_variable has reached %d.\n", THRESHOLD);
pthread_mutex_unlock(&mutex);
return NULL;
}
// 信号线程函数
void* signal_thread(void* arg) {
for (int i = 0; i < 10; i++) {
pthread_mutex_lock(&mutex);
shared_variable++;
printf("Signal thread: Incremented shared_variable to %d.\n", shared_variable);
if (shared_variable >= THRESHOLD) {
// 发出信号
pthread_cond_signal(&cond);
printf("Signal thread: Sent signal.\n");
}
pthread_mutex_unlock(&mutex);
// 模拟一些工作
sleep(1);
}
return NULL;
}
int main() {
pthread_t wait_tid, signal_tid;
// 创建等待线程和信号线程
if (pthread_create(&wait_tid, NULL, wait_thread, NULL) != 0 ||
pthread_create(&signal_tid, NULL, signal_thread, NULL) != 0) {
perror("pthread_create failed");
return EXIT_FAILURE;
}
// 等待线程结束
if (pthread_join(wait_tid, NULL) != 0 ||
pthread_join(signal_tid, NULL) != 0) {
perror("pthread_join failed");
return EXIT_FAILURE;
}
// 销毁互斥锁和条件变量
if (pthread_mutex_destroy(&mutex) != 0 ||
pthread_cond_destroy(&cond) != 0) {
perror("pthread_mutex_destroy or pthread_cond_destroy failed");
return EXIT_FAILURE;
}
return 0;
}
对这段代码的大致介绍:
初始化:
- 定义了一个互斥锁 mutex 和一个条件变量 cond 并进行初始化。
- shared_variable 是一个共享变量,THRESHOLD 是一个阈值。
等待线程函数 wait_thread:
- 首先锁定互斥锁。
- 使用 while 循环检查 shared_variable 是否小于 THRESHOLD。如果小于,调用 pthread_cond_wait 等待条件变量,该函数会自动释放互斥锁并使线程阻塞。
- 当线程被唤醒时,会重新获取互斥锁,继续执行后续代码。
信号线程函数 signal_thread:
- 对 shared_variable 进行递增操作。
- 当 shared_variable 大于等于 THRESHOLD 时,调用 pthread_cond_signal 发出信号,唤醒一个等待在条件变量上的线程。
主函数 main:
- 创建等待线程和信号线程。
- 等待两个线程结束。
- 销毁互斥锁和条件变量。
伪唤醒的概念
伪唤醒是指线程在没有收到 pthread_cond_signal
或 pthread_cond_broadcast
通知的情况下,从 pthread_cond_wait
函数中返回。这种情况可能由于多种原因发生,比如操作系统内部的调度策略、硬件中断等。
在上面的代码中,我们使用 while 循环来检查条件,而不是 if 语句。这是因为即使线程被唤醒,也不能保证 shared_variable 已经达到了 THRESHOLD。如果使用 if 语句,线程可能在伪唤醒后继续执行,而不再次检查条件,从而导致错误的结果。而使用 while 循环,线程在被唤醒后会再次检查条件,如果条件不满足,会继续等待。这样可以避免伪唤醒带来的问题,确保线程在正确的条件下继续执行。
例如,即使信号线程没有发出信号,等待线程也可能从 pthread_cond_wait 返回。此时,由于使用了 while 循环,线程会再次检查 shared_variable < THRESHOLD 的条件,如果条件仍然满足,会再次进入 pthread_cond_wait 等待。
POSIX信号量
POSIX 信号量本质上是一个计数器,它的值表示可用资源的数量。主要用于控制对共享资源的访问,防止多个线程或进程同时访问同一资源而引发的竞态条件。当一个线程或进程需要访问共享资源时,它会尝试减少信号量的值;当使用完资源后,会增加信号量的值。
POSIX信号量的接口
类型
有名信号量:有名信号量以文件系统路径名作为标识,可以在不同进程间共享,适用于不相关的进程间同步。
创建或打开:
#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag, mode_t mode,
unsigned int value);
name
:信号量的名称,是文件系统中的路径名。oflag
:标志位,如O_CREAT表示创建信号量。mode
:信号量的权限,类似文件权限。value
:信号量的初始值。
关闭
int sem_close(sem_t *sem);
sem
:指向信号量的指针。
删除
int sem_unlink(const char *name);
name
:信号量的名称。
无名信号量:无名信号量存于内存中,通过内存共享的方式供线程或相关进程使用,常用于线程间同步。
初始化信号量
函数原型:
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数解释
sem
:这是一个指向sem_t
类型对象的指针,sem_t
是 POSIX 标准定义的信号量类型,此指针指向你要初始化的信号量。pshared
:该参数用于指定信号量的共享范围,有两种取值情况:- 当
pshared
为 0 时,表明信号量仅在当前进程的各个线程间共享。这种情况下,信号量存储在进程的共享内存区域,所有线程都能访问。 - 当
pshared
不为 0 时,表示信号量可以在多个进程间共享。不过,要实现进程间共享,信号量必须存于多个进程都能访问的共享内存区域。
- 当
value
:此为信号量的初始值。信号量本质上是一个计数器,value 代表初始的可用资源数量。例如,若 value 设为 1,表示一开始有一个资源可供使用;若设为 0,则意味着初始时没有可用资源。
返回值
- 若函数调用成功,返回值为 0。
- 若调用失败,返回 -1,并且会设置 errno 来指明具体的错误类型。
错误情况
- EINVAL:可能是 sem 指针无效,或者 value 的值超过了系统所允许的最大值。
- ENOSYS:系统不支持进程间共享的信号量(当 pshared 不为 0 时)。
销毁信号量
#include <semaphore.h>
int sem_destroy(sem_t *sem);
参数说明
sem
:这是一个指向 sem_t 类型对象的指针,sem_t 是 POSIX 标准定义的信号量类型,此指针指向你要销毁的信号量。
错误情况
EBUSY
:表示仍有线程正在等待这个信号量,此时不能销毁该信号量。也就是说,若有线程正阻塞在该信号量的 sem_wait 操作上,调用 sem_destroy 就会返回此错误。EINVAL
:说明传入的 sem 指针无效,可能该指针没有指向一个有效的信号量对象。
使用注意事项
- 确保无等待线程:在调用
sem_destroy
之前,要保证没有线程正在等待该信号量。否则会返回 EBUSY 错误。通常需要在销毁信号量前,确保所有相关线程都已完成对信号量的操作。 - 已初始化的信号量:
sem_destroy
只能用于销毁通过sem_init
初始化的无名信号量。对于有名信号量,应使用sem_unlink
进行删除操作。 - 避免重复销毁:不要对同一个信号量多次调用
sem_destroy
,因为重复销毁可能会导致未定义行为。
通用操作
P 操作(等待):
函数原型:
#include <semaphore.h>
int sem_wait(sem_t *sem);
参数说明
sem
:这是一个指向 sem_t 类型信号量对象的指针,sem_t 是 POSIX 标准定义的信号量数据类型,该指针指向要操作的目标信号量。
功能与原理
sem_wait 函数实现的是信号量的 P 操作(也叫等待操作)。它的主要作用是尝试获取信号量所代表的资源。具体执行过程如下:
- 当信号量的值大于 0 时,sem_wait 会将信号量的值减 1,然后函数立即返回,这意味着调用线程或进程成功获取了资源,可以继续执行后续的操作。
- 当信号量的值等于 0 时,调用 sem_wait 的线程或进程会被阻塞,进入等待状态,直到信号量的值大于 0 。一旦信号量的值变为大于 0,线程或进程会被唤醒,将信号量的值减 1 后继续执行。
返回值
- 若操作成功,函数返回 0。
- 若操作失败,函数返回 -1,并且会设置 errno 来表明具体的错误类型。常见的错误情况包括传入的 sem 指针无效(EINVAL)等。
V 操作(发布):
函数原型
#include <semaphore.h>
int sem_post(sem_t *sem);
参数说明
·sem
:同样是指向sem_t
类型信号量对象的指针,代表要操作的信号量。
功能与原理
sem_post
函数实现的是信号量的 V 操作(也叫释放操作)。其主要功能是释放信号量所代表的资源。具体来说,当调用sem_post
时,它会将信号量的值加 1。如果此时有其他线程或进程因为调用sem_wait
而处于等待状态,那么其中一个等待的线程或进程会被唤醒,去尝试获取资源。
返回值
若操作成功,函数返回 0。
若操作失败,函数返回 -1,同时会设置 errno
来指示具体的错误,比如 sem
指针无效(EINVAL)等。
用上面介绍的接口来写一段测试代码
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
sem_t semaphore;
// 线程函数
void* thread_function(void* arg) {
// P 操作,等待信号量
if (sem_wait(&semaphore) == -1) {
perror("sem_wait");
return NULL;
}
printf("Thread entered critical section.\n");
// 模拟一些工作
sleep(2);
printf("Thread leaving critical section.\n");
// V 操作,释放信号量
if (sem_post(&semaphore) == -1) {
perror("sem_post");
return NULL;
}
return NULL;
}
int main() {
pthread_t thread;
// 初始化信号量,初始值为 1,仅在本进程的线程间共享
if (sem_init(&semaphore, 0, 1) == -1) {
perror("sem_init");
return 1;
}
// 创建线程
if (pthread_create(&thread, NULL, thread_function, NULL) != 0) {
perror("pthread_create");
return 1;
}
// 主线程等待子线程结束
if (pthread_join(thread, NULL) != 0) {
perror("pthread_join");
return 1;
}
// 销毁信号量
if (sem_destroy(&semaphore) == -1) {
perror("sem_destroy");
return 1;
}
return 0;
}
代码解释
- 信号量初始化:在 main 函数里调用 sem_init 对信号量 semaphore 进行初始化,pshared 设为 0 表示仅在本进程的线程间共享,value 设为 1 意味着初始时有一个资源可用。
- 线程创建:创建一个新线程并执行 thread_function。
- 信号量操作:在 thread_function 中,先调用 sem_wait 进行 P 操作,尝试获取资源;完成工作后,调用 sem_post 进行 V 操作,释放资源。
- 信号量销毁:主线程等待子线程结束后,调用 sem_destroy 销毁信号量,释放相关资源。
自旋锁
概念
- 自旋锁是一种忙等待的锁机制。当一个线程尝试获取自旋锁时,如果锁已经被其他线程持有,该线程不会进入阻塞状态,而是会不断地循环检查锁是否已经被释放,直到获取到锁为止。
使用场景
适用于锁的持有时间较短、线程不希望在获取锁时进行上下文切换的场景。例如,在一些对性能要求极高的系统中,如操作系统内核的某些关键代码段,可以使用自旋锁来减少上下文切换的开销。
优缺点
- 优点:避免了线程上下文切换的开销,在锁的持有时间较短的情况下,性能较高。
- 缺点:如果锁的持有时间较长,会浪费 CPU 资源,因为线程会一直处于忙等待状态。
示例
相关函数
初始化自旋锁:
int pthread_spin_init(pthread_spinlock_t *lock, int pshared)
,用于初始化一个自旋锁。lock
是指向要初始化的自旋锁的指针,pshared
参数指定自旋锁的共享属性,通常取值为PTHREAD_PROCESS_PRIVATE
(用于同一进程内的线程共享)或PTHREAD_PROCESS_SHARED
(用于不同进程间的线程共享)。
获取自旋锁:
int pthread_spin_lock(pthread_spinlock_t *lock)
,线程调用该函数来获取自旋锁。如果锁已经被其他线程持有,调用线程会在原地自旋等待,直到锁可用。
释放自旋锁:
int pthread_spin_unlock(pthread_spinlock_t *lock)
,用于释放自旋锁,使其他等待的线程有机会获取锁。
销毁自旋锁:
int pthread_spin_destroy(pthread_spinlock_t *lock)
,用于销毁自旋锁,释放相关资源。在自旋锁被销毁后,不能再对其进行获取或释放操作。
生产者消费者模型
生产者 - 消费者模型是一个经典的多线程并发编程模型,用于解决多线程环境下数据的生产和消费问题。它描述了两类线程(生产者线程和消费者线程)如何通过共享缓冲区进行协作,从而实现数据的高效处理。下面从多个方面对该模型进行详细介绍。
模型概述
- 生产者:负责生成数据,并将数据放入共享缓冲区。生产者线程会持续生产数据,直到缓冲区达到上限。
- 消费者:负责从共享缓冲区中取出数据并进行处理。消费者线程会不断消费数据,直到缓冲区为空。
- 共享缓冲区:作为生产者和消费者之间的数据交换场所,它可以是数组、队列等数据结构。缓冲区的大小通常是有限的,以避免无限增长导致的内存问题。
基于BlockingQueue的生产者消费者模型
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的线程在对阻塞队列进程操作时会被阻塞)
当阻塞队列是空的时候,从队列中获取元素将会被阻塞,直到其他线程插入数据;
当阻塞队列是满的时候,往队列中插入元素将会被阻塞,直到其他线程取走数据。
(未完成)
代码实现:
基于环形队列的生产消费模型
环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留一个空的位置,作为满的状态
- 但是我们现在有信号量这个计数器,就很简单的进行多线程间的同步过程
(未完成)
代码实现:
读者写者问题
有一个共享数据区,例如一个文件或者一段内存,有多个读者进程和多个写者进程需要访问这个共享数据区。要求满足以下条件:
- 多个读者可以同时读共享数据区,不会产生数据不一致问题。
- 写者和写者之间不能同时写,因为这会导致数据冲突和不一致。
- 读者和写者之间也需要进行同步,不能让读者和写者同时访问共享数据区,否则可能导致读者读到不完整或不一致的数据,或者写者的写入被读者干扰
我们使用伪代码来看一下读者写者是如何实现的
读写锁
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。
注意:写独占,读共享,读锁优先级高
看一看系统中读写锁的接口
初始化读写锁:
函数原型
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
参数说明
rwlock
:指向要初始化的读写锁对象的指针,类型为pthread_rwlock_t
。在使用读写锁的任何其他操作之前,必须先通过pthread_rwlock_init
对其进行初始化。attr
:指向读写锁属性对象的指针,类型为pthread_rwlockattr_t
。它用于指定读写锁的各种属性,如共享属性等。如果为NULL,则使用默认属性。
返回值
- 成功时,函数返回0。
- 失败时,函数返回一个非零的错误码,常见的错误码包括EAGAIN(资源不足)、ENOMEM(内存不足)等。
功能作用
- 初始化读写锁状态:为读写锁分配必要的内部资源,使其处于可使用状态,准备好被线程用于对共享资源的读写保护。
- 设置读写锁属性:根据传入的attr参数,设置读写锁的各种属性。例如,可以通过属性设置来决定读写锁是进程内共享还是可以在不同进程间共享。
销毁读写锁
函数原型
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
参数说明
rwlock
:指向要销毁的读写锁对象的指针,类型为pthread_rwlock_t
。这个读写锁必须是已经通过pthread_rwlock_init
成功初始化过的,且当前没有任何线程持有该读写锁,否则结果是未定义的。
返回值
- 成功时,函数返回0。
- 失败时,返回一个非零的错误码,不过在正常使用情况下,如果传入的参数正确且读写锁状态正常,一般不会出现失败的情况。
功能作用
- 释放资源:当不再需要使用读写锁时,pthread_rwlock_destroy函数用于释放与该读写锁相关的所有资源。这包括读写锁内部使用的各种数据结构和可能分配的内存等资源,防止资源泄漏。
- 使读写锁无效:将读写锁对象设置为无效状态,此后对该读写锁的任何操作(除了再次调用
pthread_rwlock_init
进行初始化)都是未定义行为。这确保了不会意外地对已经销毁的读写锁进行操作,从而避免程序出现不可预测的错误。
注意事项
- 在调用
pthread_rwlock_destroy
之前,必须确保所有线程都已经释放了该读写锁,即没有任何线程正在持有读锁或写锁。否则,可能会导致程序出现错误,例如死锁或其他未定义行为。 - 销毁操作是不可逆的,一旦读写锁被销毁,就不能再直接使用它,必须重新调用
pthread_rwlock_init
进行初始化后才能再次使用。
上锁
函数原型
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
参数说明
rwlock
:指向pthread_rwlock_t
类型的读写锁对象的指针。这个读写锁对象必须已经通过pthread_rwlock_init
函数初始化,否则函数调用结果是未定义的。
返回值
- 成功:函数返回 0,表示当前线程成功获取到了读写锁的读锁。
- 失败:返回一个非零的错误码,常见的错误码及含义如下:
- EDEADLK:当前线程已经持有该读写锁的写锁,再尝试获取读锁时会产生死锁,因此返回此错误。
- EINVAL:传入的 rwlock 指针无效,比如该指针指向的读写锁未被正确初始化。
功能与工作原理
- 功能:
pthread_rwlock_rdlock
的主要功能是让调用线程获取读写锁的读锁。读写锁允许多个线程同时持有读锁,因为读操作通常不会修改共享资源,所以多个线程可以并发地进行读操作,提高程序的并发性能。 - 工作原理:当一个线程调用
pthread_rwlock_rdlock
时,会检查读写锁的当前状态:- 如果此时没有线程持有写锁,那么该线程可以立即获取读锁,函数返回 0,并且读写锁的读锁计数会加 1。
- 如果此时有线程持有写锁,那么调用线程会被阻塞,直到写锁被释放。一旦写锁被释放,阻塞的线程会被唤醒并尝试获取读锁。
使用场景
pthread_rwlock_rdlock
适用于多线程环境下对共享资源进行读操作的场景,且读操作远远多于写操作的情况。例如,在一个缓存系统中,多个线程可能会同时读取缓存中的数据,而只有少数线程会对缓存进行更新操作。这时就可以使用读写锁,多个读线程可以通过 pthread_rwlock_rdlock
同时获取读锁来读取缓存数据,而写线程则需要获取写锁来更新缓存。
函数原型
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock)
参数说明
rwlock
:指向pthread_rwlock_t
类型的读写锁变量的指针。这个读写锁变量用于保护共享资源,确保在任何时刻只有一个线程能够对共享资源进行写操作,或者允许多个线程同时进行读操作,但读和写操作不能同时进行。
返回值
- 函数成功执行时,返回值为 0。
- 如果函数执行失败,会返回一个非零的错误码,常见的错误码包括:
EDEADLK
:表示发生了死锁,即当前线程试图获取的锁已经被它自己持有,或者由于锁的获取顺序不当等原因导致了死锁情况的发生。EINTR
:表示函数在获取锁的过程中被信号中断。在这种情况下,函数可能没有成功获取锁,需要根据具体的应用场景来决定是否需要重新尝试获取锁。EAGAIN
:表示无法立即获取锁,可能是因为系统资源不足或者已经达到了最大的锁递归深度等原因。
函数功能
该函数用于获取一个读写锁的写锁。当一个线程调用 pthread_rwlock_wrlock
时,如果此- 时没有其他线程持有该读写锁的读锁或写锁,那么当前线程将成功获取写锁,并可以对共享资源进行写操作。如果已经有其他线程持有读锁或写锁,那么当前线程会被阻塞,直到可以获取写锁为止。
解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
参数说明:
rwlock
是一个指向pthread_rwlock_t
类型对象的指针,该对象代表要释放的读写锁。此读写锁必须已经被当前线程通过相应的加锁函数成功获取,不然调用结果是未定义的。
返回值
- 成功:函数调用成功时返回 0。
- 失败:通常情况下该函数不会失败,但如果传入的 rwlock 指针无效(例如该指针为空或者指向未初始化的读写锁),可能会出现未定义行为。不过 POSIX 标准并未规定具体的错误返回码。
功能概述
在多线程编程里,读写锁是一种特殊的锁机制,它把对共享资源的访问分成读操作和写操作。允许多个线程同时进行读操作,但写操作是独占的,即同一时间只能有一个线程进行写操作,并且写操作进行时不允许其他线程进行读操作。pthread_rwlock_unlock
函数的作用就是释放之前通过 pthread_rwlock_rdlock
(获取读锁)或者 pthread_rwlock_wrlock
(获取写锁)获取到的锁,让其他等待该锁的线程有机会访问共享资源。