Linux —— 线程同步
我们今天接着来了解线程:
死锁
死锁(Deadlock)是计算机科学中多任务处理和并发控制时的一种现象,特别是在操作系统、数据库系统和网络系统中较为常见。它指的是两个或两个以上的进程(或线程)在执行过程中,因为互相等待对方持有的资源而无法继续执行的情况,从而导致所有的进程都无法向前推进,系统状态僵死。
死锁发生的四个必要条件为:
- 互斥条件:资源不能被多个进程同时使用,只能由一个进程占有。
- 请求与保持条件:已经持有至少一个资源的进程可以再请求新的资源。
- 不剥夺条件:已分配给一个进程的资源在该进程未明确释放之前,不能被其他进程强行夺走。
- 循环等待条件:存在一种进程资源的循环等待链,链中的每个进程已获得的资源同时被链中下一个进程所请求。
避免死锁的策略通常包括:
- 破坏四个必要条件之一:例如,通过实施资源分配的排序策略来破坏循环等待条件,或者允许进程在等待新资源时释放已持有的资源以破坏请求与保持条件。
- 银行家算法:这是一种避免死锁的著名算法,通过预判资源分配请求是否会导致系统进入不安全状态来决定是否分配资源。
- 死锁检测与恢复:系统定期检查是否存在死锁,一旦检测到死锁,则采取相应措施恢复,比如终止一部分进程,收回资源重新分配。
- 超时重试:为避免长时间阻塞,可以为资源请求设置超时,超时后回滚并重试请求。
理解并有效管理这些机制对于设计高效、稳定的多线程和分布式系统至关重要。
以我们现在的水平,死锁这个现象并不常见,大家了解一下即可。
线程同步
线程同步是指在多线程编程中,为了防止多个线程访问同一资源造成数据不一致、数据竞争等问题,确保线程按照预定的顺序执行而采取的一系列协调机制。其目的是确保在任一时刻,只有一个线程能够访问共享资源,其他线程则需要等待,直到该资源被释放。
线程同步的方法和机制多种多样,以下是一些常见的同步技术:
- 互斥锁(Mutex):最基本的同步原语,用于保护临界区(即访问共享资源的代码段)。当一个线程获得了互斥锁后,其他试图获取同一锁的线程将被阻塞,直到第一个线程释放锁。
- 信号量(Semaphore):信号量是一个更通用的同步工具,不仅可以实现互斥锁的功能,还能控制对有限资源的访问数量。信号量维护一个计数器,线程可以增加或减少这个计数器的值,当计数器为非正时,试图减少它的线程会被阻塞。
- 条件变量(Condition Variable):条件变量用于线程间的同步,允许一个或多个线程等待某个特定条件发生。当条件不满足时,线程可以阻塞自己,直到另一个线程通知条件已满足。
- 读写锁(Read-Write Lock):适用于读操作远多于写操作的场景。允许多个读者同时访问资源,但写者访问时会排斥所有其他读写者。这提高了并发性能。
- 原子操作(Atomic Operations):提供了一种方法来执行简单操作(如增加、减少、交换等),这些操作在多线程环境中被视为不可分割的,即操作过程中不会被其他线程打断。
- 屏障(Barrier):屏障是一个同步点,所有线程到达这个点后才会继续执行。这对于需要所有线程完成某阶段工作后再一起进入下一阶段的场景非常有用。
正确地使用线程同步机制是编写并发程序的关键,能够有效地避免竞态条件、死锁等问题,保证程序的正确性和一致性。
我们实现同步,一般会用条件变量:
条件变量
条件变量是多线程编程中用于线程同步的一种机制,它允许线程在某些条件未满足时等待,直到其他线程改变了这个条件后再唤醒它们。条件变量通常与互斥锁一起使用,以防止多个线程同时修改共享数据,并且确保在条件不满足时线程能够安全地等待。
在使用条件变量的经典模式中,涉及以下几个步骤:
- 加锁:在检查或修改共享数据之前,线程首先需要获取一个关联的互斥锁,以确保独占访问。
- 检查条件:线程检查某个条件是否满足。如果条件已经满足,线程可以继续执行。如果没有满足,则进入下一步。
- 等待:如果条件没有满足,线程调用条件变量的等待函数(如
pthread_cond_wait
在POSIX线程中)并释放互斥锁。这样做的目的是让出CPU给其他线程,并且让自己处于等待状态,直到被其他线程通过条件变量的信号函数(如pthread_cond_signal
或pthread_cond_broadcast
)唤醒。- 被唤醒后重新检查条件:当线程被唤醒时,它会重新获取互斥锁并再次检查条件是否满足。这是必要的,因为可能存在“虚假唤醒”情况,即线程被唤醒并不是因为预期的条件改变,而是其他原因。
- 执行或再次等待:如果条件满足,线程可以继续执行其后续逻辑。如果不满足,线程可能需要再次调用等待函数,重复上述过程。
条件变量的主要目的是提供一种优雅的方式,让线程能够高效地等待某个条件,并且只有当条件变为真时才继续执行,从而实现了线程间的协调和同步。在多线程编程中,正确使用条件变量可以避免数据竞争和竞态条件,提高程序的健壮性。
我们这里举个简单的例子,我们创建三个线程,向屏幕循环打印信息:
#include<iostream>
#include<pthread.h>
#include<unistd.h>
void* threadRouite(void* args)
{
const char* name = static_cast<const char*>(args);
while(true)
{
std::cout << "I am running my name is: "<< name << std::endl;
sleep(1);
}
return nullptr;
}
int main()
{
//创建线程
pthread_t tid1,tid2,tid3; //线程id
pthread_create(&tid1,nullptr,threadRouite,(void*)"thread-1");
pthread_create(&tid2,nullptr,threadRouite,(void*)"thread-2");
pthread_create(&tid3,nullptr,threadRouite,(void*)"thread-3");
pthread_join(tid1,nullptr);
pthread_join(tid2,nullptr);
pthread_join(tid3,nullptr);
}
这个时候会有线程竞争的问题,这个时候我们可以上锁,保证在一个时间段内,只有一个线程往屏幕打印。
这个时候会有一个问题,很长的一段时间都是thread-1运行,线程2,线程3都没有机会执行,为了让三个线程相对均匀的被调动,我们这个时候就要使用条件变量:
上完锁之后,我们让所有线程等待:
pthread_cond_wait
#include<iostream>
#include<pthread.h>
#include<unistd.h>
//上锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //
void* threadRouite(void* args)
{
const char* name = static_cast<const char*>(args);
while(true)
{
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond,&mutex); //让所有线程等待
std::cout << "I am running my name is: "<< name << std::endl;
sleep(1);
pthread_mutex_unlock(&mutex);
}
return nullptr;
}
int main()
{
//创建线程
pthread_t tid1,tid2,tid3; //线程id
pthread_create(&tid1,nullptr,threadRouite,(void*)"thread-1");
pthread_create(&tid2,nullptr,threadRouite,(void*)"thread-2");
pthread_create(&tid3,nullptr,threadRouite,(void*)"thread-3");
pthread_join(tid1,nullptr);
pthread_join(tid2,nullptr);
pthread_join(tid3,nullptr);
}
我们可以等3秒就唤醒一个线程:
pthread_cond_signal
#include<iostream>
#include<pthread.h>
#include<unistd.h>
//上锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //
void* threadRouite(void* args)
{
const char* name = static_cast<const char*>(args);
while(true)
{
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond,&mutex); //让所有线程等待
std::cout << "I am running my name is: "<< name << std::endl;
sleep(1);
pthread_mutex_unlock(&mutex);
}
return nullptr;
}
int main()
{
//创建线程
pthread_t tid1,tid2,tid3; //线程id
pthread_create(&tid1,nullptr,threadRouite,(void*)"thread-1");
pthread_create(&tid2,nullptr,threadRouite,(void*)"thread-2");
pthread_create(&tid3,nullptr,threadRouite,(void*)"thread-3");
//3秒后,唤醒一个线程
sleep(1);
pthread_cond_signal(&cond);
//3秒后,唤醒一个线程
sleep(1);
pthread_cond_signal(&cond);
//3秒后,唤醒一个线程
sleep(1);
pthread_cond_signal(&cond);
pthread_join(tid1,nullptr);
pthread_join(tid2,nullptr);
pthread_join(tid3,nullptr);
}
打印三次之后,程序就像是被阻塞了一样,我们来分析一下:
初始状态
- 创建了三个线程(
tid1
,tid2
,tid3
),每个线程启动时会立刻调用pthread_cond_wait(&cond, &mutex)
,这意味着它们一启动就会因为条件变量cond
而阻塞,等待被其他线程通过pthread_cond_signal
或pthread_cond_broadcast
唤醒。- 主线程随后执行,它首先等待1秒,然后发送一次信号(
pthread_cond_signal(&cond)
)。这个信号会唤醒一个处于等待状态的线程(假设是tid1
),tid1
线程开始执行并打印消息,完成后再次调用pthread_cond_wait
进入等待状态。- 主线程再次等待1秒后,发送第二个信号,唤醒第二个线程(比如
tid2
)。- 同理,第三个信号唤醒了最后一个线程
tid3
。
为什么之后会“阻塞”
- 在每个线程被唤醒并执行后,它们会再次调用
pthread_cond_wait(&cond, &mutex)
。此时,由于主线程的循环已经完成,没有更多的pthread_cond_signal
调用来唤醒它们。因此,这三个线程都再次进入等待状态,期望着未来某个时刻收到信号。- 由于主线程没有进一步的信号发送动作,并且没有设计退出条件或循环来持续发送信号,这三个线程就这样一直等待下去,看起来就像“阻塞”了。
如何修改以持续运行
如果想要线程持续运行并周期性地被唤醒,一种方式是在主线程中加入循环,持续发送信号给条件变量。但是,直接无限制地发送信号可能导致不必要的频繁唤醒和资源消耗,因此应该根据具体需求来设计合理的唤醒逻辑。
#include<iostream>
#include<pthread.h>
#include<unistd.h>
//上锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //
void* threadRouite(void* args)
{
const char* name = static_cast<const char*>(args);
while(true)
{
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond,&mutex); //让所有线程等待
std::cout << "I am running my name is: "<< name << std::endl;
sleep(1);
pthread_mutex_unlock(&mutex);
}
return nullptr;
}
int main()
{
//创建线程
pthread_t tid1,tid2,tid3; //线程id
pthread_create(&tid1,nullptr,threadRouite,(void*)"thread-1");
pthread_create(&tid2,nullptr,threadRouite,(void*)"thread-2");
pthread_create(&tid3,nullptr,threadRouite,(void*)"thread-3");
while(true)
{
sleep(1);
pthread_cond_signal(&cond); //一直发送信号
}
pthread_join(tid1,nullptr);
pthread_join(tid2,nullptr);
pthread_join(tid3,nullptr);
}
这样每个线程都会被均匀的调度。
pthread_cond_broadcast
pthread_cond_broadcast会一次唤醒所有线程,至于谁能抢到运行的权利,就看各个线程的本事了:
#include<iostream>
#include<pthread.h>
#include<unistd.h>
//上锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //
void* threadRouite(void* args)
{
const char* name = static_cast<const char*>(args);
while(true)
{
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond,&mutex); //让所有线程等待
std::cout << "I am running my name is: "<< name << std::endl;
sleep(1);
pthread_mutex_unlock(&mutex);
}
return nullptr;
}
int main()
{
//创建线程
pthread_t tid1,tid2,tid3; //线程id
pthread_create(&tid1,nullptr,threadRouite,(void*)"thread-1");
pthread_create(&tid2,nullptr,threadRouite,(void*)"thread-2");
pthread_create(&tid3,nullptr,threadRouite,(void*)"thread-3");
while(true)
{
sleep(1);
pthread_cond_broadcast(&cond); //一直发送信号,唤醒所有线程
}
pthread_join(tid1,nullptr);
pthread_join(tid2,nullptr);
pthread_join(tid3,nullptr);
}
条件变量的接口
使用一些接口之后,我们来看看条件变量的接口:
在POSIX线程(pthread)库中,条件变量提供了以下主要接口用于线程间的同步:
-
初始化和销毁:
- 初始化条件变量:
int pthread_cond_init(pthread_cond_t *cv, const pthread_condattr_t *attr);
cv
是指向条件变量的指针,attr
是可选的条件变量属性(一般传NULL
使用默认属性)。 - 销毁条件变量:
释放与条件变量关联的资源。int pthread_cond_destroy(pthread_cond_t *cv);
- 初始化条件变量:
-
等待和唤醒:
- 等待条件变量:
当前线程会释放互斥锁int pthread_cond_wait(pthread_cond_t *cv, pthread_mutex_t *mutex);
mutex
并进入等待状态,直到其他线程通过pthread_cond_signal
或pthread_cond_broadcast
唤醒它。被唤醒后,会重新获取锁。 - 信号通知:
唤醒一个(至少一个)在条件变量int pthread_cond_signal(pthread_cond_t *cv);
cv
上等待的线程。具体唤醒哪个线程由实现决定。 - 广播通知:
唤醒所有在条件变量int pthread_cond_broadcast(pthread_cond_t *cv);
cv
上等待的线程。这通常用于需要唤醒所有线程的情况。
- 等待条件变量:
-
属性操作:
- 初始化属性对象:
初始化条件变量属性对象。int pthread_condattr_init(pthread_condattr_t *attr);
- 设置属性:
设置条件变量是否在进程间共享。int pthread_condattr_setpshared(pthread_condattr_t *attr, int pshared);
pshared
参数为PTHREAD_PROCESS_SHARED
表示跨进程共享,PTHREAD_PROCESS_PRIVATE
表示仅在同一进程中共享(默认)。 - 获取属性:
获取条件变量属性中的进程共享状态。int pthread_condattr_getpshared(const pthread_condattr_t *attr, int *pshared);
- 销毁属性对象:
释放条件变量属性对象占用的资源。int pthread_condattr_destroy(pthread_condattr_t *attr);
- 初始化属性对象:
这些接口共同构成了条件变量的核心功能,允许线程之间基于某些条件进行复杂的同步控制,是构建多线程程序中不可或缺的一部分。
抢票模拟
我们这里加上条件变量,实现一个补票的逻辑:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
// 初始化互斥锁,用于保护共享资源tickets
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 初始化条件变量,用于线程间通信,通知票已补充
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// 共享资源:剩余票数
int tickets = 100; // 初始有100张票
// 线程函数,模拟售票操作
void* threadRouite(void* args) {
const char* name = static_cast<const char*>(args); // 获取线程名称
while(true) { // 无限循环,直到手动中断
// 加锁,确保接下来的操作原子性
pthread_mutex_lock(&mutex);
// 检查是否有票可售
if(tickets > 0) {
tickets--; // 卖出一张票
std::cout << "Left tickets: " << tickets << " thread name: " << name << std::endl; // 打印剩余票数和售票线程名
} else {
std::cout << "no tickets" << std::endl; // 如果没票,打印提示
// 没有票时,线程等待,释放锁,直到被通知
pthread_cond_wait(&cond, &mutex);
}
// 操作完成后解锁
pthread_mutex_unlock(&mutex);
}
return nullptr; // 线程返回指针类型为空指针
}
int main() {
// 创建三个售票线程
pthread_t tid1, tid2, tid3;
pthread_create(&tid1, nullptr, threadRouite, (void*)"thread-1"); // 创建并启动线程1
pthread_create(&tid2, nullptr, threadRouite, (void*)"thread-2"); // 创建并启动线程2
pthread_create(&tid3, nullptr, threadRouite, (void*)"thread-3"); // 创建并启动线程3
// 主线程循环,每三秒检查一次是否需要补充票
while(true) {
sleep(3); // 等待3秒
// 加锁,保护票数的修改
pthread_mutex_lock(&mutex);
// 检查是否需要补充票
if(tickets == 0) {
tickets += 100; // 补充100张票
}
// 解锁
pthread_mutex_unlock(&mutex);
// 通知所有等待的线程,票已补充
pthread_cond_broadcast(&cond); // 使用广播,唤醒所有等待线程
}
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
pthread_join(tid3, nullptr);
return 0; // 程序正常结束
}
这段代码展示了一种简单的线程同步机制,其中售票线程会尝试减少票数,如果没有票则等待;主线程定期检查并补充票数,然后通过条件变量通知等待的线程。然而,正如注释中指出的,pthread_join
调用在当前代码结构中无法执行,因为main
函数中的无限循环没有提供退出机制。在实际应用中,应当设计合适的逻辑来终止循环,并确保所有线程能够正确地结束。