1.背景概念
#include <iostream>
#include <pthread.h>
#include <string>
#include <vector>
#include <unistd.h>
using namespace std;
struct customer
{pthread_t tid;string name;
};
int g_tickets = 1000;
//定义一个全局变量的锁,保证所有线程都用同一个锁
pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;
void *GetTicket(void *args)
{customer *c = (customer *)args;while (true){pthread_mutex_lock(&g_mutex); //加锁if (g_tickets > 0){usleep(1000);cout << c->name << " is getting a ticket " << g_tickets << endl;g_tickets--;pthread_mutex_unlock(&g_mutex); //解锁}else{pthread_mutex_unlock(&g_mutex); //解锁break;}}return nullptr;
}int main()
{vector<customer> custs(5);for (int i = 0; i < 5; i++){custs[i].name = "customer-" + to_string(i + 1);pthread_create(&custs[i].tid, nullptr, GetTicket, &custs[i]);}for (int i = 0; i < 5; i++){pthread_join(custs[i].tid, nullptr);}return 0;
}
例如上诉的抢票系统中,我们有5个线程来共享临界资源 g_tickets,为了保证临界资源的原子性,我们对其采用了锁。执行之后出现如下情况:
我们发现都是一个线程抢到票,即都是同一个线程占用了锁,这是因为线程在占用锁的时候,是不受控制的,这就有可能导致一个竞争能力强的线程,从头到尾都占用一个锁。为了总是避免一个线程占用锁,于是出现了线程同步的概念。
在Linux中,线程同步是指多个线程在执行过程中,通过一些机制来协调它们对数据和资源的访问,让线程能够按照某种特定的顺序访问临界资源,以避免竞态条件、数据不一致和饥饿等问题。线程同步可以通过多种方式实现,包括互斥锁、条件变量、读写锁和信号量等。
2.条件变量
条件变量是一种同步机制,它允许一个或多个线程在某个条件不满足时阻塞,并在条件满足时被唤醒。条件变量通常与互斥锁一起使用,以确保线程安全。在多线程编程中,条件变量用于协调线程之间的执行顺序,特别是在生产者-消费者问题、读者-写者问题等场景中非常有用。
条件变量的类型为 pthread_cond_t,与互斥量类型 pthread_mutex_t 相似。
创建条件变量
方法一,宏定义全局条件变量
pthread_cond_t xxx = PTHREAD_COND_INITIALIZER;
全局的条件变量必须用宏PTHREAD_COND_INITIALIZER
进行初始化,并且不需要手动销毁。
方法二,使用 pthread_cond_init 函数
int pthread_cond_init(pthread_cond_t * cond, const pthread_condattr_t * attr);
参数:
cond
:指向条件变量的指针,该变量在函数调用后将被初始化为条件变量。attr
:指向条件变量属性的指针,用于设置条件变量的属性。如果传递NULL
,则使用默认属性。
返回值:函数成功返回0,
失败则返回错误码
等待条件变量
我们要想让线程按一定顺序访问临界资源,就需要对这些线程进行排队,而这时我们就用到了队列来一个一个排队访问。
如上图有三个线程thread-1
,thread-2
,thread-3
,这三个线程争夺一个临界资源。
thread-1
申请到了锁mutex
,但是由于我们给这个临界资源添加了条件变量,此时thread-1
不能直接访问临界资源,而是进入等待队列,并且释放持有的锁:
由于锁被释放,后续线程可以继续申请这个锁。于是thread-2
和thread-3
也分别申请到了mutex
,通过相同的方式进入了等待队列:
现在所有线程都在等待队列中,这些线程因为没有满足特定条件,所以不能访问临界资源。但是为什么要进等待队列呢?
因为要保证线程同步,也就是说,一开始谁先申请到锁访问临界资源,那么后续条件满足时,就让谁先来访问这个资源。
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
参数:
cond
:指向条件变量的指针。mutex
:指向互斥锁的指针,该互斥锁在调用pthread_cond_wait
之前必须由当前线程持有的,而后线程释放该锁,从而让其他线程也可以申请锁,进入等待队列。
返回值:函数成功返回0,
失败则返回错误码。
pthread_cond_wait
函数的工作原理是将当前线程放入条件变量cond
的等待队列中,并自动释放mutex
互斥锁。
唤醒等待线程
线程进入等待队列进行排队后需要被唤醒,于是我们会使用如下函数
int pthread_cond_signal(pthread_cond_t *cond);
- cond: 指向条件变量的指针,说明要唤醒哪一个条件变量下等待的一个线程。
- 返回值: 函数返回 0 表示成功,非零值表示出错。
当一个线程调用 pthread_cond_wait
并因此进入等待状态时,它会释放与其关联的互斥锁,并等待条件变量被信号化。pthread_cond_signal
的作用是唤醒一个等待该条件变量的线程,这些线程随后可以尝试重新获取互斥锁,并继续执行。
比如刚刚三个线程都进入了等待队列:
当使用pthread_cond_signal
唤醒一个线程时:线程重新获得之前释放的锁mutex
,随后访问临界区代码。
当后续条件再次满足,thread-2
和thread-3
也会依次再次获得锁,从而访问到临时资源。
假设现在thread-1
访问完毕临界资源后,立马再次申请了锁:
由于条件变量的存在,therad-1
不能直接访问资源,要去等待队列等待,此时thread-1
进入等待队列尾部:
这样就可以避免一个线程一直占用临界资源,从而完成线程同步了。
int pthread_cond_broadcast(pthread_cond_t *cond);
- cond: 指向条件变量的指针,说明要唤醒哪一个条件变量下等待的所有线程。
- 返回值: 函数返回 0 表示成功,非零值表示出错。
还是刚才的三个线程都处于等待队列的情况:
当用pthread_cond_broadcast唤醒所有线程,此时所有被唤醒的线程再次竞争同一把锁,竞争到锁的线程才访问临界资源。当前一个线程访问完毕后,剩下的线程继续竞争,再访问临界资源。
销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
- cond: 指向条件变量的指针。
- 返回值: 如果函数成功销毁条件变量,返回 0;如果条件变量正在使用或已被销毁,返回非零值。
pthread_cond_destroy
函数用于销毁一个条件变量,释放与该条件变量相关联的资源。在调用 pthread_cond_destroy
之前,需要确保没有任何线程正在等待或正在使用该条件变量。如果条件变量正在被使用,或者有线程等待在该条件变量上,函数调用将失败,并返回错误码 EBUSY
。
3.使用示例
我们对一开始所用的抢票代码进行优化,保证不同线程按一定顺序访问临界资源,防止出现饥饿问题。
#include <iostream>
#include <pthread.h>
#include <string>
#include <vector>
#include <unistd.h>
using namespace std;
struct customer
{pthread_t tid;string name;
};
int g_tickets = 1000;
//定义一个全局变量的锁,保证所有线程都用同一个锁
pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;
//定义一个全局条件变量
pthread_cond_t g_cond = PTHREAD_COND_INITIALIZER;void *GetTicket(void *args)
{customer *c = (customer *)args;while (true){pthread_mutex_lock(&g_mutex); //加锁if (g_tickets > 0){usleep(1000);cout << c->name << " is getting a ticket " << g_tickets << endl;g_tickets--;pthread_cond_wait(&g_cond,&g_mutex); //进入等待对列等待条件变量pthread_mutex_unlock(&g_mutex); //解锁}else{pthread_mutex_unlock(&g_mutex); //解锁break;}}return nullptr;
}int main()
{vector<customer> custs(5);for (int i = 0; i < 5; i++){custs[i].name = "customer-" + to_string(i + 1);pthread_create(&custs[i].tid, nullptr, GetTicket, &custs[i]);}//唤醒线程while(g_tickets > 0){usleep(1000);pthread_cond_signal(&g_cond);}for (int i = 0; i < 5; i++){pthread_join(custs[i].tid, nullptr);}return 0;
}
这样就避免了都是同一个线程占用临界资源。