互斥可以对操作集合上锁,避免多个线程同时访问共享资源。这些操作集合可以看作是一次原子操作。
本文章的代码库:
https://gitee.com/gamestorm577/CppStd
1. 互斥类
c++提供了各种互斥类。
mutex
构造函数
默认构造函数构造一个处于未锁定状态的互斥
锁定
mutex提供lock和try_lock接口用于锁定互斥,unlock用于解锁互斥。
1.调用线程调用lock时,如果mutex为未锁定状态,那么调用线程锁住该互斥;如果mutex为锁定状态,那么调用线程阻塞,知道mutex被解锁。
2.调用线程调用try_lock尝试锁定mutex并立即返回。如果mutex为未锁定状态,那么try_lock返回true并锁住mutex;如果mutex为锁定状态,try_lock返回false。
3.调用线程调用unlock解锁互斥。
4.调用线程调用lock或者try_lock时必须不占有mutex,调用unlock时mutex必须被调用线程锁定,否则会出现未定义行为。
代码示例:
int num = 0;
std::mutex mutex;
auto Func = [&](int id)
{
for (int i = 0; i < 5; ++i)
{
mutex.lock();
++num;
std::cout << id << ": " << num << std::endl;
mutex.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
};
std::thread t1(Func, 0);
std::thread t2(Func, 1);
t1.join();
t2.join();
可能的输出结果:
0: 1
1: 2
0: 3
1: 4
0: 5
1: 6
0: 7
1: 8
1: 9
0: 10
native_handle
返回mutex的底层实现句柄
timed_mutex
相比mutex,timed_mutex可以实现指定时间的锁定,对应的接口为try_lock_for和try_lock_until。
recursive_mutex
对于mutex,当某个执行线程已经占用mutex的情况下不能再调用lock或者try_lock,而recursive_mutex可以去掉这个限制。注意在占用mutex的情况下,调用recursive_mutex的lock或者try_lock也是有次数限制的。
recursive_time_mutex
和recursive_mutex的功能相同,同时增加了指定时间的锁定功能。
shared_mutex
shared_mutex有两个访问级别:共享性和独占性。如果一个执行线程获取了shared_mutex的独占行,那么其他线程无法获取该锁。当没有任何线程获取shared_mutex的独占性时,多个线程可以同时获取shared_mutex的共享性。
对于一些共享资源读取远多于写入操作的场景,shared_mutex可以提高并发性能。
shared_mutex的lock、try_lock接口用于获取独占性,unlock用于解锁独占性。lock_shared、try_lock_shared接口口用于获取共享性,unlock_shared用于解锁共享性。
代码示例:
auto Timer = [](std::function<void()> func) -> void
{
auto start = std::chrono::system_clock::now();
func();
auto end = std::chrono::system_clock::now();
std::chrono::duration<double, std::milli> time = end - start;
std::cout << "use time: " << time.count() << std::endl;
};
struct Test1
{
void Func()
{
m_Mut.lock();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
m_Mut.unlock();
}
std::mutex m_Mut;
};
struct Test2
{
void Func()
{
m_Mut.lock_shared();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
m_Mut.unlock_shared();
}
std::shared_mutex m_Mut;
};
auto Func1 = []()
{
Test1 test1;
std::vector<std::thread> threads(20);
for (auto& t : threads)
{
t = std::thread(&Test1::Func, &test1);
}
for (auto& t : threads)
{
t.join();
}
};
auto Func2 = []()
{
Test2 test2;
std::vector<std::thread> threads(20);
for (auto& t : threads)
{
t = std::thread(&Test2::Func, &test2);
}
for (auto& t : threads)
{
t.join();
}
};
Timer(Func1);
Timer(Func2);
输出结果:
use time: 326
use time: 33.2421
shared_timed_mutex
shared_timed_mutex和shared_mutex的功能相同,并且支持指定时间的锁定
2. lock类
c++提供了一些lock类用于方便地管理互斥类
lock_guard
lock_guard提供互斥体的RAII风格的包装。当创建lock_guard对象时,它试图占有给定互斥的所有权,当lock_guard离开作用域时,lock_guard被销毁并释放互斥体。代码示例:
struct MyMutex
{
void lock()
{
std::cout << "MyMutex lock" << std::endl;
}
void unlock()
{
std::cout << "MyMutex unlock" << std::endl;
}
};
MyMutex mut;
std::lock_guard<MyMutex> lock(mut);
输出结果:
MyMutex lock
MyMutex unlock
scoped_lock
scoped_guard提供多个互斥体的RAII风格的包装。代码示例:
struct MyMutex
{
void lock()
{
std::cout << "MyMutex lock: " << Id << std::endl;
}
bool try_lock()
{
std::cout << "MyMutex try_lock: " << Id << std::endl;
return true;
}
void unlock()
{
std::cout << "MyMutex unlock: " << Id << std::endl;
}
int Id = 0;
};
MyMutex mut1;
MyMutex mut2;
MyMutex mut3;
mut1.Id = 3;
mut2.Id = 17;
mut3.Id = 29;
std::scoped_lock lock(mut1, mut2, mut3);
输出结果:
MyMutex lock: 3
MyMutex try_lock: 17
MyMutex try_lock: 29
MyMutex unlock: 3
MyMutex unlock: 17
MyMutex unlock: 29
unique_lock
unique_lock是一个通用的互斥包装器。
shared_lock
shared_lock是一个通用的共享互斥包装器。
3. 通用锁定算法
c++提供了std::try_lock、std::lock用于锁定互斥
4. 单次调用
c++提供了接口std::call_once来确保函数只被调用一次,无论是单线程还是多线程。代码示例:
struct Test
{
void Func()
{
std::cout << "call Func: " << Id << std::endl;
}
void CallFuncOnce()
{
std::call_once(flag, &Test::Func, this);
}
std::once_flag flag;
int Id = 0;
};
Test test1;
Test test2;
test1.Id = 3;
test2.Id = 17;
test1.CallFuncOnce();
test1.CallFuncOnce();
test1.CallFuncOnce();
test1.CallFuncOnce();
std::jthread t1(&Test::CallFuncOnce, &test2);
std::jthread t2(&Test::CallFuncOnce, &test2);
std::jthread t3(&Test::CallFuncOnce, &test2);
输出结果:
call Func: 3
call Func: 17