C++所有锁的讲解、使用场景、相应的C++代码示例
一、互斥锁(Mutex)
1. std::mutex
含义: std::mutex
最基本的互斥锁,当一个线程占用锁时,其他线程必须等待该锁被释放。
使用场景: 当需要保护共享资源不被多个线程同时修改时使用。
代码示例:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // 全局互斥锁
int counter = 0; // 共享资源
void attempt_10k_increases()
{
for (int i = 0; i < 10000; ++i)
{
mtx.lock();
++counter; // 受保护的操作
mtx.unlock();
}
}
int main()
{
std::thread threads[10];
for (int i = 0; i < 10; ++i)
{
threads[i] = std::thread(attempt_10k_increases);
}
for (auto& th : threads)
{
th.join();
}
std::cout << "Result of counter: " << counter << std::endl;
return 0;
}
输出结果: Result of counter: 100000
解释: 这个程序创建了10个线程,每个线程尝试对counter
增加10000次。通过使用std::mutex
, 我们确保每次只有一个线程可以增加计数器,避免了数据竞争。
2. std::recursive_mutex
含义: 递归互斥锁,允许同一个线程多次获取同一锁。
使用场景: 在递归函数中需要多次获取同一个锁的情况。
代码示例:
#include <iostream>
#include <thread>
#include <mutex>
std::recursive_mutex rec_mtx;
int count = 0;
void recursive_increment(int level)
{
if (level > 0)
{
rec_mtx.lock();
recursive_increment(level - 1);
rec_mtx.unlock();
}
else
{
++count;
}
}
int main()
{
std::thread t(recursive_increment, 10);
t.join();
std::cout << "Count is: " << count << std::endl;
return 0;
}
输出结果: Count is: 1
解释: 这段代码在递归函数recursive_increment
中使用std::recursive_mutex
。每次调用都会尝试加锁,由于使用的是递归互斥锁,同一线程可以多次成功获取锁。
二、定时锁
1. std::timed_mutex
含义: 允许尝试锁定一定时间,如果在指定时间内没有获取到锁,则线程可以执行其他操作或放弃。
使用场景: 当你不希望线程因等待锁而无限期阻塞时使用。
代码示例:
#include <iostream>
#include <mutex>
#include <chrono>
#include <thread>
std::timed_mutex timed_mtx;
void attempt_lock_for(int id)
{
auto now = std::chrono::steady_clock::now();
if (timed_mtx.try_lock_for(std::chrono::seconds(1)))
{
std::cout << "Thread " << id << " got the lock." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2)); // hold the lock for 2 seconds
timed_mtx.unlock();
}
else
{
std::cout << "Thread " << id << " couldn't get the lock." << std::endl;
}
}
int main()
{
std::thread threads[2];
for (int i = 0; i < 2; ++i)
{
threads[i] = std::thread(attempt_lock_for, i);
}
for (auto& th : threads)
{
th.join();
}
return 0;
}
输出结果:
Thread 0 got the lock.
Thread 1 couldn't get the lock.
解释: 这段代码创建了两个线程,每个线程尝试锁定同一个std::timed_mutex
。第一个线程获取锁并持有2秒钟,而第二个线程只尝试1秒钟去获取锁,因此它失败了。
2. std::recursive_timed_mutex
含义: std::recursive_timed_mutex
结合了std::recursive_mutex
和std::timed_mutex
的特点,允许同一个线程多次加锁,并提供了尝试加锁的超时功能。
使用场景: 适用于需要递归锁定资源,并且希望能够设置尝试获取锁的超时时间的场景。这在需要防止线程在等待锁时无限阻塞的复杂递归调用中特别有用。
代码示例:
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::recursive_timed_mutex rt_mtx;
void recursive_access(int level, int thread_id)
{
if (rt_mtx.try_lock_for(std::chrono::milliseconds(100)))
{
std::cout << "Thread " << thread_id << " entered level " << level << std::endl;
if (level > 0)
{
std::this_thread::sleep_for(std::chrono::milliseconds(50));
recursive_access(level - 1, thread_id);
}
rt_mtx.unlock();
}
else
{
std::cout << "Thread " << thread_id << " could not enter level " << level << std::endl;
}
}
int main() {
std::thread t1(recursive_access, 3, 1);
std::thread t2(recursive_access, 3, 2);
t1.join();
t2.join();
return 0;
}
输出结果: 输出结果可能会变化,因为线程的执行和锁的获取取决于操作系统的线程调度策略。可能的输出包括两个线程交替进入不同的递归级别,或者一个线程完全执行完毕后另一个线程开始执行。
解释: 在这个示例中,每个线程尝试进入一个递归函数recursive_access
,该函数使用std::recursive_timed_mutex
。每个线程在进入下一个递归级别之前会尝试获取锁,并设置超时时间为100毫秒。如果一个线程在递归的某个级别成功获取了锁,它会打印信息然后在释放锁之前休眠50毫秒。如果获取锁失败(可能由于另一个线程正在持有锁),它将打印未能进入该级别的消息。
这种类型的锁是非常特定场景下的工具,适用于需要递归锁控制的同时又不希望线程在获取不到锁时无限等待的情况。
三、读写锁(Shared Mutex)
std::shared_mutex
含义: 允许多个线程同时读取资源,但只允许一个线程写入。
使用场景: 适用于读操作远多于写操作的情况。
代码示例:
#include <iostream>
#include <shared_mutex>
#include <thread>
std::shared_mutex shared_mtx;
int data = 0;
void reader_function(int id)
{
shared_mtx.lock_shared();
std::cout << "Reader " << id << " sees data as: " << data << std::endl;
shared_mtx.unlock_shared();
}
void writer_function(int new_data)
{
shared_mtx.lock();
data = new_data;
std::cout << "Writer updates data to: " << data << std::endl;
shared_mtx.unlock();
}
int main()
{
std::thread writer(writer_function, 100);
std::thread readers[10];
for (int i = 0; i < 10; ++i)
{
readers[i] = std::thread(reader_function, i);
}
writer.join();
for (auto& reader : readers)
{
reader.join();
}
return 0;
}
输出结果: 输出结果可能会有所不同,因为读写顺序由操作系统的线程调度决定。
解释: 本例中,一个写线程在修改数据,多个读线程在同时读数据。通过std::shared_mutex
,我们允许多个读操作同时进行,但写操作是独占的。
四、自旋锁
自旋锁在C++标准库中没有直接提供一个专门的类型,但它可以使用原子操作,尤其是std::atomic_flag
来实现。自旋锁是一种低级同步机制,适用于锁持有时间非常短的情况。与其他锁不同,当自旋锁无法获取锁时,它将在一个循环中持续检查锁的状态,这意味着它会保持CPU的活跃状态,而不是使线程进入休眠。
含义:自旋锁是一种在等待解锁时使线程保持忙等(busy-wait)的锁,这意味着线程会持续占用CPU时间直到它能获取到锁。
使用场景:自旋锁适用于锁持有时间非常短且线程不希望在操作系统调度中频繁上下文切换的场景。这通常用在低延迟系统中,或者当线程数量不多于CPU核心数量时,确保CPU不会在等待锁时空闲。
自旋锁的代码示例
下面是使用std::atomic_flag
实现简单自旋锁的示例:
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
class SpinLock
{
private:
std::atomic_flag lock_flag = ATOMIC_FLAG_INIT;
public:
void lock()
{
while (lock_flag.test_and_set(std::memory_order_acquire))
{
// 循环等待,直到锁变为可用状态
}
}
void unlock()
{
lock_flag.clear(std::memory_order_release);
}
};
SpinLock spinlock;
void work(int id)
{
spinlock.lock();
std::cout << "Thread " << id << " entered critical section." << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟工作
std::cout << "Thread " << id << " leaving critical section." << std::endl;
spinlock.unlock();
}
int main()
{
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i)
{
threads.emplace_back(work, i);
}
for (auto& th : threads)
{
th.join();
}
return 0;
}
输出结果:
Thread 0 entered critical section.
Thread 0 leaving critical section.
Thread 1 entered critical section.
Thread 1 leaving critical section.
...
解释:
在此示例中,SpinLock
类使用std::atomic_flag
实现。lock
方法通过test_and_set
在一个循环中尝试设置标志位直到成功,从而实现锁的功能。unlock
方法通过clear
清除标志位。这保证了在某个时刻只有一个线程可以进入临界区,同时使用了忙等待而不是线程休眠。由于忙等待的性质,自旋锁特别适用于预期锁只被短暂持有的场景。
五、唯一锁(Unique Lock)
std::unique_lock
含义: 比std::lock_guard
更灵活的锁,支持延迟锁定、时间锁定、以及在同一作用域中的锁的转移。
使用场景: 需要在复杂控制流中灵活管理锁的情况。
代码示例:
#include <iostream>
#include <mutex>
#include <thread>
std::mutex mtx;
void print_even(int x)
{
if (x % 2 == 0)
{
std::unique_lock<std::mutex> lock(mtx);
std::cout << x << " is even." << std::endl;
}
else
{
std::cout << x << " is odd." << std::endl;
}
}
int main()
{
std::thread threads[10];
for (int i = 0; i < 10; ++i)
{
threads[i] = std::thread(print_even, i);
}
for (auto& th : threads)
{
th.join();
}
return 0;
}
输出结果:
0 is even.
1 is odd.
2 is even.
3 is odd.
...
解释: 此示例中,我们仅在打印偶数时获取锁。std::unique_lock
允许在需要时才加锁,这提供了比std::lock_guard
更大的灵活性。
附加
1、锁保护(Lock Guard)
std::lock_guard
含义: 自动管理锁的生命周期,确保作用域结束时释放锁。
使用场景: 当你需要确保在当前作用域结束时自动释放锁,以避免死锁。
代码示例:
#include <iostream>
#include <mutex>
#include <thread>
std::mutex mtx;
void print_block(int n, char c)
{
std::lock_guard<std::mutex> lock(mtx);
for (int i = 0; i < n; ++i)
{
std::cout << c;
}
std::cout << '\n';
}
int main() {
std::thread t1(print_block, 50, '*');
std::thread t2(print_block, 50, '$');
t1.join();
t2.join();
return 0;
}
输出结果:
**************************************************
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
或者两行输出的顺序可能会反过来,取决于线程的调度。
解释: 这个示例中,我们使用std::lock_guard
来确保在打印过程中互斥锁被持有。这样可以避免输出交错。
2、条件变量
条件变量是一种同步原语,它可以阻塞一个或多个线程,直到某个特定条件为真。条件变量总是与互斥锁(std::mutex
)一起使用,以避免竞争条件。基本操作包括:
- 等待(wait):线程阻塞,并释放其持有的互斥锁,直到另一个线程通知(notify)条件变量。
- 通知(notify_one/notify_all):解除一个或所有等待线程的阻塞状态。
如何使用条件变量
条件变量用于复杂的同步问题,例如当线程需要等待某些条件(如资源可用或任务完成)满足时。它们不仅用于避免死锁,还用于减少不必要的忙等待,使得线程管理更为高效。
代码示例
假设有一个生产者-消费者场景,其中生产者不能在缓冲区满时生产,消费者不能在缓冲区空时消费:
#include <iostream>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> products;
void producer(int id)
{
for (int i = 0; i < 5; ++i)
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return products.size() < 5; });
products.push(i);
std::cout << "Producer " << id << " produced " << i << std::endl;
lock.unlock();
cv.notify_all();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
void consumer(int id)
{
while (true)
{
std::unique_lock<std::mutex> lock(mtx);
if (cv.wait_for(lock, std::chrono::seconds(1), [] { return !products.empty(); }))
{
int product = products.front();
products.pop();
std::cout << "Consumer " << id << " consumed " << product << std::endl;
lock.unlock();
cv.notify_all();
}
else
{
break; // Assume done if no production for 1 second.
}
}
}
int main()
{
std::thread p1(producer, 1), p2(producer, 2);
std::thread c1(consumer, 1), c2(consumer, 2);
p1.join();
p2.join();
c1.join();
c2.join();
return 0;
}
在这个例子中,我们使用了条件变量来同步生产者和消费者之间的操作:
- 生产者在队列未满时生产,并在生产后通知消费者;
- 消费者在队列非空时消费,并在消费后通知生产者。
使用条件变量的优势在于它能够减少资源的浪费,提高线程间的协作效率,特别是在需要频繁等待特定条件的场景中。