游戏后端技术_原子变量与锁
最近更新:2024-09-23
|
字数总计:1.3k
|
阅读估时:5分钟
|
阅读量:次
- 原子变量与锁
- 锁的使用
- 锁
- 互斥锁(std::mutex)
- 读写锁(通过共享锁std::shared_mutex)
- 条件变量(std::condition_variable)
- 原子变量的使用
- 如何在锁和原子变量间进行选择
原子变量与锁
锁的使用
锁
互斥锁(std::mutex)
- std::lock_guard
- 不提供接口,RAII对象管理加锁和解锁
- 案例:LockedQueue.h
- 通过lock_guard对象来管理锁的lock和unlock操作
- std::unique_lock
- 构造函数第二个参数是标记位
- 默认情况下,对象生成时会调用互斥锁的lock接口
- std::defer_lock, 延迟加锁,对象构造时没有加锁,后面需要显式调用lock接口
- std::adopt_lock, 使用已经调用lock的互斥锁构建对象
- 总之,提供了三种调用lock时机的构建对象方法,通过RAII在类对象走出生命周期时调用unlock(自动)
- 案例:WorldSocket::ReadDataHandler
读写锁(通过共享锁std::shared_mutex)
- 背景:读多写少的场景下使用
- std::shared_lock 读锁
- 构造函数当中调用lock_shared
- 析构函数当中调用unlock_shared
- std::unique_lock 写锁
- 构造函数当中调用lock
- 析构函数当中调用unlock
- 案例:ObjectAccessor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| #include <iostream> #include <mutex> #include <shared_mutex> #include <thread>
std::shared_mutex sh_mtx; int shared_data = 0;
void read_data(){ std::shared_lock<std::shared_mutex> lock(sh_mtx); std::cout << "Read shared_data: " << shared_data << std::endl; } void write_data(){ std::unique_lock<std::shared_mutex> lock(sh_mtx); ++shared_data; std::cout << "Incremented shared_data: " << shared_data << std::endl; } int main(){ std::thread t1(read_data); std::thread t2(write_data); std::thread t3(read_data);
t1.join(); t2.join(); t3.join(); return 0; }
|
条件变量(std::condition_variable)
- 需要互斥锁配合使用(std::unique_lock)
- 案例:ProducerConsumerQueue.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| void Push(const T& value) { std::lock_guard<std::mutex> lock(_queuelock); _queue.push(std::move(value)); _condition.notify_one(); } void WaitAndPop(T& value) { std::unique_lock<std::mutex> lock(_queueLock); while(_queue.empty() && !_shutdown) _condition.wait(lock);
if(_queue.empty() || _shutdown) return; value = _queue.front(); _queue.pop(); }
|
原子变量的使用
- 什么是原子变量
- 多线程环境下,确保对原子变量的操作不会形成竞态关系
- std::atomic
- 原子操作
- load
- store
- exchange
- compare_exchange_weak, compare_exchange_strong
- fetch_add, fetch_sub, fetch_or, fetch_xor
- 内存序
- 编译器优化重排,cpu指令优化重排
- 内存序规定了多个线程访问同一个内存地址的语义
- 六种内存序(规定了):
- 某个线程对内存地址的更新何时能被其他线程看见
- 某个线程内存地址访问附近可以做哪些优化
- 六种内存序(包括):
- memory_order_relaxed 允许优化,不做同步保证
- memory_order_release 允许后往前的优化,不允许前往后优化,做了同步保证(通常写操作)
- memory_order_acquire 允许前往后优化,不允许后往前优化,做了同步保证(通常读操作)
- memory_order_consume 不建议使用
- memory_order_acq_rel 以当前操作作为分割线,不能跨线优化,前面不允许优化到后面,后面也不允许优化到前面,做了同步保证
- memory_order_seq_cst 所有附近的原子操作按序执行,做了同步保证的
- 案例:MPSCQueue(多生产者单消费者, 无锁队列)
- 多生产者
- 多生产者线程同时进入时,exchange确保返回的是不同节点
- preHead->next.store是操作的不同的位置,并确保其他核心操作能得到最新值
- 单消费者
- 不需要考虑多个线程同时进入的问题
- _tail 和 _tail.next是有相关性的,以及_tail->next是acquire操作,_tail能取到最新值
- acquire和release怎么记住?
- 写是依赖前面的内容,所以release规定前面的不能优化到后面去;读是后面的代码的依赖,所以acquire规定了后面的不能优化到前面去。
- 应用 WorldSocket
- 生产者 主线程及Map线程
- 消费者线程是network线程(某个socket是绑定在一个network线程)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| void Enqueue(T* input) { Node* node = new Node(input); Node* prevHead = _head.exchange(node, std::memory_order_acq_rel); prevHead -> Next.store(node, std::memory_order_release); } bool Dequeue(T*& result) { Node * tail = _tail.load(std::memory_order_relaxed); Node * next = tail->Next.load(std::memory_order_acquire); if(!next) return false; result = next->Data; _tail.store(next, std::memory_order_release); delete tail; return true; }
|
如何在锁和原子变量间进行选择
- 互斥锁
- 没有获取到互斥锁的线程行为
- 先在用户态自旋一会尝试获取锁
- 之后会进行线程切换
- 当前占用锁的线程释放锁时,会让其他锁的线程转为就绪态等待操作系统调度
- 原子变量
- 什么时候使用互斥锁
- 需要锁定一块代码区域
- 操作临界资源耗时
- 什么时候使用原子变量
- 只需要确保某个变量的操作是原子性的(操作足够简单)
- 对性能要求很高的时候
- 目的:可以减小锁带来的开销
2024-07-11
该篇文章被 Cleofwine
归为分类:
Game