C++ 多线程02:互斥量(mutex)
admin
2024-05-14 19:14:34
0次

C++ 多线程:互斥量(mutex)

文章目录

  • C++ 多线程:互斥量(mutex)
    • std::mutex
    • std::recursive_mutex
    • std::time_mutex
    • std::recursive_timed_mutex
    • std::shared_mutex
    • std::shared_timed_mutex
    • 总结

C++ 11中的互斥量,声明在 头文件中,互斥量的使用可以在各种方面,比较常用在对共享数据的读写上,如果有多个线程同时读写一个数据,那么想要保证多线程安全,就必须对共享变量的读写进行保护(上锁),从而保证线程安全。 互斥量主要有四中类型:

  • std::mutex,最基本的 Mutex 类。
  • std::recursive_mutex,递归 Mutex 类。
  • std::time_mutex,限时 Mutex 类。
  • std::recursive_timed_mutex,限时递归 Mutex 类。 当然C++14和C++17各增加了一个:
  • std::shared_timed_mutex,限时读写锁(C++14)
  • std::shared_mutex,读写锁(C++17)

std::mutex

构造函数

mutex();
mutex(const mutex&) = delete;
复制代码

从上面的构造函数可以看出,std::mutex不允许拷贝构造,当然也不允许move,最初构造的mutex对象是处于未锁定状态的,若构造不成功会抛出 std::system_error 。

析构函数

~mutex();
复制代码

销毁互斥。若互斥被线程占有,或在占有mutex时线程被终止,则会产生未定义行为。

lock

void lock();
复制代码

锁定互斥,调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:

  • 如果该互斥量当前没有被其他线程锁住,则调用线程将该互斥量锁住,直到调用unlock之前,该线程一直拥有该锁。
  • 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住,指导其他线程unlock该互斥量。
  • 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

try_lock

bool try_lock();
复制代码

尝试锁住互斥量,立即返回。成功获得锁时返回 true ,否则返回 false。 如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况:

  • 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。
  • 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。
  • 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

unlock

void unlock();
复制代码

解锁互斥。互斥量必须为当前执行线程所锁定(以及调用lock),否则行为未定义。

看下面一个简单的例子实现两个线程竞争全局变量g_num对其进行写操作,然后打印输出:

#include 
#include   // std::chrono
#include   // std::thread
#include   // std::mutexint g_num = 0;  // 为 g_num_mutex 所保护
std::mutex g_num_mutex;void slow_increment(int id) 
{for (int i = 0; i < 3; ++i) {g_num_mutex.lock();++g_num;std::cout << "th" << id << " => " << g_num << '\n';g_num_mutex.unlock();std::this_thread::sleep_for(std::chrono::seconds(1));}
}  int main()
{std::thread t1(slow_increment, 0);std::thread t2(slow_increment, 1);t1.join();t2.join();
}
复制代码

加了互斥量实现有序的写操作并输出:

th0 => 1
th1 => 2
th0 => 3
th1 => 4
th1 => 5
th0 => 6
复制代码

如果不增加mutex包含,可能输出就不是有序的打印1到6,如下:

thth01 => 2 => 2th1 => 3
th0 => 4
th0 => 5
th1 => 6
复制代码

std::recursive_mutex

如上面所说的,如果使用std::mutex,如果一个线程在执行中需要再次获得锁,会出现死锁现象。要避免这种情况下就需要使用递归式互斥量std::recursive_mutex,它不会产生上述的死锁问题,可以理解为同一个线程多次获得锁“仅仅增加锁的计数”,同时,必须要确保unlock和lock的次数相同,其他线程才可能取得这个mutex。它的接口与std::mutex的完全一样,用法也基本相同除了可重入(必须同一线程才可重入,其他线程需等待),看下面的例子:

#include 
#include 
#include class X {std::recursive_mutex m;std::string shared;public:void fun1() {m.lock();shared = "fun1";std::cout << "in fun1, shared variable is now " << shared << '\n';m.unlock();}void fun2() {m.lock();shared = "fun2";std::cout << "in fun2, shared variable is now " << shared << '\n';fun3(); // 递归锁在此处变得有用std::cout << "back in fun2, shared variable is " << shared << '\n';m.unlock();}void fun3() {m.lock();shared = "fun3";std::cout << "in fun3, shared variable is now " << shared << '\n';m.unlock();}
};int main() 
{X x;std::thread t1(&X::fun1, &x);std::thread t2(&X::fun2, &x);t1.join();t2.join();
}
复制代码

在fun2中调用fun3,而fun3中还使用了lock和unlock,只有递归式互斥量才能满足当前情况。

输出如下:

in fun1, shared variable is now fun1
in fun2, shared variable is now fun2
in fun3, shared variable is now fun3
back in fun2, shared variable is fun3
复制代码

std::time_mutex

timed_mutex增加了带时限的try_lock。即try_lock_for和try_lock_until。

try_lock_for 尝试锁互斥。阻塞直到超过指定的 timeout_duration 或得到锁,取决于何者先到来。成功获得锁时返回 true,否则返回false 。函数原型如下:

template< class Rep, class Period >
bool try_lock_for( const std::chrono::duration& timeout_duration );
复制代码

若timeout_duration小于或等于timeout_duration.zero(),则函数表现同try_lock()。由于调度或资源争议延迟,此函数可能阻塞长于timeout_duration。

#include 
#include 
#include 
#include 
#include 
#include std::timed_mutex mutex;using namespace std::chrono_literals;void do_work(int id) {std::ostringstream stream;for (int i = 0; i < 3; ++i) {if (mutex.try_lock_for(100ms)) {stream << "success ";std::this_thread::sleep_for(100ms);mutex.unlock();} else {stream << "failed ";}std::this_thread::sleep_for(100ms);}std::cout << "[" << id << "] " << stream.str() << std::endl;
}int main() {// try_lock_forstd::vector threads;for (int i = 0; i < 4; ++i) {threads.emplace_back(do_work, i);}for (auto& t : threads) {t.join();}
}复制代码
[3] failed success failed 
[0] success failed success 
[2] failed failed failed 
[1] success success success 
复制代码

try_lock_until 也是尝试锁互斥。阻塞直至抵达指定的timeout_time或得到锁,取决于何者先到来。成功获得锁时返回 true,否则返回false。timeout_time与上面的timeout_duration不一样,timeout_duration表示一段时间,比如1秒,5秒或者10分钟,而timeout_time表示一个时间点,比如说要等到8点30分或10点24分才超时。

使用倾向于timeout_time的时钟,这表示时钟调节有影响。从而阻塞的最大时长可能小于但不会大于在调用时的 timeout_time - Clock::now() ,依赖于调整的方向。由于调度或资源争议延迟,函数亦可能阻塞长于抵达timeout_time之后。同try_lock(),允许此函数虚假地失败并返回false,即使在 timeout_time 前的某点任何线程都不锁定互斥。函数原型如下:

template< class Clock, class Duration >
bool try_lock_until( const std::chrono::time_point& timeout_time);
复制代码

看下面的例子:

#include 
#include 
#include 
#include 
#include 
#include std::timed_mutex mutex;
using namespace std::chrono;void do_work() {mutex.lock();std::cout << "thread 1, sleeping..." << std::endl;std::this_thread::sleep_for(std::chrono::seconds(4));mutex.unlock();
}void do_work2() {auto now = std::chrono::steady_clock::now();if (mutex.try_lock_until(now + 5s)) {auto end = steady_clock::now();std::cout << "try_lock_until success, ";std::cout << "time use: " << duration_cast(end-now).count() << "ms." << std::endl;mutex.unlock();} else {auto end = steady_clock::now();std::cout << "try_lock_until failed, ";std::cout << "time use: " << duration_cast(end-now).count() << "ms." << std::endl;}
}int main() {// try_lock_untilstd::thread t1(do_work);std::thread t2(do_work2);t1.join();t2.join();
}
复制代码

获得锁时输出:

thread 1, sleeping...
try_lock_until success, time use: 4000ms.
复制代码

修改一下,让其超时,输出:

thread 1, sleeping...
try_lock_until failed, time use: 5000ms.
复制代码

std::recursive_timed_mutex

以类似std::recursive_mutex的方式,recursive_timed_mutex提供排他性递归锁,同线程可以重复获得锁。另外,recursive_timed_mutex通过try_lock_for与try_lock_until方法,提供带时限地获得recursive_timed_mutex锁,类似std::time_mutex。

std::shared_mutex

cpp 17 新出的具有独占模式和共享模式的锁。共享模式能够被std::shared_lock(这个后面再详细将)占有。

std::shared_mutex 是读写锁,把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。

它提供两种访问权限的控制:共享性(shared)和排他性(exclusive)。通过lock/try_lock获取排他性访问权限(仅有一个线程能占有互斥),通过lock_shared/try_lock_shared获取共享性访问权限(多个线程能共享同一互斥的所有权)。这样的设置对于区分不同线程的读写操作特别有用。

std::shared_mutex通常用于多个读线程能同时访问同一资源而不导致数据竞争,但只有一个写线程能访问的情形。比如,有多个线程调用shared_mutex.lock_shared(),多个线程都可以获得锁,可以同时读共享数据,如果此时有一个写线程调用 shared_mutex.lock(),则读线程均会等待该写线程调用shared_mutex.unlock()。对于C++11 没有提供读写锁,可使用 boost::shared_mutex。

std::shared_mutex新增加的三个接口:

void lock_shared();
bool try_lock_shared();
void unlock_shared();
复制代码

一个简单例子如下:

#include 
#include   // 对于 std::unique_lock
#include 
#include class ThreadSafeCounter {public:ThreadSafeCounter() = default;// 多个线程/读者能同时读计数器的值。unsigned int get() const {std::shared_lock lock(mutex_);return value_;}// 只有一个线程/写者能增加/写线程的值。void increment() {std::unique_lock lock(mutex_);value_++;}// 只有一个线程/写者能重置/写线程的值。void reset() {std::unique_lock lock(mutex_);value_ = 0;}private:mutable std::shared_mutex mutex_;unsigned int value_ = 0;
};int main() {ThreadSafeCounter counter;auto increment_and_print = [&counter]() {for (int i = 0; i < 3; i++) {counter.increment();std::cout << std::this_thread::get_id() << ' ' << counter.get() << '\n';// 注意:写入 std::cout 实际上也要由另一互斥同步。省略它以保持示例简洁。}};std::thread thread1(increment_and_print);std::thread thread2(increment_and_print);thread1.join();thread2.join();
}// 解释:下列输出在单核机器上生成。 thread1 开始时,它首次进入循环并调用 increment() ,
// 随后调用 get() 。然而,在它能打印返回值到 std::cout 前,调度器将 thread1 置于休眠
// 并唤醒 thread2 ,它显然有足够时间一次运行全部三个循环迭代。再回到 thread1 ,它仍在首个
// 循环迭代中,它最终打印其局部的计数器副本的值,即 1 到 std::cout ,再运行剩下二个循环。
// 多核机器上,没有线程被置于休眠,且输出更可能为递增顺序。
复制代码

可能的输出:

139847802500864 1
139847802500864 2
139847802500864 3
139847794108160 4
139847794108160 5
139847794108160 6
复制代码

std::shared_timed_mutex

它是从C++14 才提供的限时读写锁:std::shared_timed_mutex。 对比std::shared_mutex新增下面两个接口,其实这两个接口与上面讲到的std::timed_mutex的try_lock_for和try_lock_until类似。都是限时等待锁。只不过是增加了共享属性。

template< class Rep, class Period >
bool try_lock_shared_for( const std::chrono::duration& timeout_duration );template< class Clock, class Duration >
bool try_lock_shared_until( const std::chrono::time_point& timeout_time );
复制代码

总结

由于它们额外的复杂性,读/写锁std::shared_mutex , std::shared_timed_mutex优于普通锁std::mutex,std::timed_mutex的情况比较少见。但是理论上确实存在。

如果在频繁但短暂的读取操作场景,读/写互斥不会提高性能。它更适合于读取操作频繁且耗时的场景。当读操作只是在内存数据结构中查找时,很可能简单的锁会胜过读/写锁。

如果读取操作的开销非常大,并且您可以并行处理许多操作,那么在某些时候增加读写比率应该会导致读取/写入器性能优于排他锁的情况。断点在哪里取决于实际工作量。

另请注意,在持有锁的同时执行耗时的操作通常是一个坏兆头。可能有更好的方法来解决问题,然后使用读/写锁。

还要注意,在使用mutex时,要时刻注意lock()与unlock()的加锁临界区的范围,不能太大也不能太小,太大了会导致程序运行效率低下,大小了则不能满足我们对程序的控制。并且我们在加锁之后要及时解锁,否则会造成死锁,lock()与unlock()应该是成对出现。

参考: www.apiref.com/cpp-zh/cpp/…

相关内容

热门资讯

蜻蜓订购APP贵金属订购非法期...   蜻蜓订购APP以"投资平台"名义对外宣传,主打贵金属期货(订购)交易。该平台一方面强调自身"正规...
悟空订购APP非法期货交易欺骗...   近期,有投资者反映,“悟空订购APP”等平台以白银、贵金属订购为名,宣传所谓“高收益”“低门槛”...
小赢淘金APP期货投资骗局,平...   “小赢淘金”之流,最擅长的就是用“注册送现金”、“充值返券”的糖衣炮弹来引流。别天真地以为那是福...
八方淘金APP非法期货交易欺骗...   警惕!八方生财、每日淘金、小赢淘金、悟空订购、蜻蜓订购、斗金订购等APP均涉嫌非法期货交易!  ...
智易购APP贵金属订购交易骗局...   很多人看到“3元起投”“充值送福利”“操作简单”“随时提现”这类宣传,很容易把智易购APP当成普...
喜玛银庄APP商品订购投资骗局...   喜玛银庄APP白银、铂金“现货订购”骗局警示:无资质平台涉嫌非法期货交易,投资者爆仓后如何维权?...
吉鑫乐享APP炒贵金属订购交易...   近期,多名投资者反映在“吉鑫乐享APP”进行贵金属投资时遭遇严重亏损,甚至血本无归。看似正规的“...
易掘金APP虚假期货投资骗局,...   “易掘金”的APP它打着“高收益、灵活投资”的旗号,宣称通过“商品订购”就能轻松赚钱,吸引了不少...
在大吉订购APP订购交易亏损被...   近期,不少投资者反映在“大吉订购App”上投资白银、铂金等商品遭遇严重亏损。该平台宣称提供现货订...
铂银壹号APP商品订购交易亏损...   许多中小投资者误以为网上的“商品现货订购”是合规合法的理财渠道,殊不知这背后隐藏着巨大的合规风险...
兴鑫淘金APP贵金属订购交易亏...   白银、贵金属或其他商品类投资,除国家批准的正规期货交易场所及持牌期货公司外,市面上大量第三方交易...
铂银掘金APP无资质的违规平台...   铂银掘金等平台的投资者,往往是被“注册送现金”“充值返现券”“首单免亏”等广告话术吸引,进而下载...
八方淘金APP订购交易不合规,...   “八方淘金APP”“铂银掘金APP”“铂银壹号APP”等软件被指以“现货订购”为名,实际采用高杠...
在指尖订购APP订购商品亏损被...   指尖订购APP这类平台,表面是“订购交易”,实则是披着电商外衣的非法期货陷阱。绝大多数投资者被蒙...
在犀贝易购APP做商品订购交易...   市面上打着“现货投资”“贵金属交易”旗号的APP并不少,但真正合规、受监管的平台并不多。犀贝易购...
易达淘金APP虚假商品订购交易...   易达淘金APP存在严重的虚假宣传与违规交易行为。该平台打着“商品订购”的旗号,实则通过不合规的交...
大浪淘金APP白银订购亏损被骗...   大浪淘金APP最隐蔽的套路,在于其完全封闭的内盘交易系统。表面上,它宣称与国际贵金属市场接轨;实...
一品好购APP上订购白银出现亏...   一品好购APP原名“一品订购”,运营方为上海宝智未来信息技术有限公司。近期不少投资者反映在该平台...
九州订购APP贵金属订购投资骗...   九州订购APP正通过抖.音、快手、今日头条等平台大量投放投资广告,用“5元起投”“充值返现”“0...
坚锅淘金APP交易软件并不正规...   当前,市面上大量所谓的“现货商城订购”交易软件,早已偏离了正常的实物商品买卖,演变为高杠杆、高手...