C++中的锁
C++中的锁机制
C++中的锁机制以下几种:
- 互斥锁:包括std::mutex、std::recursive_mutex、std::timed_mutex、std::recursive_timed_mutex等。互斥锁用于保护共享资源,可以保证同一时刻只有一个线程访问共享资源。
- 读写锁:包括std::shared_mutex、std::shared_timed_mutex等。读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。
- 条件变量:包括std::condition_variable、std::condition_variable_any等。条件变量允许线程等待某个条件发生变化,只有当条件满足时才能继续执行。
- 原子操作:包括std::atomic、std::atomic_flag等。原子操作用于保证某个操作的执行不会被其他线程中断,从而避免了数据竞争的发生。
- 自旋锁:包括std::spin_lock、std::atomic_flag等。自旋锁在等待锁的过程中不断地循环检查锁是否可用,而不是放弃CPU,从而避免了线程上下文切换带来的开销。
- 信号量:包括std::binary_semaphore、std::counting_semaphore等。信号量用于控制同时访问某个资源的线程数量,可以实现线程的互斥和同步。
悲观锁和乐观锁
在C++中,锁通常被分为两种类型:悲观锁和乐观锁
- 其中悲观锁是指在访问共享资源时先获取锁,防止其他线程同时修改该资源,适用于写操作多的场景。C++中的互斥锁就是一种悲观锁。
- 而乐观锁则是在不加锁的情况下,尝试去读取和修改共享资源,如果遇到冲突,再使用重试等机制解决冲突,适用于读操作多于写操作的场景。
- 在C++中,可以使用atomic类型来实现乐观锁。atomic类型提供了对基本类型的原子操作,包括读、写、比较交换等。在进行原子操作时,它使用硬件原语实现同步,避免了使用锁所带来的额外开销和死锁的问题。
- 除了atomic类型,C++11还引入了一些使用乐观锁的算法,如无锁队列和无锁哈希表等。这些算法使用原子操作来实现线程安全,同时充分利用了乐观锁的优势,避免了使用锁所带来的开销。
Mutex:互斥
所有线程间共享数据的问题,都是修改数据导致的(竞争条件) 。如果所有的共享数据都是只读的,就没问题,因为一个线程所读取的数据不受另一个线程是否正在读取相同的数据而影响
恶性条件竞争
恶性条件竞争通常发生于多线程对多于一个的数据块的修改时,产生了非预想的执行效果,即竞态条件是多个线程同时访问共享资源,导致结果依赖于线程执行的顺序和时间。
- 竞态条件(Race Condition)指的是多个线程访问共享变量时,最终的结果取决于多个线程的执行顺序。竞态条件不一定总是错误的,但它们可能导致非预期的结果。
- 数据竞争(Data Race)指的是多个线程同时访问同一个共享变量,并且至少有一个线程对该变量进行了写操作。数据竞争是一种错误,因为它可能导致未定义的行为。
在多线程编程中,竞态条件和数据竞争是常见的问题。解决这些问题的关键是使用同步机制。
避免恶性条件竞争:
- 要避免恶性的条件竞争,一种方法是就使用一定的手段,对线程共享的内存区域的数据结构采用某种保护机制,如使用锁
- 另一种就是选择对该区域的数据结构和不变量的设计进行修改,如保证该区域为原子操作,,修改完的结构必须能完成一系列不可分割的变化,但是这种无锁的方法很难一定保证线程安全
- 另一种处理条件竞争的方式是,使用事务(transacting)的方式去处理该数据共享区域
mutex 头文件介绍
mutex 又称互斥量,C++11 中与 mutex 相关的类(包括锁类型)和函数都声明在 <mutex>
头文件中,所以如果你需要使用 std::mutex
,就必须包含<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
可以保证多个线程对该函数只调用一次。