对象生命周期管理
C++11标准库引入
shared_ptr
和wak_ptr
。
shared_ptr
是强引用(可以改变资源的引用计数),控制对象的生命期:只要有一个指向对象的shared_ptr
存在,该对象就不会析构。weak_ptr
是弱引用(无法改变资源的引用计数),不控制对象的生命期:它知道对象是否还存活;可通过线程安全的lock()
尝试提升为有效的shared_ptr
。- 智能指针解决交叉引用导致的资源泄露问题
cpp
// 智能指针解决多线程访问共享对象时的安全问题
#include <iostream>
#include <memory>
#include <thread>
using namespace std;
class A
{
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
void testA() { cout << "testA()" << endl; }
};
// void handler(A *q)
void handler(weak_ptr<A> pw) // 弱智能指针不会引起对象的引用计数改变!
{
// std::this_thread::sleep_for(std::chrono::seconds(2));
shared_ptr<A> sp = pw.lock();
if (sp != nullptr) // 检测共享对象是否存活
{
sp->testA();
}
else
{
cout << "A 对象已经析构,不能再访问.." << endl;
}
}
int main()
{
// A *p = new A();
shared_ptr<A> p(new A());
thread t(handler, weak_ptr<A>(p));
t.detach(); // 分离线程
std::this_thread::sleep_for(std::chrono::seconds(2)); // 主线程睡眠2s
return 0;
}
线程同步精要
线程同步的四项原则:
- 首要原则是尽量最低限度地共享对象,减少需要同步的场合。
- 其次是使用高级的并发编程构件,如
TaskQueue
、Producer-Consumer Queue
等。- 不得已使用底层同步原语时,只用非递归的互斥其和条件变量,慎用读写锁(与简单的mutex相比,它实际上降低了性能),不要用信号量。
- 除了使用
atomic
整数外,不用"内核级"同步原语。
MutexLock
:封装临界区,是一个简单的资源类,用RAII
(对象创建时获取资源,对象销毁时释放资源)方式封装互斥器的创建与销毁。
MutexLockGuard
:封装临界区的进入和退出,即加锁和解锁。(类似于C++11标准中的<mark>lock_guard</mark>
)
cpp
#include "noncopyable.h"
#include "CurrentThread.h"
#include <assert.h>
#include <pthread.h>
class MutexLock : noncopyable
{
public:
MutexLock()
: holder_(0)
{
// 初始化互斥锁
pthread_mutex_init(&mutex_, NULL);
}
~MutexLock()
{
// 断言当前没有线程持有锁
assert(holder_ == 0);
// 销毁互斥锁
pthread_mutex_destroy(&mutex_);
}
bool isLockedByThisThread()
{
// 检查当前线程是否持有锁
return holder_ == CurrentThread::tid();
}
void assertLocked()
{
assert(isLockedByThisThread());
}
void lock() // 仅供MutexLockGuard调用,严禁用户代码调用
{
pthread_mutex_lock(&mutex_);
holder_ == CurrentThread::tid();
}
void unlock() // 仅供MutexLockGuard调用,严禁用户代码调用
{
holder_ = 0;
pthread_mutex_unlock(&mutex_);
}
pthread_mutex_t* getPthreadMutex() // 仅供Condition调用,严禁用户代码调用
{
return &mutex_;
}
private:
pthread_mutex_t mutex_; // 互斥锁
pid_t holder_; // 持有锁的线程ID
};
class MutexLockGuard : noncopyable
{
public:
explicit MutexLockGuard(MutexLock& mutex)
: mutex_(mutex)
{
mutex_.lock();
}
~MutexLockGuard()
{
mutex_.unlock();
}
private:
MutexLock& mutex_;
};
- 以上代码仅展示互斥锁的简单封装,并未达到工业强度(
mutex
创建为PTHREAD_MUTEX_DEFAULT
类型而不是PTHREAD_MUTEX_NORMAL
类型,严格应该指定;没有检查返回值,assert()
在release build中是空语句。)
cpp
// 用MutexLockGuard代替普通的互斥锁模拟车站卖票
MutexLock mutex_;
std::mutex mtx; // 全局互斥锁 -> 保证不会出现竞态条件
void sellTicket(int index) // 模拟卖票的线程函数
{
while (ticketCount > 0)
{
{
MutexLockGuard lock(mutex_);
// lock_guard<std::mutex> lock(mtx); // 构造时获取锁,出作用域{}析构锁(解锁)
if (ticketCount > 0) // 锁 + 双重判断 -> 防止三个线程在ticketCount为1时都再卖一次票
{
// 临界区代码段 -> 原子操作 -> 线程间互斥操作
cout << "窗口:" << index << "卖出第:" << ticketCount << "张票" << endl;
ticketCount--;
}
}
// mtx.lock(); // 上锁
// if (ticketCount > 0) // 锁 + 双重判断 -> 防止三个线程在ticketCount为1时都再卖一次票
// {
// // 临界区代码段 -> 原子操作 -> 线程间互斥操作
// cout << "窗口:" << index << "卖出第:" << ticketCount << "张票" << endl;
// ticketCount--;
// }
// mtx.unlock(); // 解锁
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
- 当
ticketCount
为1时,可能多个线程都会进入while
循环抢互斥锁,而此时只需卖1张票即可,故添加双重判断以避免可能产生的编程错误。
线程同步的四项原则,尽量用高层同步设施(线程池、队列、倒计时)。 使用普通互斥器和条件变量完成剩余的同步任务,采用
RAII
方式
多线程服务器
文中的"多线程服务器"是指运行在Linux操作系统上的独占式网络应用程序。 Linux下进程间通信(IPC)的方式很多,陈硕大神首选使用Socket(主要指TCP):可跨主机,具有伸缩性。
进程可理解为"内存中正在允许的程序",每个进程都有自己独立的地址空间。
线程是CPU调度的最小单位,特点是共享地址空间,从而可以高效地共享数据。
陈硕大神推荐的C++多线程服务端编程模式为one loop per thread + thread pool。 one loop per thread :程序中的每个IO线程有一个event loop(Reactor),用于处理读写和定时事件。Event loop代表了线程的主循环,需要让哪个线程干活,就把timer或IO channel(如TCP连接)注册到哪个线程的loop里即可。 thread pool :对于没有IO而仅有计算任务的线程,使用event loop有点浪费,陈硕大神使用的是blocking queue实现的任务队列
TaskQueue
。
在需要限制CPU占用率的场景下可以考虑采用单线程程序,而多线程程序的适用场景有:
- 有多个CPU可用(单核机器上多线程无性能优势);
- 线程间有共享数据,即内存中的全局状态;
- 延迟latency和吞吐量throughput同样重要;
- 具有可预测的性能。随着负载增加,性能缓慢下降,超过某个临界点之后会急速下降;
- 多线程能有效地划分责任与功能,让每个线程的逻辑比较简单,任务单一。
多线程服务程序中的线程大致分为3类
- IO线程:主循环是IO multiplexing,阻塞地等待在
select/poll/epoll_wait
系统调用上。- 计算线程:主循环是blocking queue,阻塞地等待在
condition variable
上。- 第三方库所用的线程,比如logging或database connection。
cpp
// 在Muduo网络库中陈硕大神基于MutexLockGuard和Condition
// 实现了线程安全的任务队列TaskQueue
// 在此基于C++11封装一个blocking的简单任务队列TaskQueue
// C++11提供的unique_ptr、lock_guard等非常方便封装vector、queue等
// 成为线程安全的容器。
#pragma once
#include "noncopyable.h"
#include <deque>
#include <mutex>
#include <memory>
#include <condition_variable>
template<typename T>
class BlockingQueue : noncopyable
{
public:
BlockingQueue()
: mutex_(),
queue_()
{}
void put(const T& x)
{
std::unique_lock<std::mutex> lock(mutex_);
queue_.push_back(x);
notEmpty_.notify_all();
}
T take()
{
std::unique_lock<std::mutex> lock(mutex_);
while (queue_.empty())
{
notEmpty_.wait(lock);
}
T front(std::move(queue_.front())); // 取出队列前端的元素
queue_.pop_front();
return std::move(front);
}
size_t size() const
{
std::lock_guard<std::mutex> lock(mutex_);
return queue_.size();
}
private:
// // mutable修饰符用于C++中,表示即使在一个const成员函数中,该变量仍可修改
mutable std::mutex mutex_; // 互斥锁
std::deque<T> queue_;
std::condition_variable notEmpty_; // 用于表示队列非空
};
多线程编程精要
C++的标准库容器和
std::string
都不是线程安全的,一方面是避免不必要的性能开销,另一方面是单个成员函数的线程安全并不具备可组合性。pthread_t
的值容易重复,在Linux系统中,陈硕大神建议使用gettid()
系统调用的返回值作为线程id。
cpp
safe_vector<int> vec; // 全局可见 -> 其每个成员函数都是线程安全的
if (!vec.empty()) // 未加锁保护
{
int x = vec[0]; // 在多线程情况下不安全
}
// 可能在if语句判空后,别的线程清空vec的元素,造成vec[0]失效
线程创建的几条原则:
- 程序库不应该在未提前告知的情况下创建自己的"背景线程"。
- 尽量使用相同的方式创建线程,例如
Mudue::Thread
。 - 在进入
main()
函数之前不应该启动线程。 - 程序中线程的创建最好能在初始化阶段全部完成。
高效的多线程日志
"日志",即文本的、供人阅读的日志,通常用于故障诊断和追踪,亦可用于性能分析。
muduo日志库采用的是双缓冲技术,基本思路是准备两块buffer:A和B,前端负责往buffer A填数据(日志信息),后端负责将buffer B的数据写入文件。当buffer A写满之后,交换A和B,让后端将buffer A的数据写入文件,而前端则往buffer B填入新的日志信息,如此往复。用两个buffer的好处是在新建日志消息的时候不必等待磁盘文件操作,也避免每条新日志消息都触发(唤醒)后端日志线程。
实际实现采用了四个缓冲区,可以进一步减少或避免日志前端的等待。
cpp
// muduo/base/AsyncLogging.h
// LargeBuffer大小为4MB,可以存至少1000条日志消息
typedef boost::ptr_vector<LargeBuffer> BufferVector;
// ptr_vector::auto_type类似于std::unique_ptr
typedef BufferVector::auto_type BufferPtr
muduo::MutexLock mutex_; // 用于保护后面的四个数据成员
muduo::Condition cond_;
BufferPtr currentBuffer_; // 当前缓冲
BufferPtr nextBuffer_; // 预备缓冲
BufferVector buffers_; // 将写入文件的已填满的缓冲
cpp
// muduo/base/AsyncLogging.cc
// 前端在生成一条日志消息的时候会调用AsyncLogging::append()
void AsyncLogging::append(const char* logline, int len)
{
muduo::MutexLockGuard lock(mutex_);
if (currentBuffer_->avail() > len)
{
// 当前缓冲剩余的空间足够大,直接把日志消息拷贝(追加)到当前缓冲中
currentBuffer_->append(logline, len);
}
else
{
// 否则,当前缓冲区已写满,把它送入(移入)buffers_
buffers_.push_back(std::move(currentBuffer_));
// 试图将预备缓冲移用为当前缓冲
if (nextBuffer_)
{
currentBuffer_ = std::move(nextBuffer_);
}
else
{
currentBuffer_.reset(new Buffer); // Rarely happens
}
// 追加日志消息,并通知(唤醒)后端开始写入日志数据
currentBuffer_->append(logline, len);
cond_.notify();
}
}
// 接收方(后端)实现
void AsyncLogging::threadFunc()
{
// 准备好两块空闲的buffer,以备在临界区内交换
BufferPtr newBuffer1(new Buffer);
BufferPtr newBuffer2(new Buffer);
BufferVector buffersToWrite;
while (running_)
{
// swap out what need to be written, keep CS short
{
muduo::MutexLockGuard lock(mutex_);
if (buffers_.empty())
{
cond_.waitForSeconds(flushInterval_);
}
// 条件满足时,将缓冲区currentBuffer_移入buffers_
buffers_.push_back(std::move(currentBuffer_));
// 并将空闲的newBuffer1移为当前缓冲
currentBuffer_ = std::move(newBuffer1);
// 将buffers_与buffersToWrite交换
buffersToWrite.swap(buffers_);
if (!nextBuffer_)
{
// 用newBuffer2替换nextBuffer_,保证前端始终有一个预备buffer可供调配
nextBuffer_ = std::move(newBuffer2);
}
}
// 临界区外安全的访问buffersToWrite,将日志数据写入文件
// 重新填充newBuffer1和newBuffer2
// 这样下次执行时还有两个空闲buffer可用于替换前端的当前缓冲和预备缓冲
}
// flush output
}
在前后端之间高效传递日志消息的办法不止一种,比方说使用常规的
muduo::BlockingQueue<std::string>
或muduo::BoundedBlockingQueue<std::string>
在前后端之间传递日志消息,其中每个std::string
是一条消息。这种做法每条日志消息都要分配内存,特别是在前端线程分配的内存要由后端释放,因此对malloc
的实现要求较高,需要针对多线程特别优化。 muduo库的异步日志实现用了一个全局锁。尽管临界区很小,但是如果线程数目较多,锁争用也可能影响性能。一种解决办法是像Java的ConcurrentHashMap
那样用多个桶子bucket,前端写日志的时候再按线程id哈希到不同的bucket中,以减少contention。
C++11多线程
- "语言级别"→ 跨平台 - windows/linux/max
- "语言层面"→ thread -> windows - createThread; linux - pthread_create(实质调用的仍是各系统的接口)
基本用法
- 如何创建启动一个线程 -> 传入线程所需的线程函数和参数,线程自动开启std::thread t(threadFunction);
- 子线程如何结束 -> 子线程函数在执行完成,线程就结束了
- 主线程如何处理子线程 -> (1)t.join():等待t线程结束,当前线程继续往下运行;(2)t.detach():将t线程设置为分离线程,主线程结束,整个线程结束,所有的子线程都自动结束。
cpp
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
void download()
{
// 模拟下载, 总共耗时500ms,阻塞线程500ms
this_thread::sleep_for(chrono::milliseconds(500));
cout << "child thread: " << this_thread::get_id() << ", download success...." << endl;
}
void func(int num, string str)
{
cout << "child thread: " << this_thread::get_id << endl;
cout << "num: " << num << ", str: " << str << endl;
}
int main()
{
// 打印主线程ID
cout << "main thread ID: " << this_thread::get_id() << endl;
// 创建子线程
thread t(download);
// join() -> 阻塞函数:若download()未完成,则主线程阻塞至任务执行完毕;若download()完成,则继续执行
t.join();
// 若程序要求固定顺序,则可用join()函数来控制 → 例如download()完成后再创建一个子线程
thread t1(func, 13, "lucky number");
// detach() -> 线程分离函数
t1.detach();
// 让主线程休眠, 等待子线程执行完毕 -> 防止线程分离后,func()未完成,主线程就退出并销毁子线程
this_thread::sleep_for(chrono::seconds(5));
// 获取当前计算机的CPU核心数
int num = thread::hardware_concurrency();
cout << "CPU number: " << num << endl;
return 0;
}
临界区互斥锁
- 多线程程序执行的结果是一致的,不会随着CPU对线程的不同调用顺序而产生不同的运行结果
- 若多线程程序会产生不同的运行结果,则称为竞态条件
- 当多个线程均涉及到临界区资源的修改时,可用mutex对临界区资源进行加锁解锁,以保证程序正常执行!
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;
// C++ thread 模拟车站三个窗口卖票的程序
int ticketCount = 100; // 车站有100张车票,由三个窗口一起卖票
std::mutex mtx; // 全局互斥锁 -> 保证不会出现竞态条件
// 模拟卖票的线程函数
void sellTicket(int index)
{
while (ticketCount > 0)
{
mtx.lock(); // 上锁
if (ticketCount > 0) // 锁 + 双重判断 -> 防止三个线程在ticketCount为1时都再卖一次票
{
// 临界区代码段 -> 原子操作 -> 线程间互斥操作
cout << "窗口:" << index << "卖出第:" << ticketCount << "张票" << endl;
ticketCount--;
}
mtx.unlock(); // 解锁
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main()
{
list<std::thread> tlist;
// 创建线程 -> 模拟车站卖票
for (int i = 0; i < 3; ++i)
{
tlist.push_back(std::thread(sellTicket, i));
}
for (std::thread& t : tlist)
{
t.join();
}
cout << "所有窗口卖票结束..." << endl;
return 0;
}
- 一般加锁,若程序在临界区中return,则可能无法解锁,造成死锁的问题。lock_guard是在作用域中加锁,而出作用域会自动解锁,若临界区中return相当于出了作用域,故会自动解锁,不会因return造成死锁问题!
cpp
// 模拟卖票的线程函数
void sellTicket(int index)
{
while (ticketCount > 0)
{
{
lock_guard<std::mutex> lock(mtx); // 构造时获取锁,出作用域{}析构锁(解锁)
if (ticketCount > 0) // 锁 + 双重判断 -> 防止三个线程在ticketCount为1时都再卖一次票
{
// 临界区代码段 -> 原子操作 -> 线程间互斥操作
cout << "窗口:" << index << "卖出第:" << ticketCount << "张票" << endl;
ticketCount--;
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
- lock_guard定义处会调用构造函数加锁,离开定义域的话lock_guard会被销毁,调用析构函数解锁 → 若作用域较大,则锁的颗粒度较大,很大程度上影响效率。通信中常用unique_lock!
- 一般加锁、解锁不涉及return时,可以考虑使用mutex!
- lock_guard不可能用在函数参数传递或返回过程中,只能用于简单代码段的互斥操作中!
- unique_lock不仅可以用于简单代码段的互斥操作,亦可用在函数的调用过程中!
同步通信
多线程编有两个问题:
- 线程间的互斥(竞态条件 -> 临界区代码段"加锁")
- 线程间的同步通信 -> 生产者、消费者模型
若生产者还未生产"产品",消费者无法消费 -> 故需进行线程同步("线程间通话")
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono> // 包含 chrono 以支持 sleep_for
using namespace std;
mutex mtx; // 互斥锁
condition_variable cv; // 条件变量
class Queue
{
public:
Queue() : max_size(5) {} // 假设队列的最大容量为 5
void put(int val)
{
unique_lock<mutex> lck(mtx);
// 队列未满时生产者生产 -> 否则等待
while (que.size() >= max_size)
{
cv.wait(lck);
}
que.push(val);
cv.notify_all(); // 生产完通知其它线程
cout << "生产者生产了:" << val << " 号物品" << endl;
}
int get()
{
unique_lock<mutex> lck(mtx);
// 队列非空时消费者消费 -> 否则等待
while (que.empty())
{
cv.wait(lck);
}
int val = que.front();
que.pop();
cv.notify_all(); // 消费完通知其它线程
cout << "消费者消费了:" << val << " 号物品" << endl;
return val;
}
private:
queue<int> que;
int max_size; // 队列的最大容量
};
void producer(Queue* que)
{
for (int i = 1; i <= 10; ++i)
{
que->put(i);
this_thread::sleep_for(chrono::milliseconds(100));
}
}
void consumer(Queue* que)
{
for (int i = 1; i <= 10; ++i)
{
que->get();
this_thread::sleep_for(chrono::milliseconds(100));
}
}
int main()
{
Queue que; // 线程共享的队列
thread t1(producer, &que);
thread t2(consumer, &que);
t1.join();
t2.join();
return 0;
}
- cv.notify_all() -> 通知其它在cv上等待的线程
- 其它在cv上等待的线程一旦收到通知:等待状态 -> 阻塞状态 -> 获取互斥锁 -> 继续执行程序!
无锁队列CAS
- 若临界区代码段较大,用互斥锁可能会比较"重",不利于效率提升
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <atomic> // 包含原子类型
#include <list>
#include <chrono>
using namespace std;
// volative -> 防止多线程对共享变量进行缓存 -> 访问的都是原始内存的变量
volatile std::atomic_bool isReady = false;
volatile std::atomic_int m_count = 0;
void task()
{
while (!isReady)
{
std::this_thread::yield(); // 线程让出当前时间片,等待下一次调度
}
for (int i = 0; i < 100; ++i)
{
m_count++;
}
}
int main()
{
list <std::thread> tlist;
for (int i = 0; i < 10; ++i)
{
tlist.push_back(std::thread(task));
}
std::this_thread::sleep_for(std::chrono::seconds(3));
isReady = true; // 等待子线程"工作"完后,置true,防止子线程一直调度
for (std::thread& t : tlist)
{
t.join();
}
cout << "m_count:" << m_count << endl;
return 0;
}
- 多线程缓存可以加快线程运行的效率
- volatile -> 无需多线程缓存共享变量 -> 某线程修改共享变量后及时反应!
Linux多线程
线程是轻量级的进程(LWP:light weight process),在Linux环境下线程的本质仍是进程。可理解为:进程是资源分配的最小单位,线程是操作系统调度执行的最小单位。
基本函数
cpp
#include <pthread.h>
// 返回线程ID的函数
pthread_t pthread_self(void); // ID类型为pthread_t -> 无符号长整型数
/*
线程创建函数 -> 一旦调用,即可获得一个子线程
thread -> 传出参数,if 创建成功,则将线程ID存入指针指向的内存
attr -> 线程属性,一般情况默认NULL即可
start_routine -> 函数指针,创建出的子线程的处理动作
arg -> 作为实参传递到start_routine指针指向的函数内部
线程创建成功则返回0
*/
int pthread_create(pthread_t *thread, const pthread_attrt_t *attr, void *(*start_routine)(void *), void *arg));
/*
线程退出函数 -> 一旦调用,线程退出
不会导致虚拟地址空间的释放(主要针对主线程)
retval -> 线程退出时携带的数据,若不需携带则指定为NULL
*/
void pthread_exit(void *retval);
/*
线程回收函数 -> 每次只能回收一个子线程
thread -> 要被回收的子线程的线程ID
retval -> 传出参数,二级指针,指向一级指针的地址,该地址中存储了pthread_exit()传递出的数据,若不需要,则指定为NULL
线程回收成功则返回0
*/
int pthread_join(pthread_t thread, void **retval);
/*
线程分离函数 -> 子线程和主线程分离后,其退出时占用的内核资源由其他进程接管并回收
分离后主线程中的线程回收函数无法回收子线程资源
thread -> 要与主线程分离的子线程ID
线程分离成功则返回0
*/
int pthread_detach(pthread_t thread);
基本用法
cpp
// sample.c -> 主线程接收子线程传出的数据
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>
void* callback(void* arg)
{
printf("子线程:%ld\n", pthread_self()); // 打印子线程的线程ID
int *a = (int*) arg; // 强制类型转换
*a = 100;
pthread_exit(&a); // 线程退出,并将地址传给主线程的pthread_join()函数
return NULL;
}
int main()
{
pthread_t tid; // 线程tid
int a; // 测试变量a
pthread_create(&tid, NULL, callback, &a); // 创建子线程
printf("主线程:%ld\n", pthread_self()); // 打印主线程的线程ID
pthread_join(tid, NULL); // 利用线程回收函数
printf("a = %d\n", a);
pthread_exit(NULL); // 线程退出函数
return 0;
}
/*
linux下执行命令
gcc sample.c -lpthread -o ./bin/app
./bin/app
*/
线程同步的4种方式
cpp
// 4种同步方式 -> linux环境
#include <pthread.h>
// 互斥锁 -> 一般情况下,每一个共享资源对应一把互斥锁,锁的个数与线程的个数无关
pthread_mutex_t mutex; // 互斥锁的类型
/*
互斥锁初始化函数和释放函数
restrict mutex 和 mutex -> 互斥锁变量的地址
attr -> 属性,默认指定为NULL
函数调用成功会返回0
*/
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
/*
pthread_mutex_init(&mutex, NULL);
...
pthread_mutex_destroy(&mutex);
*/
int pthread_mutex_lock(pthread_mutex_t *mutex); // 加锁函数
int pthread_mutex_unlock(pthread_mutex_t *mutex); // 解锁函数
// 读写锁 -> if 所有进程都是读操作,那么读是并行的(但是使用互斥锁,读操作也是串行的)
pthread_rwlock_t rwlock; // 读写锁的类型
// 条件变量 -> 生产者 & 消费者模型,实质:线程阻塞
pthread_cond_t cond; // 条件变量类型
/*
线程阻塞函数 -> 线程调用则被阻塞,被唤醒后继续执行
restrict cond -> 条件变量的地址
restrict mutex -> 互斥锁的地址
线程唤醒函数 -> 唤醒阻塞在条件变量上的线程,至少一个解除阻塞
cond -> 条件变量的地址
*/
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
int pthread_cond_signal(pthread_cond_t *cond);
// 信号量函数 -> 生产者 & 消费者
#include<semaphore.h>
sem_t sem; // 信号量类型
/*
信号量初始化和释放函数
sem -> 信号量变量的地址
pshared -> 0:线程同步;非0:进程同步
value -> 初始化信号量拥有的资源数
*/
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
// wait和post函数
int sem_wait(sem_t *sem); // 检测是否需要等待,若有资源可用,则资源数 --- 1;否则阻塞
int sem_post(sem_t *sem); // 一般工作完使用,资源数 + 1