文章目录
更新中
本文将多线程中的一些加锁部分抽取出来深入学习,C++11部分有了一些崭新的库,C++17又支持了共享互斥锁。而C++98更偏向于C语言的方式,所以将C和C++区分开展示案例。
C语言加锁总结
1. 几种锁
1.1 互斥锁
1.1.1 普通互斥锁
std::mutex | 说明 |
---|---|
基础含义 | 只有一个线程可以占用该锁,其他线程想要使用是必须等待该线程释放 |
基本用法 | std::mutex mut_num; mut_num.lock(); mut_num.unlock(); mut_num.join();//主线程阻塞,等待子线程执行结束 |
使用场景 | 多个线程同时对一个资源进行修改时可能发生意向不到的结果, 所以需要加锁以确保多个线程不会使用混乱 |
注意事项 | 不可重入:同一个线程对同一个互斥锁加锁两次会导致死锁 手动解锁:使用完毕后必须手动解锁,否则会导致死锁 |
加锁代码示例
//mutex.cpp
#include <thread>
#include <mutex>
#include <cstring>
#include <chrono>
#include <iostream>
char g_buf[10];
std::mutex mut_buf;
#define MAX_TH 3
void pthread_fun1(){
mut_buf.lock();
strcpy(g_buf, "abcdef");
std::this_thread::sleep_for(std::chrono::seconds(1));
printf("%s\n", g_buf);
mut_buf.unlock();
}
int main()
{
std::thread arr1[MAX_TH];
for(int i=0; i<MAX_TH; ++i) {
arr1[i] = std::thread(pthread_fun1);
}
for(int i=0; i<MAX_TH; ++i) {
if(arr1[i].joinable())
arr1[i].join();
}
return 0;
}
输出:
abcdef
abcdef
abcdef
附CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(test)
set(CMAKE_CXX_STANDARD 11)
#set(EXECUTABLE_OUTPUT_PATH )
add_executable(mutex.exe mutex.cpp)
target_link_libraries(mutex.exe pthread)
不解锁代码错误示例
#include <thread>
#include <mutex>
#include <cstring>
#include <chrono>
#include <iostream>
char g_buf[10];
std::mutex mut_buf;
#define MAX_TH 3
void pthread_fun1(){
mut_buf.lock();
strcpy(g_buf, "abcdef");
std::this_thread::sleep_for(std::chrono::seconds(1));
printf("%s\n", g_buf);
//mut_buf.unlock();
}
int main()
{
std::thread arr1[MAX_TH];
for(int i=0; i<MAX_TH; ++i) {
arr1[i] = std::thread(pthread_fun1);
}
for(int i=0; i<MAX_TH; ++i) {
if(arr1[i].joinable())
arr1[i].join();
}
return 0;
}
输出结果,程序挂死
abcdef
1.1.2 递归互斥锁
std::recursive_mutex | 说明 |
---|---|
基础含义 | 只有一个线程可以占用该锁,其他线程想要使用是必须等待该线程释放 |
基本用法 | std::recursive_mutex mut_num; mut_num.lock(); mut_num.unlock(); mut_num.join();//主线程阻塞,等待子线程执行结束 |
使用场景 | 一个线程多个函数对一个资源加锁,或者在递归函数中多次对同一个资源加锁 |
注意事项 | 可重入:同一个线程对递归互斥锁加锁两次可正常加锁 手动解锁:使用完毕后必须手动解锁,否则会导致死锁 |
1.2 读写锁
std::shared_mutex | 说明 |
---|---|
头文件 | #include <shared_mutex> |
基础含义 | 允许多个线程读同一个资源,但只允许一个资源写入 |
使用场景 | 适用于读操作远多于写操作的情况 |
注意事项 | 可重入:同一个线程对递归互斥锁加锁两次可正常加锁 手动解锁:使用完毕后必须手动解锁,否则会导致死锁 版本: C++17后才可使用 |
使用方法
std::shared_mutex mut_num;
mut_num.lock();
mut_num.unlock();
mut_num.lock_shared();
mut_num.unlock_shared();
mut_num.join(); // 主线程阻塞,等待子线程执行结束
代码示例
#include <thread>
#include <shared_mutex>
#include <cstring>
#include <iostream>
char g_buf[10];
std::shared_mutex mtx;
#define MAX_TH 3
void pthread_write()
{
mtx.lock();
strcpy(g_buf, "abcdef");
std::this_thread::sleep_for(std::chrono::seconds(1));
printf("write: %s\n", g_buf);
mtx.unlock();
}
void pthread_read()
{
mtx.lock_shared();
std::this_thread::sleep_for(std::chrono::seconds(1));
printf("read: %s\n", g_buf);
mtx.unlock_shared();
}
int main()
{
std::thread arr1[MAX_TH], arr2[MAX_TH];
for(int i=0; i<MAX_TH; ++i)
{
arr1[i] = std::thread(pthread_write);
arr2[i] = std::thread(pthread_read);
}
for(int i=0; i<MAX_TH; ++i)
{
if(arr1[i].joinable()){
arr1[i].join();
}
if(arr2[i].joinable()){
arr2[i].join();
}
}
return 0;
}
输出结果:读的线程同时输出,写的线程隔一秒输出
write: abcdef
read: abcdef
read: abcdef
read: abcdef
write: abcdef
write: abcdef
1.3 自旋锁
含义:自旋锁是指线程在等待解锁时,使线程处于忙等的锁,这意味着线程将持续占用CPU,知道加锁成功。
说明: C++标准库并没有提供专门的自选锁类型,但可以使用std::atomic_flag
来实现。
使用场景: 自旋锁适用于锁持有时间非常短,且线程不希望在操作系统调度中频繁上下文切换的场景。这通常用在低延迟系统中,或者当线程数量不多于CPU数量时,确保CPU不会在等待锁时空闲。
代码示例
#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;
}
1.4 定时锁
1.4.1 普通定时锁
1.4.2 递归定时锁
2. 锁保护
在加锁时,最大的危险不是不会加锁,而是在使用完毕后忘记释放锁。本章节将介绍几种更安全的加锁方式。
2.1 lock_guard
std::lock_guard
可以实现锁的自动管理,在作用域结束后自动释放锁。
**实现原理:**lock_guard类对象为局部变量,在函数声明周期结束时,对象的声明周期结束,自动调用析构函数,实现解锁。
代码示例
#include <thread>
#include <mutex>
#include <cstring>
#include <chrono>
#include <iostream>
char g_buf[10];
std::mutex mtx;
#define MAX_TH 3
void pthread_fun1()
{
std::lock_guard<std::mutex> lock(mtx);
strcpy(g_buf, "abcdef");
std::this_thread::sleep_for(std::chrono::seconds(1));
printf("%s\n", g_buf);
}
int main()
{
std::thread arr1[MAX_TH];
for(int i=0; i<MAX_TH; ++i)
{
arr1[i] = std::thread(pthread_fun1);
}
for(int i=0; i<MAX_TH; ++i)
{
if(arr1[i].joinable())
arr1[i].join();
}
return 0;
}
实现原理
template<typename T>
class self_lock
{
public:
self_lock(T ¶)
{
plockInst = ¶
plockInst->lock();
}
~self_lock()
{
if(plockInst)
{
plockInst->unlock();
}
}
private:
T *plockInst;
};
2.2 唯一锁
std::unique_lock
也是可以自动解锁,但比std::lock_guard
的功能更丰富。
唯一锁支持第二个参数