目录
一,线程概念
1,什么是线程
2,Linux 下的线程
3,POSIX 线程库
二,线程同步
1,数据不一致问题
2,如何解决这类问题
3,死锁
三,线程同步的运用
1,生产者-消费者问题(producer-consumer)
2,线程池
3,LeetCode
一,线程概念
1,什么是线程
我们先来简单的回顾一下什么是进程:进程就是正在进行的程序,是程序执行时的一个实例。我们可以把它看作是充分描述程序已经执行到何种程度的数据结构的汇集。从内核的观点来看,进程就是担任分配系统资源(例如 CPU 时间,内存资源等)的实体。
而线程(thread)就是一个进程里的一个执行序列(或者说是控制序列)。每个进程都至少有一个线程,不过经常会存在在同一个地址空间中并发的运行多个控制线程的情况。
为什么在一个进程中要有多个控制线程呢?因为这样我们就可以把进程设计成能够在同一时间段内能够做多件事情,每个线程都去处理各自独立的任务。
这样设计有很多好处,例如:
① 如果我们采用多进程,那必须通过 OS 提供的复杂机制才能实现某些资源的共享,而在一个进程内的多线程可以自动的共享一个地址空间与文件描述符等资源。
② 从 OS 的角度看,相比进程间的切换,线程之间的切换要轻松很多,创建一个线程的代价也要比创建一个进程的代价小。
③ 可以提高程序的吞吐量,改善程序的响应时间。使用多进程,相互独立的任务的处理就可以交叉的进行(比如我们可以让一个线程去与用户交互,另一个线程去做相关数据的计算工作)。
2,Linux 下的线程
Linux 实现线程的机制非常独特。从内核的角度来看,它并没有线程这个概念。Linux 把所有的线程都当做进程来实现。内核并没有去设计特别的调度算法或是定义特别的数据结构来表示线程。相反,线程仅仅被视为一个与其他进程共享某些资源的进程。每个线程都与进程一样,都拥有一个属于自己的 task_struct。所以在内核看来,线程就像是一个普通的进程。Linux 把线程称为轻量级进程(lightweight process,LWP)。
在 Linux 中,进程是资源分配的基本单位,而线程是 CPU 进行调度的基本单位。虽然线程共享了进程的很多资源,但是每个线程也都有自己的一些独立的数据:线程 ID,独立的栈空间,信号屏蔽字,调度优先级,errno,一组寄存器。
这里说明一下为什么线程需要有自己的独立栈空间。每个线程都有自己独立的堆栈,供各个被调用但是还没有被返回的函数使用。例如:在一个线程中函数A调用了函数B,函数B又去调用了函数C,那么当函数C执行时,供 A,B,C 使用的栈帧就会被全部储存在这个栈中。通常每个线程都会去调用自己相关的函数,从而有一个与其他线程不同的执行历史,所以每个线程都需要有自己的堆栈来维护这个执行历史。
3,POSIX 线程库
// 比较两个线程的 ID 是否相同, 相同返回非零值,否则返回 0
int pthread_equal(pthread_t tid1, pthread_t tid2);
// 返回当前线程的 ID
pthread_t pthread_self();
// 创建一个线程, 成功返回 0, 否则返回错误码
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine)(void*), void *arg);
③ 线程终止
// 终止线程
void pthread_exit(void *value_ptr);
// 取消一个正在执行的线程, 成功返回 0, 失败返回错误码
int pthread_cancel(pthread_t thread);
// 等待线程结束, 成功返回 0, 失败返回错误码
int pthread_join(pthread_t thread, void **value_ptr);
进程与线程的相关函数的比较
进程 | 线程 | 描述 |
fork | pthread_create | 创建新的控制流 |
exit | pthread_exit | 从现有的控制流退出 |
getpid | pthread_self | 获取控制流的 ID |
waitpid | pthread_join | 在当前控制流中获得其他控制流的退出状态 |
abort | pthread_cancel | 请求控制流的非正常退出 |
④ 分离线程
// 分离线程, 成功返回 0, 失败返回错误码
int prhead_detach(pthread_t tid);
// 如果不关心线程的返回值, 不想去手动调用 join, 这个时候我们可以告诉系统,
// 当线程退出时,自动释放线程资源。
二,线程同步
线程同步指的是通过各种机制(如互斥锁、条件变量、信号量等)来确保这些进程或线程按照既定的顺序或条件来访问共享资源,以避免数据不一致等问题。
1,数据不一致问题
什么是数据不一致问题?我们来看两个例子。
第一个例子:两个线程读写相同变量
线程A读取这个变量然后为这个变量赋值,但是赋值操作需要两个CPU时钟周期。当线程B在线程A为变量赋值的两个时钟周期之间读取这个变量时,就会读取到我们不期望的结果。
第二个例子:两个线程试图在同一时间修改同一变量
我们想对一个 int 类型的变量 i 进行 ++ 操作,虽然 ++i 在我们看来只是一条语句,但是对于机器来说 ++i 其实对应着三条指令:
① 将变量 i 放入寄存器
② 将寄存器中的变量值加1
③ 把寄存器中的值写回变量 i 中
2,如何解决这类问题
pthread 库中提供了一些机制来帮助我们实现同步,接下来将介绍一下部分的机制:
① 互斥量
我们可以通过使用 pthread 的互斥接口保护数据,确保同一时间只有一个线程访问数据。互斥
量(mutex)从本质上说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放
互斥量上的锁。对互斥量进行加锁以后,任何其他试图再次对互斥量加锁的线程将会被阻塞直
到当前线程释放该互斥锁。如果释放互斥锁时有多个线程阻塞,所有在该互斥锁上的阻塞线程
都会变成可运行状态,第一个变为运行状态的线程可以对互斥量加锁,其他线程将会看到互斥
锁依然被锁住,只能回去再次等待它重新变为可用。在这种方式下,每次只有一个线程可以
向前执行。
相关接口:
互斥变量用 pthread_mutex_t 数据类型来表示,在使用互斥变量之前,必须首先对它进
行初始化,可以把它置为常量 PTHREAD_MUTEX_INITIALIZER 也可以通过调 pthread_mutex_init 函数进行初始化。如果动态地分配互斥量,那么在释放内存前需要调用pthread_mutex_destroy。
初始化与销毁:
// 可以使用静态分配来初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
// 也可以动使用态分配来初始化, 默认情况下 attr 可以设置为 nullptr
int pthread_mutex_init(pthread_mutex t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
// 销毁
int pthread mutex destroy(pthread_mutext *mutex);
// 成功返回 0, 否则返回错误码
对互斥量进行加锁,需要调用 pthread_mutex_lock,如果互斥量已经上锁,调用线程将阻塞直到互斥量被解锁。对互斥量解锁,需要调用 pthread_mutex_unlock。
加锁与解锁:
// 加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 如果线程不希望被阻塞,它可以使用 pthread_mutex_trylock 尝试对互斥量进行加锁。
int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 成功返回 0, 否则返回错误码
对于在上一部分提到的两个问题,我们只需要把 write 操作和 ++i 操作给锁住,放入临界区即可解决。
为了实现互斥锁操作,大多数体系结构都提供了 swap 或 exchange 指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,所以就能够保证原子性。即使是多处理器平台,访问内存的时钟周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待时钟周期。 现在我们可以给出一个简单实现 lock 和 unlock 的伪代码:
lock:
movb $0, %al
xchgb %al, mutex // 交换操作
if(a1寄存器的内容>0){
return 0;
}
else{
挂起等待;
}
goto lock;
unlock :
movb $1, mutex
唤醒等待 mutex 的线程;
return 0;
② 条件变量
条件变量是线程可用的另一种同步机制,通常与 mutex 一起使用。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。条件本身是由互斥量保护的,线程在改变条件状态前必须首先锁住互斥量,其他线程在获得互斥量之前不会察觉到这种改变,因为必须锁定互斥量以后才能计算条件。
相关接口:
条件变量使用之前必须首先进行初始化,条件变量用 pthread_cond_t 数据类型来表示。在使用条件变量之前,必须首先对它进行初始化,可以把它置为常量 PTHREAD_COND_INITIALIZER 也可以通过调 pthread_cond_init 函数进行初始化。如果动态地分配条件变量,那么在释放内存前需要调用 pthread_cond_destroy。
初始化与销毁:
// 可以使用静态分配来初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER
// 也可以动使用态分配来初始化, 默认情况下 attr 可以设置为 nullptr
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
// 销毁
int pthread_cond_destroy(pthread_cond* cond);
// 成功返回 0, 否则返回错误码
等待条件满足:
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond
pthread_mutex_t *restrict mutex, const struct timespec *restrict timeout);
// 成功返回 0, 否则返回错误码
我们需要传递给 pthread_cond_wait 一个互斥量对条件进行保护,调用者把锁住的互斥量传给函数。函数把调用线程放到等待条件的线程列表上,然后对互斥量解锁,这两个操作是原子操作。至于为什么我们要传入一个被锁住的 mutex 变量给 pthread_cond_wait ?我们稍后再提。
有两个函数可以用于通知线程条件已经满足。pthread_cond_signal 函数将唤醒等待该条件的某个线程,而 pthread_cond_broadcast 函数将唤醒等待该条件的所有线程。
唤醒等待:
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
// 成功返回 0, 否则返回错误码
下面来说说为什么 pthread_cond_wait 函数需要一个被锁住的 mutex 变量:
① 调用者把锁住的互斥量传给函数。函数把调用线程放到等待条件的线程列表上,然后对互斥量解锁,这两个操作是原子操作。这样就关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道,这样线程就不会错过条件的任何变化。
② 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且去通知等待在条件变量上的线程。而条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。
条件变量使用的规范:
// 等待条件
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(&cond, &mutex);
修改条件
pthread_mutex_unlock(&mutex);
这里再来说说为什么上述代码中用的是 while 而不是 if。因为我们要避免假唤醒。这是多线程程序中的一个现象,其中线程可能无明显原因地被唤醒。如果只使用 if 检查条件,当线程因为假唤醒而醒来,它会继续执行后面的代码,即使条件实际上并未满足。使用 while 循环可以确保在每次唤醒时重新检查条件,以保证代码的健壮性。
// 给条件发送信号
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
还有一些其他的同步机制,例如读写锁、信号量,在这里就不一一介绍了。
3,死锁
在很多情况下,进程或者是线程需要互斥性的访问(独占)某些资源,这种情况下就非常容易发生死锁。那什么是死锁呢?我们来举个例子:线程1需要申请资源A和B,线程2也需要申请资源A和资源B。线程1申请到了资源A并且对资源A上了锁,线程2申请到了资源B并且对资源B上了锁。当线程1申请资源B,线程2申请资源A,这时就会发生死锁。这两个线程都在相互请求另一个线程拥有的资源,所以这两个线程都无法向前运行,于是就产生了死锁。
下面是死锁的规范定义:如果一个线程集合中的每个线程都在等待只能由该线程集合中的其他线程才能引发的事件,那么该线程集合就是死锁的。
换言之,这一死锁线程集合中的每一个线程都在等待另一个死锁的线程已经占有的资源。但是由于所有线程都不能运行,它们中的任何一个都无法释放资源,所以没有一个线程可以被唤醒。
死锁的四个必要条件
① 互斥条件:一个资源只能被一个执行流使用
② 占有和等待条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
③ 不可抢占条件:已经分配给一个执行流的资源不能强制性地被抢占,它只能被占有它的执行流显式地释放。
④ 环路等待条件:死锁发生时,系统中一定有由两个或两个以上的执行流组成的一条环路,该环路中的每个执行流都在等待着下一个执行流所占有的资源。
死锁发生时,以上四个条件一定是同时满足的。如果其中任何一个条件不成立,死锁就不会发生。
对死锁进行建模
这里简单提一下处理死锁的四个策略:
① 忽略该问题。
② 检测死锁并恢复:检测死锁是否发生,一旦发生死锁,采取行动解决问题
③ 仔细对资源进行分配,动态地避免死锁。
④ 预防死锁:通过破坏引起死锁的四个必要条件之一,防止死锁的产生。
三,线程同步的运用
1,生产者-消费者问题(producer-consumer)
作为线程同步的一个例子,这里考虑生产者-消费者问题,也称作有界缓冲区(bounded-buffer)问题。两个进程共享一个公共的固定大小的缓冲区。其中一个是生产者,将信息放入缓冲区;另一个是消费者,从缓冲区中取出信息。(也可以把这个问题一般化为 m 个生产者和 n 个消费者问题,但是为了简化解决方案这里就暂时只讨论一个生产者和一个消费者的情况)
这里的问题在于当缓冲区已满,而此时生产者还想向其中放入一个新的数据项的情况。其解决办法是让生产者睡眠,等消费者从缓冲区中取出一个或多个数据项时再唤醒它。类似地,当消费者试图从缓冲区中取数据而发现缓冲区为空时,消费者就睡眠,直到生产者向其中放入一些数据时再将其唤醒。
这里采用的是模拟阻塞队列(blocking queue)来当作缓冲区的生产消费者模型:
#include <iostream>
using namespace std;
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>
#include "blocking_queue.hpp"
void* producer(void* arg){
Blocking_Queue<string>* ptr_bq = static_cast<Blocking_Queue<string>*>(arg);
int i = 0;
while(true){
string data = "data-" + to_string(i);
++i;
ptr_bq->put(data);
cout << "生产者已将 " << data << " 放入队列" << endl;
usleep(rand() % 50000);
}
}
void* consumer(void* arg){
Blocking_Queue<string>* ptr_bq = static_cast<Blocking_Queue<string>*>(arg);
while(true){
string data = ptr_bq->take();
cout << "消费者已得到 " << data << endl;
usleep(rand() % 100000);
}
}
int main(){
srand(time(nullptr));
Blocking_Queue<string> bq(10);
pthread_t pro_thread, con_thread;
pthread_create(&pro_thread, nullptr, producer, &bq);
pthread_create(&con_thread, nullptr, consumer, &bq);
pthread_join(pro_thread, nullptr);
pthread_join(con_thread, nullptr);
return 0;
}
blocking_queue.hpp
#pragma once
#include <iostream>
#include <pthread.h>
#include <queue>
template<class T>
class Blocking_Queue{
static const size_t SIZE = 10; // 默认大小
typedef T data_type;
private:
std::queue<data_type> _bq;
size_t _capacity; // 阻塞队列的容量
pthread_mutex_t _mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t _consume = PTHREAD_COND_INITIALIZER;
pthread_cond_t _product = PTHREAD_COND_INITIALIZER;
public:
Blocking_Queue(size_t capacity = SIZE): _capacity(capacity){}
void put(const data_type& data){
lock();
while(full()){
producer_wait();
}
_bq.push(data);
if(_bq.size() == 1){
wake_consumer();
}
unlock();
}
data_type take(){
lock();
while(empty()){
consumer_wait();
}
data_type data = _bq.front();
_bq.pop();
if(_bq.size() == _capacity - 1){
wake_producer();
}
unlock();
return data;
}
private:
void lock(){ pthread_mutex_lock(&_mutex); }
void unlock(){ pthread_mutex_unlock(&_mutex); }
// 生产者 等待 消费者去消费队列中的数据
void producer_wait(){ pthread_cond_wait(&_consume, &_mutex); }
// 消费者 等待 生产者生产数据到队列中
void consumer_wait(){ pthread_cond_wait(&_product, &_mutex); }
// 唤醒消费者, 告诉消费者此时队列中已经有产品了
void wake_consumer(){ pthread_cond_signal(&_product); }
// 唤醒生产者, 告诉生产者此时队列已经有空位了
void wake_producer(){ pthread_cond_signal(&_consume); }
bool full(){ return _bq.size() == _capacity; }
bool empty(){ return _bq.size() == 0; }
};
makefile
pc_problem:pc_problem.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f pc_problem
部分运行结果:
这样设计有以下好处:
① 将生产者与消费者解耦
② 支持并发
③ 支持生产者与消费者的忙闲不均
2,线程池
接下来是第二个线程同步的例子:线程池。线程池是一种基于池化技术的线程使用模式,用于减少在多线程程序中创建和销毁线程的开销。线程池中维护了一组已经创建但处于空闲状态的线程,当有新的任务到来时,可以直接利用这些现成的线程来执行任务,而不是每次任务到来时再创建新线程。线程池不仅可以提高程序性能,还能提高系统的稳定性。
线程池的核心组成:
① 任务队列:一个用于存放待处理任务的队列。新的任务会被加入到这个队列中等待执行。
② 工作线程:线程池中的线程,负责从任务队列中取出任务并执行。
③ 线程池管理器:负责管理线程池中的线程,包括线程的创建、销毁和任务分配等。
线程池的主要优点:
① 降低资源消耗:通过重复利用已创建的线程,减少线程创建和销毁的次数,从而降低资源消耗。
② 提高响应速度:当任务到来时,无需等待新线程的创建即可立即执行,从而提高响应速度。
③ 提高线程的可管理性:线程是稀缺资源,通过线程池可以有效进行统一分配、调优和监控,避免创建无限多的线程而耗尽系统资源。
其实线程池就是一个单生产者与多消费者的模型。
main_thread.cc
#include <iostream>
#include "task.hpp"
#include "thread_pool.hpp"
#include <unistd.h>
int main(){
Thread_Pool<Task> tp;
while(true){
// 获取任务
int task_idx = rand() % Task::tasks_size();
Task task(task_idx);
// 将任务交给线程池
tp.tasks_push(task);
//sleep(1);
}
return 0;
}
task.hpp
#pragma once
#include <iostream>
#include <vector>
#include <functional>
#include <unistd.h>
enum
{
SUCCEED = 0,
ERROR = 1,
};
// 一个简易的任务类
class Task
{
private:
static std::vector<std::function<void()>> _tasks; // 默认任务列表
int _task_idx;
int _exit_code = SUCCEED;
public:
Task(int task_idx) : _task_idx(task_idx) {}
// 添加新的任务
void push_task(std::function<void()> task) { _tasks.push_back(task); }
int get_exit_code() { return _exit_code; }
void run()
{
if (_task_idx < _tasks.size())
{
_tasks[_task_idx]();
}
else
{
_exit_code = ERROR;
}
}
static size_t tasks_size() { return _tasks.size(); }
};
// 一个简易的任务列表
std::vector<std::function<void()>> Task:: _tasks = {
[]()
{
std::cout << "task-0 is runing" << std::endl;
sleep(1);
},
[]()
{
std::cout << "task-1 is runing" << std::endl;
sleep(1);
},
[]()
{
std::cout << "task-2 is runing" << std::endl;
sleep(1);
},
[]()
{
std::cout << "task-3 is runing" << std::endl;
sleep(1);
},
[]()
{
std::cout << "task-4 is runing" << std::endl;
sleep(1);
},
[]()
{
std::cout << "task-5 is runing" << std::endl;
sleep(1);
},
};
thread_pool.hpp
#pragma once
#include <iostream>
#include <pthread.h>
#include <vector>
#include <queue>
#include <string>
template <class T>
class Thread_Pool
{
private:
// 与线程相关的数据
struct Thread_Data
{
pthread_t tid;
std::string name;
};
typedef T task_type;
// 静态成员
static const size_t T_SIZE = 10; // 默认的线程个数
static void *routine(void *); // 线程创建启动后默认要执行的函数
private:
// 成员变量
pthread_mutex_t _mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t _cond = PTHREAD_COND_INITIALIZER;
std::vector<Thread_Data> _threads; // 线程数组
std::queue<task_type> _tasks; // 任务队列
public:
Thread_Pool(size_t size = T_SIZE) : _threads(size)
{
for (int i = 0; i < _threads.size(); ++i)
{
pthread_create(&_threads[i].tid, nullptr, routine, this);
_threads[i].name = "thread-" + std::to_string(i);
}
}
public:
// 锁相关的操作
void lock() { pthread_mutex_lock(&_mutex); }
void unlock() { pthread_mutex_unlock(&_mutex); }
void thread_sleep() { pthread_cond_wait(&_cond, &_mutex); }
void thread_weak() { pthread_cond_signal(&_cond); }
// 任务队列相关的操作
void tasks_push(const task_type& task)
{
lock();
_tasks.push(task);
thread_weak();
unlock();
}
task_type tasks_pop()
{
task_type tmp = _tasks.front();
_tasks.pop();
return tmp;
}
bool tasks_empty() { return _tasks.empty(); }
bool tasks_size() { return _tasks.size(); }
// 线程相关操作
std::string get_thread_name(pthread_t tid)
{
for (const auto &thread : _threads)
{
if (thread.tid == tid)
return thread.name;
}
return "none";
}
};
template <class T>
void *Thread_Pool<T>::routine(void *args)
{
Thread_Pool<T> *tp = static_cast<Thread_Pool<T>*>(args);
tp->lock();
while (true)
{
while (tp->tasks_empty())
{
tp->thread_sleep();
}
std::cout << tp->get_thread_name(pthread_self()) << "已获取任务" << std::endl;
tp->tasks_pop().run();
}
tp->unlock();
}
makefile
main_thread:main_thread.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f main_thread
部分运行结果
3,LeetCode
这里再推荐三道 LeetCode 来让大家感受一下怎么去同步线程
1114. 按序打印 - 力扣(LeetCode)https://leetcode.cn/problems/print-in-order/description/
class Foo {
pthread_mutex_t _mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t _mutex2 = PTHREAD_MUTEX_INITIALIZER;
public:
Foo() {
pthread_mutex_lock(&_mutex1);
pthread_mutex_lock(&_mutex2);
}
void first(function<void()> printFirst) {
// printFirst() outputs "first". Do not change or remove this line.
printFirst();
pthread_mutex_unlock(&_mutex1);
}
void second(function<void()> printSecond) {
pthread_mutex_lock(&_mutex1);
// printSecond() outputs "second". Do not change or remove this line.
printSecond();
pthread_mutex_unlock(&_mutex2);
}
void third(function<void()> printThird) {
pthread_mutex_lock(&_mutex2);
// printThird() outputs "third". Do not change or remove this line.
printThird();
}
};
1117. H2O 生成 - 力扣(LeetCode)https://leetcode.cn/problems/building-h2o/description/
class H2O {
pthread_mutex_t _mutex;
pthread_cond_t _h_cond;
pthread_cond_t _o_cond;
int _h_cnt = 0;
int _o_cnt = 0;
public:
H2O() {
pthread_mutex_init(&_mutex,nullptr);
pthread_cond_init(&_h_cond,nullptr);
pthread_cond_init(&_o_cond,nullptr);
}
void hydrogen(function<void()> releaseHydrogen) {
lock();
while(_h_cnt == 2){
pthread_cond_wait(&_h_cond, &_mutex);
}
// releaseHydrogen() outputs "H". Do not change or remove this line.
releaseHydrogen();
++_h_cnt;
if(_h_cnt == 2 and _o_cnt == 1){
_h_cnt = 0;
_o_cnt = 0;
}
pthread_cond_signal(&_o_cond);
unlock();
}
void oxygen(function<void()> releaseOxygen) {
lock();
while(_o_cnt == 1){
pthread_cond_wait(&_o_cond, &_mutex);
}
// releaseOxygen() outputs "O". Do not change or remove this line.
releaseOxygen();
++_o_cnt;
if(_h_cnt == 2){
_o_cnt = 0;
_h_cnt = 0;
}
pthread_cond_broadcast(&_h_cond);
unlock();
}
private:
void lock(){ pthread_mutex_lock(&_mutex); }
void unlock(){ pthread_mutex_unlock(&_mutex); }
};
1226. 哲学家进餐 - 力扣(LeetCode)https://leetcode.cn/problems/the-dining-philosophers/description/
class DiningPhilosophers {
private:
static const int N = 5;
pthread_mutex_t _mutex_arr[N];
public:
DiningPhilosophers() {
for(int i=0;i<N;++i){
_mutex_arr[i] = PTHREAD_MUTEX_INITIALIZER;
}
}
void wantsToEat(int philosopher,
function<void()> pickLeftFork,
function<void()> pickRightFork,
function<void()> eat,
function<void()> putLeftFork,
function<void()> putRightFork) {
int left = (philosopher + 1) % N;
int right = philosopher;
if(philosopher % 2 == 0){
lock(left);
lock(right);
pickLeftFork();
pickRightFork();
}
else{
lock(right);
lock(left);
pickRightFork();
pickLeftFork();
}
eat();
putLeftFork();
putRightFork();
unlock(left);
unlock(right);
}
private:
void lock(int idx){ pthread_mutex_lock(&_mutex_arr[idx]); }
void unlock(int idx){ pthread_mutex_unlock(&_mutex_arr[idx]); }
};