C++中的锁机制以下几种:
在C++中,锁通常被分为两种类型:悲观锁和乐观锁
- 在C++中,可以使用atomic类型来实现乐观锁。atomic类型提供了对基本类型的原子操作,包括读、写、比较交换等。在进行原子操作时,它使用硬件原语实现同步,避免了使用锁所带来的额外开销和死锁的问题。
- 除了atomic类型,C++11还引入了一些使用乐观锁的算法,如无锁队列和无锁哈希表等。这些算法使用原子操作来实现线程安全,同时充分利用了乐观锁的优势,避免了使用锁所带来的开销。
所有线程间共享数据的问题,都是修改数据导致的(竞争条件) 。如果所有的共享数据都是只读的,就没问题,因为一个线程所读取的数据不受另一个线程是否正在读取相同的数据而影响
恶性条件竞争通常发生于多线程对多于一个的数据块的修改时,产生了非预想的执行效果,即竞态条件是多个线程同时访问共享资源,导致结果依赖于线程执行的顺序和时间。
在多线程编程中,竞态条件和数据竞争是常见的问题。解决这些问题的关键是使用同步机制。
避免恶性条件竞争:
mutex 又称互斥量,C++11 中与 mutex 相关的类(包括锁类型)和函数都声明在 <mutex>
头文件中,所以如果你需要使用 std::mutex
,就必须包含<mutex>
头文件。
std::mutex:
基本的 mutex 类。std::recursive_mutex:
递归 mutex 类。std::time_mutex:
定时 mutex 类。std::recursive_timed_mutex:
定时递归 mutex 类。std::try_lock
:尝试同时对多个互斥量上锁。std::lock
:可以同时对多个互斥量上锁。std::call_once
:如果多个线程需要同时调用某个函数,call_once
可以保证多个线程对该函数只调用一次。C++中通过实例化 std::mutex
创建互斥量,通过调用成员函数lock()
进行上锁,unlock()
进行解锁。对于互斥量,必须记住在每个线程执行完毕后必须去unlock()
释放已获得的锁。
值得一提的是,C++标准库为互斥量提供了一个RAII语法的模板类std::lock_guard
和std::unique_lock
。 - std::lock_guard
:其会在构造的时候提供已锁的互斥量,并在析构的时候进行解锁,此时就不用手动去解锁unlock
,即使发生异常也会释放,从而保证了一个已锁的互斥量总是会被正确的解锁。 - std::unique_lock
:unique_lock
更加灵活,可以在任意的时候加锁或者解锁,因此其资源消耗也更大,通常是在有需要的时候(比如和条件变量配合使用,我们将在介绍条件变量的时候介绍这个用法)才会使用,否则用lock_guard
。
std::mutex 的成员函数:
std::mutex
不允许拷贝构造,也不允许 move
拷贝,最初产生的 mutex
对象是处于 unlocked 状态的。lock()
:调用线程将锁住该互斥量。线程调用该函数会发生下面3种情况:- 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock 之前,该线程一直拥有该锁;
- 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住,直到 mutex 被释放;
- 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
unlock()
:释放对互斥量的所有权。try_lock()
:尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面3种情况:- 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 `unlock` 释放互斥量。
- 如果当前互斥量被其他线程锁住,则当前调用线程返回 `false`,**而并不会被阻塞掉。**
- 如果当前互斥量被当前调用线程锁住,则会产生死锁(`deadlock`)。
#include <iostream>
#include <thread>
#include <list>
#include <vector>
#include <mutex>
std::list<int> my_list;
int sum = 0;
std::mutex my_mutex;
void add_to_list(int new_value)
{
for (int i = 0; i < 10; i++) {
//使用std::lock_guard类模板,使用RAII机制
std::lock_guard<std::mutex> guard(my_mutex);
sum++;
my_list.push_back(new_value);
std::cout << "线程插入值为:" << new_value << std::endl;
}
}
int main(int argc, char* argv[])
{
std::vector<std::thread> thr(5);
for (int i = 0; i < 5; i++) {
thr[i] = std::thread(add_to_list, i);
}
for (auto& t:thr) {
t.join();
}
std::cout << sum << std::endl;
for (auto j : my_list)
std::cout << j << std::endl;
}
使用互斥量保护数据,保证线程安全绝不简单,不是简简单单的在共享数据区域内加lock
或try_lock
或者std::lock_guard
那样。一个迷失的指针或引用,将会让这种保护形同虚设:
锁的粒度是用来描述通过一个锁保护着的数据量大小。一个细粒度锁(a fine-grained lock)能够保护较小的数据量,一个粗粒度锁(a coarse-grained lock)能够保护较多的数据量。选择粒度对于锁来说很重要:
因此必须保证锁的粒度既可以保证线程安全也能保证并发的执行效率。
死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
- **协议1:** 所有进程开始前,必须一次性地申请所需的所有资源,这样运行期间就不会再提出资源要求,破坏了请求条件,即使有一种资源不能满足需求,也不会给它分配正在空闲的资源,这样它就没有资源,就破坏了保持条件,从而预防死锁的发生。
- **协议2:** 允许一个进程只获得初期的资源就开始运行,然后再把运行完的资源释放出来。然后再请求新的资源。
#### recursive_mutex
介绍
std::recursive_mutex
与 std::mutex
一样,也是一种可以被上锁的对象,但是和 std::mutex
不同的是,std::recursive_mutex
允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,std::recursive_mutex
释放互斥量时需要调用与该锁层次深度相同次数的 unlock()
,可理解为 lock()
次数和 unlock()
次数相同,除此之外,std::recursive_mutex
的特性和 std::mutex
大致相同。
std::time_mutex
比 std::mutex
多了两个成员函数,try_lock_for()
,try_lock_until()
。
try_lock_for
:函数接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex
的 try_lock()
不同,try_lock()
如果被调用时没有获得锁则直接返回 false
),如果在此期间其他线程释放了锁,则该线程可以获得对斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false
。try_lock_until
:函数则接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false
。#### recursive_timed_mutex
介绍
和 std:recursive_mutex
与 std::mutex
的关系一样,std::recursive_timed_mutex
的特性也可以从 std::timed_mutex
推导出来,感兴趣的同鞋可以自行查阅。
自旋锁(spin lock)是一种多线程同步机制,它是在等待锁的过程中不断地循环检查锁是否可用,而不是放弃CPU,从而避免了线程上下文切换带来的开销。自旋锁适用于锁被占用的时间很短的场景,因为自旋锁在等待锁的过程中会一直占用CPU,当锁被占用的时间较长时,这种方式会浪费大量的CPU资源。在锁的持有时间较短的情况下,自旋锁可以在等待锁的过程中避免线程上下文切换的开销,从而提高性能。
自旋锁std::spin_mutex
是C++17中的新特性,定义在<mutex>
头文件中。需要使用编译器支持C++17的特性才能使用std::spin_mutex
。
在C++11中,可以使用std::atomic_flag来实现自旋锁,它是一个布尔类型的原子变量,但是在使用时需要注意以下几点:
#include <atomic>
#include <thread>
class SpinLock {
public:
SpinLock() : flag_(ATOMIC_FLAG_INIT) {}
void lock() {
while (flag_.test_and_set(std::memory_order_acquire)) {
// 自旋等待
}
}
void unlock() { flag_.clear(std::memory_order_release); }
private:
std::atomic_flag flag_;
};
在上述代码中,test_and_set
成员函数将 flag_
设置为true
并返回之前的值,如果返回的值为 true
,则表示自旋等待;如果返回的值为 false
,则表示自旋锁已经被当前线程占用,可以执行加锁操作。clear
成员函数将 flag_
设置为 false
。
需要注意的是,std::atomic_flag
对象的默认内存顺序是 std::memory_order_seq_cst
,在使用时需要根据具体情况进行调整,例如在加锁时使用 std::memory_order_acquire
,在解锁时使用 std::memory_order_release
。
我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。