C++ 加锁的原则

本文围绕C++多线程编程展开,阐述了多线程、进程并行访问共享资源时的锁使用规则,如加锁保护、职责单一、范围尽量小、避免嵌套加锁等,还给出了进程间通讯、可重入函数设计、锁中调用函数及使用跳转语句等方面的建议,以确保线程安全。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

规则1 多线程、进程并行访问共享资源时,一定要加锁保护
说明:共享资源包括全局变量,静态变量,共享内存,文件等。 建议封装像智能指针一样的对象对锁进行管理,比如我们就封装了一个auto_lock,在构造时申请 锁,析构中释放锁,保证不会忘记“解锁”。如果锁的作用范围有限,则可以这样: do { auto_lock lock(&lock); //… }while(0);

规则2 锁的职责单一
说明:每个锁只锁一个唯一共享资源;这样,才能保证锁应用的单一,也能更好的确保加锁的范围尽 量小。 对于共享全局资源,应该根据实际需要,每类或每个资源,有一把锁。这样,这把锁只锁对这个资源 访问的代码,通常这样的代码都会是比较简单的资源操作代码,不会是复杂的函数调用等。相反,如 果我们对几类或几个资源共用一把锁。这把锁的责任范围就大了,使用复杂,很难理清锁之间的关系(有 没有释放锁,或者锁之间的嵌套加锁等),容易导致死锁问题。

规则 3 锁范围尽量小,只锁对应资源操作代码
说明:使用锁时,尽量减少锁的使用范围。我们使用锁,为了方便,会大范围的加锁,如:直接锁几 个函数调用。这种使用,一方面会导致多线程执行效率的低下,容易变成串行执行;另一方面,容易 出现锁未释放,或者锁的代码中再加锁的场景,最后导致死锁。 所以,对锁操作的最好办法,就是只锁简单资源操作代码。对应资源访问完后,马上释放锁。尽量在 函数内部靠近资源操作的地方加锁而不是靠近线程、函数外部加锁。

规则4 避免嵌套加锁;如果必须加锁,务必保证不同地方的加锁顺序是一样的
说明:加上一把锁之后,在释放之前,不能再加锁。 典型的锁中加锁的场景:OMU代码中对几个容器的同时遍历,每个容器一把锁,就导致需要加多把锁。 这种场景的解决方法:先加一把锁,对一个容器遍历,选择出合乎要求的数据,并保存在临时变量中; 再加另一把锁,使用临时变量,再对其他容器遍历。 锁中加锁,必须保证加锁的顺序是一样的,比如先加的锁后解锁, Lock1 Lock2 Unlock2 Unlock1 则其他地方的加锁顺序,必须与这里的顺序一样,避免死锁,不允许出现: lock2 lock1 unlock2 unlock1

建议1 进程间通讯,使用自己保证互斥的数据库系统、共享内存,或socket消息机制;尽量避免使用文件等进程无法管理的资源
说明:由于文件在不同进程间访问,无法保证互斥。当然,可以在进程间加进程锁,但只受限于我们能加锁的进程,对于第三方进程等无法保证。这样,当多个进程同时对文件进行写操作时,将会导致文件数据破坏,或文件写失败等问题。
当数据库系统本身的访问接口带有互斥机制,当多个进程同时访问时,可以保证数据库数据的完整。
共享内存,只限制于使用共享内存的几个进程,需要我们对这些访问共享内存的进程加锁。但由于共享内存,第三方进程等无法访问,这也能比较好的保护数据,避免文件系统存在的问题。
socket消息机制,由操作系统socket通讯机制保证互斥,在多个进程间,通过消息来保证数据的互斥。 进程的消息都是操作系统转发而来的独立数据,属于进程私有数据,不存在进程间并行访问的问题。

建议2 可重入函数尽量只使用局部变量和函数参数,少用全局变量、静态变量
说明:支持多线程并行访问的函数称之为可重入函数。设计可重入函数时,尽量使用局部变量和函数 参数来传递数据,在多线程并行访问时,互相之间不会受影响。相反,如果使用全局变量、静态变量, 就需要同步。 示例: int iTotalCnt = 10; void WriteFile() { for (int i=0; i<iTotalCnt; i++) { //写个数据到内存; } iTotalCnt = 0; //写完数据之后,赋值计数为0 } 上面的函数,如果是并行访问,将会导致有部分调用WriteFile的线程,不执行for循环;因为iTotalCnt 可能被其他线程修改为0。 引申:一些库函数也是非线程安全,调用时可能会出现多线程并发访问问题。

建议3 锁中避免调用函数;如果必须调用函数,务必保证不会造成死锁
说明:这条规则是对加锁范围尽量小(只锁对应资源操作代码)规则的补充。不能把调用函数也加到加 锁范围中。因为被调用函数的内部到底做了什么事情,是如何做的,调用者可能不是很清楚。尤其是 当被调用函数内部又加锁的情况,就容易导致两个锁互饿,导致死锁。 示例: Callfunc() { Lock2; //…. Unlock2; } Thread_func1() { Lock1; Callfunc(); Unlock1; } Thread_func2() { Lock2; Lock1; //… Unlock1; Unlock2; } 当上述线程函数Thread_func1()和Thread_func2()并行执行时,就很有可能导致死锁。而且这种死锁 情况还是比较难分析。因为我们调用函数,很多时候只关注函数实现的功能 ,而忽略函数内部的具体实现。 其次,锁中调用函数,也会把对资源操作的代码扩大化,不利于并行效率。更主要的是,这种操作, 由于加锁的范围变大,引起死锁的可能就增大。

建议4 锁中避免使用跳转语句
说明:跳转语句包含return、break、continue、goto等。如果锁中有宏调用的代码,要特别注意,分 析宏中是否存在隐含的跳转语句。 在函数返回时忘记把锁释放,特别是存在很多分支都可能返回的时候,可能一些分支会忘记释放锁。

<think>好的,用户现在问的是在C++多线程编程中,go函数里是否必须加锁。首先需要明确用户所说的“go函数”具体指的是什么,因为在C++标准库中没有直接叫go的函数。可能用户是指类似Go语言中的goroutine,也就是C++中启动线程的函数,比如std::thread或者某些库中的go函数,比如Boost库中的boost::thread或者某些并行库中的启动函数。假设用户指的是启动线程的函数,那么问题就是在线程函数中是否必须加锁。 接下来,用户的问题核心在于是否需要加锁,这涉及到多线程编程中的共享数据访问问题。需要回顾多线程编程中的基本原则:当多个线程访问共享资源(变量、数据结构等)时,如果至少有一个线程在修改该资源,就会存在数据竞争,导致未定义行为。因此,在这种情况下必须使用或其他同步机制来保证线程安全。 不过,并非所有情况都需要加锁。例如,如果线程之间不共享任何数据,或者共享的数据是只读的,那么不需要加锁。此外,C++11之后引入的原子操作(std::atomic)可以在某些情况下替代,实现无编程,提高性能。 需要分情况讨论:必须加锁的情况和无需加锁的情况,并举例说明。同时,应该提到替代方案,比如原子操作或无数据结构,以及错误使用可能带来的问题,比如死、性能下降等。 用户可能是刚开始学习多线程编程,对的使用场景不太清楚,或者遇到了数据竞争的问题,想了解何时需要加锁。因此,回答需要详细说明不同场景,并给出示例,帮助用户理解何时必须使用,何时可以不用,以及替代方法。 可能还需要纠正用户术语上的误解,比如“go函数”在C++中的对应概念,避免混淆。同时,提醒用户注意线程安全的重要性,特别是在共享数据修改时,必须同步访问,否则会导致不可预测的结果。 总结回答的结构:首先澄清术语,然后分情况讨论必须加锁和无需加锁的场景,给出示例,最后总结最佳实践和注意事项。</think>在C++多线程编程中,是否需要在`go`函数(假设您指线程入口函数)中加锁,**取决于线程间是否存在共享数据的并发访问**。以下是关键原则和场景分析: --- ### 一、必须加锁的场景 ✅ 当多个线程**同时读写同一共享数据**时,必须加锁(如`std::mutex`)以防止数据竞争(Data Race)。 **示例1:共享变量修改** ```cpp #include <thread> #include <mutex> std::mutex mtx; int shared_counter = 0; void thread_func() { for (int i = 0; i < 10000; ++i) { std::lock_guard<std::mutex> lock(mtx); // 必须加锁! ++shared_counter; // 多线程并发写操作 } } int main() { std::thread t1(thread_func); std::thread t2(thread_func); t1.join(); t2.join(); // 最终 shared_counter = 20000(加锁保证正确性) } ``` **未加锁的后果**: - 数据竞争导致`shared_counter`最终值不确定(可能远小于20000)。 - 程序行为未定义(UB),可能崩溃或输出错误结果。 --- ### 二、无需加锁的场景 ✅ #### 1. **线程间无共享数据** 若线程仅操作**独立数据**(如局部变量),无需加锁: ```cpp void thread_func(int id) { int local_var = id; // 每个线程独享的局部变量 // 无需同步操作 } ``` #### 2. **只读共享数据** 所有线程仅读取共享数据,不修改时,无需加锁: ```cpp const std::vector<int> global_readonly_data{1, 2, 3}; void thread_func() { for (int num : global_readonly_data) { // 只读操作 // 无需加锁 } } ``` #### 3. **原子操作替代** 使用`std::atomic`实现无同步(适用于简单类型): ```cpp std::atomic<int> atomic_counter(0); void thread_func() { for (int i = 0; i < 10000; ++i) { ++atomic_counter; // 原子操作,无需显式加锁 } } // 最终 atomic_counter = 20000(线程安全) ``` --- ### 三、的选择与最佳实践 🔧 1. **优先使用RAII** 如`std::lock_guard`或`std::unique_lock`,避免手动`lock/unlock`导致死: ```cpp std::mutex mtx; void safe_write() { std::lock_guard<std::mutex> lock(mtx); // 自动释放 // 操作共享数据... } ``` 2. **减少的粒度** 范围尽量小,避免长时间持有影响性能: ```cpp void optimize_lock() { // 非临界区代码... { std::lock_guard<std::mutex> lock(mtx); // 临界区代码(尽量简短) } // 非临界区代码... } ``` 3. **避免嵌套** 若必须使用多个,按固定顺序加锁,或使用`std::scoped_lock`(C++17+): ```cpp std::mutex mtx1, mtx2; void nested_lock() { std::scoped_lock lock(mtx1, mtx2); // 同时加锁,自动防死 // 操作共享数据... } ``` --- ### 四、总结 | 场景 | 是否需要加锁 | 替代方案 | |-------------------------------|--------------|-----------------------| | 多线程读写同一共享变量 | ✅ 必须 | 原子操作 (`std::atomic`) | | 线程独立数据或只读共享数据 | ❌ 不需要 | — | | 简单计数器/标志位 | ❌ 可无 | `std::atomic` | **核心原则**: - 加锁的目的是为了解决**共享数据的线程安全问题**。 - 无共享或只读共享时无需加锁,优先考虑无设计(如原子操作或线程本地存储)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值