C++锁机详解:应用场景与实用代码一网打尽
Linux开发架构之路 2024-06-13

一、互斥锁(Mutex)

1. std::mutex

含义: std::mutex 最基本的互斥锁,当一个线程占用锁时,其他线程必须等待该锁被释放。

使用场景: 当需要保护共享资源不被多个线程同时修改时使用。

代码示例:

#include#include#includestd::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#include#includestd::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#include#include#includestd::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_mutexstd::timed_mutex的特点,允许同一个线程多次加锁,并提供了尝试加锁的超时功能。

使用场景: 适用于需要递归锁定资源,并且希望能够设置尝试获取锁的超时时间的场景。这在需要防止线程在等待锁时无限阻塞的复杂递归调用中特别有用。

代码示例:

#include#include#include#includestd::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#include#includestd::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#include#include#includeclass 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::vectorthreads; 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#include#includestd::mutex mtx; void print_even(int x) { if (x % 2 == 0) { std::unique_locklock(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#include#includestd::mutex mtx; void print_block(int n, char c) { std::lock_guardlock(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#include#include#include#includestd::mutex mtx; std::condition_variable cv; std::queueproducts; void producer(int id) { for (int i = 0; i < 5; ++i) { std::unique_locklock(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_locklock(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; }

在这个例子中,我们使用了条件变量来同步生产者和消费者之间的操作:

  • 生产者在队列未满时生产,并在生产后通知消费者;

  • 消费者在队列非空时消费,并在消费后通知生产者。

使用条件变量的优势在于它能够减少资源的浪费,提高线程间的协作效率,特别是在需要频繁等待特定条件的场景中。



声明: 本文转载自其它媒体或授权刊载,目的在于信息传递,并不代表本站赞同其观点和对其真实性负责,如有新闻稿件和图片作品的内容、版权以及其它问题的,请联系我们及时删除。(联系我们,邮箱:evan.li@aspencore.com )
0
评论
  • 相关技术文库
  • C语言
  • 编程
  • 软件开发
  • 程序
  • 数据库管理:有效组织、维护和控制数据

    数据库是存放数据的仓库。它的存储空间很大,可以存放百万条、千万条、上亿条数据。但是数据库并不是随意地将数据进行存放,是有一定的规则的,否则查询的效率会很低。当今世界是一个充满着数据的互联网世界,充斥着...

    昨天
  • 管理信息系统的标准和要求

    管理信息系统(Management Information System,简称MIS)是一个以人为主导,利用计算机硬件、软件、网络通信设备以及其他办公设备,进行信息的收集、传输、加工、储存、更新、拓展和维护的系统。 管理信息系统(...

    昨天
  • 自动化控制技术的广泛应用

    在现代科学技术的众多领域中,自动控制技术起着越来越重要的作用。自动控制是指在没有人直接参与的情况下,利用外加的设备或装置(称控制装置或控制器),使机器,设备或生产过程(统称被控对象)的某个工作状态或参数(...

    昨天
  • python计算平均数的IPO模式

    求平均值的方法:首先新建一个python文件;然后初始化sum总和的值;接着循环输入要计算平均数的数,并计算总和sum的值;最后利用“总和/数量”的公式计算出平均数即可。65a220812a55465d245f518b81fb68d2.png本文操作...

    07-18
  • 怎么理解RPC远程过程调用?

    一.什么是RPC?RPC(remote process call),中文是远程过程调用的意思。怎么理解这个远程过程调用呢?可以这样理解,可以与本地的过程调用对比下,本地过程调用,也就是调用函数或者是调用方法,比如说,在单体架...

    07-18
  • 磁带驱动器备份数据的5个常见问题解决方案

    磁带驱动器是一种用于读写磁带的工具。磁带驱动器(tape drive)是存储计算机中的数据到磁带上的设备,其主要目的是数据备份和归档。磁带驱动器,注意当你的时候意味着要更换磁盘驱动器里的磁盘而且要备份一整夜?我们...

    07-18
  • 局域网是如何连接形成更大范围的信息处理系统的?

    本地电话网是在一个封闭编号区内,由端局(或端局、汇接局)、局间中继线、长市中继线以及端局的用户线、电话机、用户交换机所组成的自动电话网。 每个本地电话网均为自动电话交换网,有一个单独的长途区号,一个长...

    07-18
  • 如何使用数字滤波器进行信号处理

    数字滤波器是由数字乘法器、加法器和延时单元组成的一种算法或装置。数字滤波器的功能是对输入离散信号的数字代码进行运算处理,以达到改变信号频谱的目的。 如果采用通用的计算机,随时编写程序就能进行信号处理...

    07-18
  • 宽带智能网多媒体业务运营成本降低策略

    宽带智能网,是研究在以atm为基础的宽带网络上利用智能网技术如何开发各种多媒体业务。宽带智能网不是简单地将多种业务集成,它的目的是要实现一个可编程的业务平台,实现业务的灵活加载、扩展和新业务的增加。与以...

    07-18
  • DSL技术如何解决“最后一公里”传输瓶颈问题?

    所谓领域专用语言(domain specific language / DSL),其基本思想是“求专不求全”,不像通用目的语言那样目标范围涵盖一切软件问题,而是专门针对某一特定问题的计算机语言。DSL之于程序员正如伽南地之于以色列人,是...

    07-18
  • 基于muduo高性能网络库+Protobuf开发

    前言介绍基于c++的分布式网络框架,项目基于muduo高性能网络库+Protobuf开发,实现的主要功能是通过

    07-12
下载排行榜
更多
评测报告
更多
EE直播间
更多
广告