编程实践
结合C++ Effective系列参考树、尤其是工程经验教训的总结。
并发
- 除非必要,尽量少用线程。
- 多线程编程要守护好内存,使用atomic、mutex、condition variable、future、semaphore、latch、barrier等同步机制避免数据竞争。
- 尽量缩小临界区,临界区指独占的资源,禁止其他线程访问变量的代码片段,如持有mutex的作用域。与回调函数相似,应尽可能精简此类操作,避免执行耗时的处理、阻塞性操作如sleep。能在临界区、回调函数外处理的,尽可能在外部。
- 必须正确管理多线程中的对象,避免某线程正在访问的对象被另一线程清理,如用move方法使对象从一个线程正确移交给另一个线程,避免内存泄漏。
- 使用condition variable时,必须增加条件判断并在循环中等待。下例中,线程
ReceivingUnPro
中条件变量错过了通知,缺少条件判断,就始终处于等待状态,而ReceivingPro
依靠while判断条件,解决了问题。
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>
std::condition_variable g_dataCond;
std::string g_words = "";
std::mutex g_mtx;
bool g_isReady = false;
void ReceivingUnpro() {
std::unique_lock<std::mutex> lg(g_mtx);
std::cout << "waiting!" << std::endl;
g_dataCond.wait(lg);
std::cout << "Bad receiving thread got the message: " << g_words
<< std::endl;
}
void ReceivingPro() {
std::unique_lock<std::mutex> lg(g_mtx);
while (!g_isReady) {
g_dataCond.wait(lg);
}
std::cout << "Protected receiving thread got the message: " << g_words
<< std::endl;
}
void Sending() {
std::lock_guard<std::mutex> lg(g_mtx);
g_words.append("Go forward!");
g_isReady = true;
g_dataCond.notify_all();
}
int main() {
std::thread b(Sending);
b.join();
std::thread ap(ReceivingPro);
ap.join();
std::thread a(ReceivingUnpro);
a.join();
return 0;
}
理论上,条件变量有虚假唤醒问题,所以要条件判断避免。文章来源:https://www.toymoban.com/news/detail-833455.html
- 用std::lock_gaurd、std::unique_lock确保锁被释放,不要用std::mutex的lock()、unlock()以免遗忘或异常导致dead lock。
错误的例子,
#include <mutex>
std::mutex x;
{
x.lock();
// 处理数据的代码
// 发生异常,导致后面未执行
x.unlock();
}
下例中出现dead lock,文章来源地址https://www.toymoban.com/news/detail-833455.html
#include <unistd.h>
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
using std::cout;
using std::endl;
using std::lock_guard;
using std::mutex;
using std::chrono::seconds;
mutex m1, m2, m3;
std::condition_variable cnd;
std::mutex dt_mtx;
bool ready = false;
std::vector<int> vec_i;
void ProcessData() {
for (const auto i : vec_i) {
std::cout << i << " ";
}
std::cout << std::endl;
}
void T1F() {
std::unique_lock<std::mutex> lock(dt_mtx);
cnd.wait(lock, [] { return ready; });
ProcessData();
}
void T2F() {
{
std::lock_guard<std::mutex> lock(dt_mtx);
for (int i = 0; i < 10; i++) {
vec_i.push_back(i);
sleep(1);
}
ready = true;
}
cnd.notify_all();
}
void Fun1() {
lock_guard<mutex> l1(m1);
std::this_thread::sleep_for(seconds(1));
lock_guard<mutex> l2(m2);
std::this_thread::sleep_for(seconds(1));
cout << "Fun1 is finishing" << endl;
}
void Fun2() {
lock_guard<mutex> l1(m2);
std::this_thread::sleep_for(seconds(1));
lock_guard<mutex> l2(m3);
std::this_thread::sleep_for(seconds(1));
cout << "Fun2 is finishing" << endl;
}
void Fun3() {
lock_guard<mutex> l1(m3);
std::this_thread::sleep_for(seconds(1));
lock_guard<mutex> l2(m1);
std::this_thread::sleep_for(seconds(1));
cout << "Fun3 is finishing" << endl;
}
int main() {
std::thread t1(Fun1);
std::thread t2(Fun2);
std::thread t3(Fun3);
if (t1.joinable()) {
cout << "t1 is joining" << endl;
t1.join();
}
if (t2.joinable()) {
cout << "t2 is joining" << endl;
t2.join();
}
if (t3.joinable()) {
cout << "t3 is joining" << endl;
t3.join();
}
#ifdef VERSION1
std::thread t1(T1F);
std::thread t2(T2F);
t1.join();
t2.join();
#endif
return 0;
}
对象与内存管理
- 避免访问越界,如索引数组前判断下标是否超出数组区间。
- 申请内存要先检查大小。
- 数组作为函数参数,若是已知的固定长度,建议用std::array;若不确定长度,传递来的数组表现为指针,则函数参数要加上数组长度,或者将数组的指针与长度封装为一个类。
- 避免函数返回其局部变量的地址,建议改为返回复制的值。
- lambda的作用范围超出局部时,如用于线程等,按引用捕获可能导致局部变量过早被清理,应改为按值捕获;应明确捕获的变量、合理的捕获类型。
常量
- 建议优先用
constexpr
定义常量,编译时硬编码变量,提升效率;改关键字要求被修饰对象在编译时可确定变量的值。 - 使用
const
修饰函数内部不会修改的参数,类内不变的变量、不修改不可变的成员变量的成员方法。 - const实例化的对象,只允许调用其const方法。
到了这里,关于【编程】C++语言编程规范-2的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!