3天2晚详细路书Day 1（4月4日 周五）：北京 → 忻州 → 太原（住太原）核心：忻州古城（速览）+ 晋祠 + 山西博物院（人文双核心）04:30 北京出发导航点：北京（起点）→ 忻州古城（南门）路线：G5京昆高速 → G1812沧榆高速 → 二广高速里程：约450km | 预计耗时：5.5h（含1次服务区休整15min）服务区：保定服务区、五台山服务区10:00 抵达忻州古城游玩时长：1h（速览，不深度）必逛：南北大街、秀容书院（外观）、古城墙午餐：古城内「忻州瓦酥/羊杂割/碗托」（简餐，30min）11:30 离开忻州11:30 忻州 → 太原导航点：忻州古城 → 晋祠（正门）路线：二广高速 → 太原绕城高速里程：约80km | 预计耗时：1h12:30 抵达晋祠12:30–15:00 晋祠（核心景点）门票：80元（建议提前1天线上预约）游玩时长：2.5h必看：圣母殿、鱼沼飞梁、难老泉、周柏唐槐15:00 离开晋祠 → 山西博物院15:30–17:30 山西博物院导航点：晋祠 → 山西博物院（主馆）门票：免费（需预约，带身份证）游玩时长：2h必看：晋侯鸟尊、侯马盟书、虞弘墓石椁17:30 结束游览18:00 入住太原酒店推荐区域：迎泽大街/柳巷（市中心，近美食）晚餐：柳巷「清和元刀削面/认一力蒸饺/过油肉」（1h）休息：缓解长途驾驶疲劳Day 2（4月5日 周六）：太原 → 洛阳（住洛阳）核心：直达洛阳，龙门石窟 + 白马寺（两大世界遗产）07:30 太原出发导航点：太原酒店 → 龙门石窟（西北服务区）路线：二广高速 → 菏宝高速里程：约460km | 预计耗时：5.5h（含2次服务区休整，各20min）服务区：长治服务区、济源服务区（加油/简餐）13:00 抵达洛阳，午餐午餐：龙门石窟附近「牛肉汤/烩面/锅贴」（40min）13:40 前往龙门石窟14:00–16:30 龙门石窟门票：90元（提前预约，含西山+东山+香山寺）游玩时长：2.5h（西山石窟精华，重点看卢舍那大佛）16:30 离开 → 白马寺17:00–18:30 白马寺导航点：龙门石窟 → 白马寺（正门）里程：约15km | 耗时：25min门票：35元游玩时长：1.5h（中国第一古刹，看齐云塔、国际佛殿苑）18:30 结束游览19:00 入住洛阳酒店推荐区域：老城区/十字街（近夜景与美食）晚餐：十字街「洛阳水席/牡丹饼/不翻汤」（1h）晚上：可选逛应天门/天堂明堂夜景（外观免费）Day 3（4月6日 周日）：洛阳 → 小浪底 → 石家庄 → 北京核心：小浪底水库（自然景观）→ 石家庄休整 → 返京07:00 洛阳出发 → 小浪底导航点：洛阳酒店 → 小浪底水利枢纽风景区（南门）路线：王城大道 → 白横线 → 小浪底专用线里程：约40km | 预计耗时：1h08:00 抵达小浪底08:00–09:30 小浪底水库门票：40元游玩时长：1.5h（大坝观景台、黄河湿地公园、泄洪道）09:30 离开小浪底 → 石家庄09:30 小浪底 → 石家庄（休整）导航点：小浪底 → 河北博物院（正门）路线：二广高速 → 菏宝高速 → G4京港澳高速里程：约525km | 预计耗时：6.5h（含2次服务区休整，各20min+午餐40min）16:00 抵达石家庄（河北博物院）16:00–16:40 石家庄休整（河北博物院）门票：免费（带身份证）游玩时长：40min（速览金缕玉衣、长信宫灯）16:40 离开 → 北京16:40 石家庄 → 北京导航点：河北博物院 → 北京（终点）路线：G4京港澳高速 → 北京六环里程：约290km | 预计耗时：3.5h（含1次服务区休整15min）20:10 抵达北京，行程结束预约：晋祠、龙门石窟、山西博物院、河北博物院 务必提前1天在官方公众号预约。路况：4.6下午京港澳高速（石家庄→北京）易堵，16:30前务必离开石家庄。美食清单：忻州（羊杂、瓦酥）、太原（刀削面、过油肉）、洛阳（牛肉汤、水席）。
根据界面QComboBox切换调整需要获取信息类型，按确定后将界面内容整理为json的全流程。定义需要获取的信息类型，避免字符串匹配：enum class EnvInfo { none = 0, Platform = 1, Object = 2, Env = 3 };在EditPlatformAttributeWindow类中维护AttributeWindow的vector指针列表，动态维护AttributeWindow个数：std::vector<AttributeWindow*> m_attribute_windows_;初始化comboBox，并设置槽函数onComboBoxIndexChanged，注意用QVariant绑定，方便后期获取index转为EnvInfo：void AttributeWindow::InitCombox() { ui->comboBox_model_4->clear(); ui->comboBox_model_4->addItem("请选择属性", QVariant::fromValue(static_cast<int>(EnvInfo::none))); ui->comboBox_model_4->addItem("平台", QVariant::fromValue(static_cast<int>(EnvInfo::Platform))); ui->comboBox_model_4->addItem("目标", QVariant::fromValue(static_cast<int>(EnvInfo::Object))); ui->comboBox_model_4->addItem("环境", QVariant::fromValue(static_cast<int>(EnvInfo::Env))); connect(ui->comboBox_model_4, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &AttributeWindow::onComboBoxIndexChanged); }ComboBox切换后调整需要获取的信息：void AttributeWindow::onComboBoxIndexChanged(int index) { QVariant data_ = ui->comboBox_model_4->itemData(index); m_env_type = static_cast<EnvInfo>(data_.toInt()); if (m_env_type == EnvInfo::none) { } if (m_env_type == EnvInfo::Platform) { adjustLabels(true); ui->label_2->setText("id"); ui->label_4->setText("silent_control"); ui->label_5->setText("dof.\nheading"); ui->label_9->setText("coordinate.\nlatitude"); // ... } if (m_env_type == EnvInfo::Object) { adjustLabels(true); ui->label_2->setText("id"); ui->label_3->setText("device_type"); // ... } if (m_env_type == EnvInfo::Env) { adjustLabels(false); ui->label->setText("wind_speed"); ui->label_2->setText("ship_density"); // ... } }根据需要获取的信息，调整label显隐，因为代码过于冗余，单独拆分为函数：void AttributeWindow::adjustLabels(bool is_show) { if (!is_show) { ui->label_3->hide(); ui->textEdit_3->hide(); } else { ui->label_3->show(); ui->textEdit_3->show(); } }获取内容:UIContext AttributeWindow::GetContext() { int index_ = ui->comboBox_model_4->currentIndex(); QVariant data_ = ui->comboBox_model_4->itemData(index_); m_env_type = static_cast<EnvInfo>(data_.toInt()); if (m_env_type==EnvInfo::none) return ui_context_; if (m_env_type == EnvInfo::Platform) { ui_context_.env_type = EnvInfo::Platform; ui_context_.id = ui->textEdit_2->toPlainText(); ui_context_.device_type = ui->textEdit_3->toPlainText(); ui_context_.dof_heading = ui->textEdit_5->toPlainText(); ui_context_.coordinate_latitude = ui->textEdit_8->toPlainText(); // ... return ui_context_; } if (m_env_type == EnvInfo::Object) { ui_context_.env_type = EnvInfo::Object; ui_context_.name = ui->textEdit->toPlainText(); ui_context_.id = ui->textEdit_2->toPlainText(); // ... return ui_context_; } if (m_env_type == EnvInfo::Env) { ui_context_.env_type = EnvInfo::Env; ui_context_.wind_speed = ui->textEdit->toPlainText(); ui_context_.ship_density = ui->textEdit_2->toPlainText(); // ... return ui_context_; } }按确定按钮获取内容： void EditPlatformAttributeWindow::on_pushButton_Confirm_clicked() { this->close(); QJsonObject obj; QJsonArray ObjectAttribute_s; // 环境参数(self、object、env) for (const auto& it : m_attribute_windows_) { if (it->m_env_type == EnvInfo::none) { QMessageBox msgBox; msgBox.setStyleSheet(MYSTYLE::messageBoxStyle); msgBox.setWindowTitle("提示信息"); QString msg_ = "请配置场景属性！！"; msgBox.setText(msg_); msgBox.setWindowFlags(msgBox.windowFlags() | Qt::WindowStaysOnTopHint); QPushButton* closeButton = msgBox.addButton("关闭", QMessageBox::RejectRole); msgBox.exec(); if (msgBox.clickedButton() == closeButton) return; return; } if (it->m_env_type == EnvInfo::Platform) { UIContext content_ = it->GetContext(); QJsonObject SelfAttribute; SelfAttribute["id"] = content_.id; SelfAttribute["device_type"] = content_.device_type; SelfAttribute["silent_control"] = content_.silent_control; QJsonObject DOF_SELF; DOF_SELF["heading"] = content_.dof_heading; // ... SelfAttribute["dof"] = DOF_SELF; // ... obj["self_attribute"] = SelfAttribute; } if (it->m_env_type == EnvInfo::Object) { UIContext content_ = it->GetContext(); QJsonObject ObjectAttribute; ObjectAttribute["name"] = content_.name; ObjectAttribute["id"] = content_.id; ObjectAttribute["device_type"] = content_.device_type; ObjectAttribute["silent_control"] = content_.silent_control; QJsonObject DOF_OBJECT; DOF_OBJECT["heading"] = content_.dof_heading; ObjectAttribute["dof"] = DOF_OBJECT; // ... ObjectAttribute_s.append(ObjectAttribute); } if (it->m_env_type == EnvInfo::Env) { UIContext content_ = it->GetContext(); QJsonObject env; env["wind_speed"] = content_.wind_speed; // ... obj["environment"] = env; } } obj["objects_attribute"] = ObjectAttribute_s; // 分析模式 20/11/25 yilin run_type = ui->comboBox_model->currentIndex(); m_run_type = static_cast<RunModel>(run_type); DataManager::getInstance().SetRunMode(m_run_type); // 仿真模式 20/11/25 yilin simulation_type = ui->comboBox_model_4->currentIndex(); m_simulation_type = static_cast<SinulationMode>(simulation_type); }
一点关于达梦VARCHAR(255)的理解。某涉及达梦数据库的项目中，从达梦数据库取出来数据后需要和本地的std::unordered_map比对，来和本地平台的enum class属性的类型进行匹配，但是std::string之间比对出现了问题，通过for (char c : str) std::cout << (int)c << " "; std::cout << std::endl;输出字符串的每个字符的 ASCII 值，发现ASCII 值后半部分不相同，大概率是达梦数据库针对VARCHAR(255)类型数据兼容性没有做到位，导致ODBC取出来字符串有隐藏的字符。解决方案如下。建立StringComparator类，处理std::string比对的情况。#include <string> #include <algorithm> #include <cctype> class StringComparator { public: enum CompareMode { CaseSensitive, // 区分大小写 IgnoreCase, // 忽略大小写 IgnoreAllSpaces, // 忽略所有空白 IgnoreCaseAndSpaces // 忽略大小写和所有空白 }; // 构造函数，设置比较模式 StringComparator(CompareMode mode = IgnoreCaseAndSpaces) : m_mode(mode) {} // 设置比较模式 void setMode(CompareMode mode) { m_mode = mode; } // 执行比较 bool compare(const std::string& a, const std::string& b) const { std::string sa = trim(a); std::string sb = trim(b); switch (m_mode) { case CaseSensitive: return sa == sb; case IgnoreCase: return toLower(sa) == toLower(sb); case IgnoreAllSpaces: return removeSpaces(sa) == removeSpaces(sb); case IgnoreCaseAndSpaces: return toLower(removeSpaces(sa)) == toLower(removeSpaces(sb)); default: return sa == sb; } } private: CompareMode m_mode; // 工具函数 static std::string trim(const std::string& s) { auto start = std::find_if_not(s.begin(), s.end(), ::isspace); auto end = std::find_if_not(s.rbegin(), s.rend(), ::isspace).base(); return (start < end ? std::string(start, end) : ""); } static std::string toLower(const std::string& s) { std::string result = s; std::transform(result.begin(), result.end(), result.begin(), ::tolower); return result; } static std::string removeSpaces(const std::string& s) { std::string result; std::copy_if(s.begin(), s.end(), std::back_inserter(result), [](char c){ return !std::isspace(c); }); return result; } }; 应用：StringComparator cmp(StringComparator::IgnoreCaseAndSpaces); std::string str1 = " Hello World "; std::string str2 = "hello world"; if (cmp.compare(str1, str2)) { std::cout << "good match" << std::endl; } else { std::cout << "bad match" << std::endl; } // 也可以随时切换比较模式 cmp.setMode(StringComparator::CaseSensitive); if (cmp.compare(str1, str2)) { std::cout << "Strict equality" << std::endl; }
作为开发过程中手册浏览。1.1. 线程代码示例#include <thread> #include <iostream> // Linux -lpthread void ThreadMain() { std::cout << "begin sub thread main " << std::this_thread::get_id() << std::endl; for (int i = 0; i < 10; i++) { std::cout << "in thread " << i << std::endl; std::this_thread::sleep_for(std::chrono::seconds(1)); // 1000ms } std::cout << "end sub thread main " << std::this_thread::get_id() << std::endl; } int main(int argc, char* argv[]) { std::cout << "main thread ID " << std::this_thread::get_id() << std::endl; // 线程创建启动 std::thread th(ThreadMain); std::cout << "begin wait sub thread " << std::endl; // 阻塞等待子线程退出 th.join(); std::cout << "end wait sub thread " << std::endl; return 0; } 1.2. 对象生命周期和线程等待和分离#include <thread> #include <iostream> // Linux -lpthread bool is_exit = false; void ThreadMain() { std::cout << "begin sub thread main " << std::this_thread::get_id() << std::endl; for (int i = 0; i < 10; i++) { if (!is_exit) break; std::cout << "in thread " << i << std::endl; std::this_thread::sleep_for(std::chrono::seconds(1)); // 1000ms } std::cout << "end sub thread main " << std::this_thread::get_id() << std::endl; } int main(int argc, char *argv[]) { { std:: thread th(ThreadMain); //出错，thread对象被销毁 子线程还在运行 } { std::thread th(ThreadMain); th.detach(); // 子线程与主线程分离 守护线程 // 坑 ：主线程退出后 子线程不一定退出 // 可能造成的问题是主线程推出(主程序退出)，程序报错(主线程退出，全部资源释放) // 一般解决方案：通过 is_exit 来控制，起到通知作用 // 如果不做 detach，则必须维护 th 对象 } { std::thread th(ThreadMain); std::this_thread::sleep_for(std::chrono::seconds(1)); // 1000ms is_exit = true; // 通知子线程退出 std::cout << "主线程阻塞，等待子线程退出" << std::endl; th.join(); // 主线程阻塞，等待子线程退出 std::cout << "子线程已经退出！" << std::endl; } std::cin.get(); return 0; }课程中用的vs2019，可以看到thread类的源码，可以看到构造、析构的写法(若没有join则删除对象，释放空间)。如果调用detach，则分离，主线程中不用再关注该线程若join方式来精确控制子线程，则一般需要维系该线程对象，一般将th对象放在成员函数中当线程的执行结果不需要返回给主线程，或者可以通过其他方式通知主线程时，可以使用detach()。例如，一个后台日志记录线程，它只负责将日志信息写入文件，不需要和主线程交互，就可以使用detach()。当线程的执行时间不确定，或者可能比主线程更长时，可以使用detach()。例如，一个网络请求线程，它可能会因为网络延迟或者服务器故障而阻塞很久，如果使用join()，就会导致主线程也无法继续运行，影响用户体验。如果使用detach()，就可以让主线程继续响应用户操作，而网络请求线程在后台等待结果。当线程的执行逻辑和主线程无关，或者可以独立完成时，可以使用detach()。例如，一个音乐播放器程序，它可以创建一个音乐播放线程来控制音乐的播放、暂停、切换等操作，而主线程只负责显示界面和接收用户输入。这样的话，音乐播放线程就可以使用detach()，让它和主线程分离。detach()也有一些潜在的风险和问题，例如：detach()后的线程可能会访问已经被销毁的对象或变量，导致程序崩溃或者数据不一致。例如，如果一个线程要访问主线中的对象以及指针问题。detach()后的线程可能会在主线程结束后还没有执行完毕，导致程序异常退出或者资源泄露。例如，如果一个线程要写入文件或者数据库等外部资源。detach()后的线程可能会难以控制和监控，导致程序逻辑混乱或者性能下降。例如，如果一个程序创建了过多的detach()的线程。因此，在使用detach()时，需要注意以下几点：尽量避免在detach()的线程中访问主线中的对象或变量，或者使用同步机制来保护共享资源。尽量确保detach()的线程在主线结束前能够正常退出，或者提供一种机制来通知detach()的线程退出。尽量控制detach()的线程的数量和质量，避免过度消耗系统资源和影响程序稳定性。1.3. 全局函数作为线程入口分析参数传递内存操作#include <thread> #include <iostream> #include <string> //Linux -lpthread using namespace std; class Para { public: Para() { cout << "Create Para" << endl; } Para(const Para &p) { cout << "Copy Para" << endl; } ~Para() { cout << "Drop Para" << endl; } string name; }; void ThreadMain(int p1, float p2, string str, Para p4) { this_thread::sleep_for(100ms); cout << "ThreadMain " << p1 << " " << p2 << " " << str << " " << p4.name << endl; } int main(int argc, char *argv[]) { thread th; { float f1 = 12.1f; Para p; p.name = "test Para class"; //所有的参数做复制 th = thread(ThreadMain, 101, f1, "test string para", p);// 在这里会产生复制，thread对象在建立时候，会将参数复制 } th.join(); return 0; }如何避免复制，1.4. 线程函数传递指针和引用 中解释。1.4. 线程函数传递指针和引用#include <thread> #include <iostream> #include <string> //Linux -lpthread class Para { public: Para() { std::cout << "Create Para" << std::endl; } Para(const Para &p) { std::cout << "Copy Para" << std::endl; } ~Para() { std::cout << "Drop Para" << std::endl; } std::string name; }; void ThreadMain(int p1, float p2, std::string str, Para p4) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::cout << "ThreadMain " << p1 << " " << p2 << " " << str << " " << p4.name << std::endl; } void ThreadMainPtr(Para *p) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::cout << "ThreadMainPtr name = " << p->name << std::endl; } void ThreadMainRef(Para &p) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::cout << "ThreadMainPtr name = " << p.name << std::endl; } int main(int argc, char *argv[]) { { //传递引用 Para p; p.name = "test ref"; std::thread th(ThreadMainRef, std::ref(p));// ref 是为了标识为引用 th.join(); } std::cin.get(); { //传递线程指针 Para p; p.name = "test ThreadMainPtr name"; std::thread th(ThreadMainPtr, &p); //错误 ，线程访问的p空间会提前释放 // 解决方案是 1. 将 p 放在类成员当中 2. p 传入到堆 3. 静态 p // 所以 p 这个空间，一定要保证在整个线程运行期间，在他的生命周期内 th.detach(); } // Para 释放 std::cin.get(); { //传递线程指针 Para p; p.name = "test ThreadMainPtr name"; std::thread th(ThreadMainPtr, &p); th.join(); std::cin.get(); } std::thread th; { float f1 = 12.1f; Para p; p.name = "test Para class"; //所有的参数做复制 th = std::thread(ThreadMain, 101, f1, "test std::string para", p); } th.join(); return 0; }1.5. 使用成员函数作为线程入口并封装线程基类接口#include <thread> #include <iostream> #include <string> //Linux -lpthread class MyThread { public: //入口线程函数 void Main() { std::cout << "MyThread Main " << name << ":" << age; } std::string name; int age = 100; }; class XThread { public: virtual void Start() { is_exit_ = false; th_ = std::thread(&XThread::Main, this); } virtual void Stop() { is_exit_ = true; Wait(); } virtual void Wait() { if (th_.joinable()) th_.join(); } bool is_exit() { return is_exit_; } private: virtual void Main() = 0; std::thread th_; bool is_exit_ = false; }; class TestXThread : public XThread { public: void Main() override { std::cout << "TestXThread Main begin" << std::endl; while (!is_exit()) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::cout << "." << std::flush;// flush保证及时刷新，cout的内容不会延迟 } std::cout << "TestXThread Main end" << std::endl; } std::string name; }; int main(int argc, char *argv[]) { TestXThread testth;// 进入派生类重载 testth.name = "TestXThread name "; testth.Start(); std::this_thread::sleep_for(std::chrono::seconds(3)); testth.Stop(); testth.Wait(); std::cin.get(); // 成员函数作为参数 MyThread myth; myth.name = "Test name 001"; myth.age = 20; std::thread th(&MyThread::Main, &myth); // &MyThread::Main 成员函数指针 // &myth 指当前对象地址(*this) th.join(); return 0; }1.6. lambda临时函数作为线程入口#include <thread> #include <iostream> #include <string> // test lambda thread class TestLambda { public: void Start() { std::thread th([this]() { std::cout << "name = " << name << std::endl; }); th.join(); } std::string name = "test lambda"; }; int main(int argc, char *argv[]) { std::thread th( [](int i) { std::cout << "test lmbda " << i << std::endl; }, 123 ); th.join(); TestLambda test; test.Start(); return 0; }1.7. 多线程的状态及其切换流程分析线程状态说明：初始化：线程正在创建初始化内存空间等，代码干预不多，从初始化到就绪有一定的消耗，所以才会引入线程池，为了减少类似消耗就绪：线程在就绪列表中，等待CPU调度运行：该线程正在运行阻塞：线程被阻塞挂起。Blocked状态包括：pend(锁、事件、信号量等阻塞)、suspend(主动pend)、delay(延时阻塞)、pendtime(因为锁、事件、信号量事件等超时等待)退出：线程运行结束，等待父线程回收其控制块资源(栈等，不会释放线程运行中创建的堆资源)1.8. 竞争状态和临界区介绍_互斥锁mutex代码演示临界区：读写共享数据的代码片段#include <thread> #include <iostream> #include <string> #include <mutex> // Linux -lpthread using namespace std; static mutex mux; // 尽晚申请，尽早释放 void TestThread() { for (;;) { // 获取锁资源，如果没有则阻塞等待 // mux.lock(); // 操作系统层面的锁 if (!mux.try_lock()) { // try_lock 可以帮助看到多线程竞争锁的情况 cout << "." << flush; this_thread::sleep_for(100ms); // 要进行停顿，否则会不停循环，将资源耗尽 // try_lock可以控制多线程的同时带来了性能开销 continue; } cout << "==============================" << endl; cout << "test 001" << endl; cout << "test 002" << endl; cout << "test 003" << endl; cout << "==============================" << endl; mux.unlock(); // 尽晚申请，尽早释放 this_thread::sleep_for(1000ms); } } int main(int argc, char* argv[]) { for (int i = 0; i < 10; i++) { thread th(TestThread); th.detach(); } std::cin.get(); return 0; } 1.9. 互斥锁的坑_线程抢占不到资源原因和解决方法#include <thread> #include <iostream> #include <string> #include <mutex> static std::mutex mux; void ThreadMainMux(int i) { for (;;) { mux.lock(); std::cout << i << "[in]" << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(100)); mux.unlock(); std::this_thread::sleep_for( std::chrono::milliseconds(1)); // 这里，交给操作系统释放资源 // 总之就是不能在unlock之后，立刻lock。一个小坑，只看代码很可能难以发现 } } int main(int argc, char* argv[]) { for (int i = 0; i < 3; i++) { std::thread th(ThreadMainMux, i + 1); th.detach(); } std::cin.get(); return 0; } 1.10. 超时锁timed_mutex和可重入锁recursive#include <thread> #include <iostream> #include <string> #include <mutex> static std::mutex mux; void TestThread() { for (;;) { // 获取锁资源，如果没有则阻塞等待 // mux.lock(); // if (!mux.try_lock()) { std::cout << "." << std::flush; std::this_thread::sleep_for(std::chrono::milliseconds(100)); continue; } std::cout << "==============================" << std::endl; std::cout << "test 001" << std::endl; std::cout << "test 002" << std::endl; std::cout << "test 003" << std::endl; std::cout << "==============================" << std::endl; mux.unlock(); std::this_thread::sleep_for(std::chrono::milliseconds(1000)); } } void ThreadMainMux(int i) { for (;;) { mux.lock(); std::cout << i << "[in]" << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(1000)); mux.unlock(); std::this_thread::sleep_for(std::chrono::milliseconds(1)); } } std::timed_mutex tmux; void ThreadMainTime(int i) { for (;;) { if (!tmux.try_lock_for(std::chrono::milliseconds(500))) { std::cout << i << "[try_lock_for timeout]" << std::endl; // 尝试获取一个名为tmux的定时互斥锁。如果锁已经被其他线程占用，并且在500毫秒内没有释放， // 则当前线程会返回false，否则会返回true并获取锁。 continue; } std::cout << i << "[in]" << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(2000)); tmux.unlock(); std::this_thread::sleep_for(std::chrono::milliseconds(1)); } // try_lock_for : // 尝试锁定关联互斥。阻塞直至经过指定的 timeout_duration // 或获得锁，之先到来者。成功获得锁时返回 true ，否则返回 false 等效地调用 // mutex()->try_lock_for(timeout_duration) } std::recursive_mutex rmux; void Task1() { rmux.lock(); std::cout << "task1 [in]" << std::endl; rmux.unlock(); } void Task2() { rmux.lock(); std::cout << "task2 [in]" << std::endl; rmux.unlock(); } void ThreadMainRec(int i) { for (;;) { rmux.lock(); Task1(); std::cout << i << "[in]" << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(2000)); Task2(); rmux.unlock(); std::this_thread::sleep_for(std::chrono::milliseconds(1)); } } int main(int argc, char* argv[]) { for (int i = 0; i < 3; i++) { std::thread th(ThreadMainRec, i + 1); th.detach(); } std::cin.get(); for (int i = 0; i < 3; i++) { std::thread th(ThreadMainTime, i + 1); th.detach(); } std::cin.get(); for (int i = 0; i < 3; i++) { std::thread th(ThreadMainMux, i + 1); th.detach(); } std::cin.get(); for (int i = 0; i < 10; i++) { std::thread th(TestThread); th.detach(); } std::cin.get(); return 0; } 还是推荐一下cppreference.com的示例代码：timed_mutex 类是能用于保护数据免受多个线程同时访问的同步原语。以类似 mutex 的行为， timed_mutex 提供排他性非递归所有权语义。另外， timed_mutex 提供通过 try_lock_for() 和 try_lock_until() 方法试图带时限地要求 timed_mutex 所有权的能力。timed_mutex 类满足定时互斥与标准布局类型的所有要求。recursive_mutex 的使用场景之一是保护类中的共享状态，而类的成员函数可能相互调用:#include <iostream> #include <thread> #include <mutex> class X { std::recursive_mutex m; std::string shared; public: void fun1() { std::lock_guard<std::recursive_mutex> lk(m); shared = "fun1"; std::cout << "in fun1, shared variable is now " << shared << '\n'; } void fun2() { std::lock_guard<std::recursive_mutex> lk(m); shared = "fun2"; std::cout << "in fun2, shared variable is now " << shared << '\n'; fun1(); // 递归锁在此处变得有用 std::cout << "back in fun2, shared variable is " << shared << '\n'; }; }; int main() { X x; std::thread t1(&X::fun1, &x); std::thread t2(&X::fun2, &x); t1.join(); t2.join(); }1.11. 共享锁shared_mutex解决读写问题#include <thread> #include <iostream> #include <string> #include <mutex> #include <shared_mutex> // c++17 共享锁 // shared_mutex smux; // c++14 std::shared_timed_mutex stmux; void ThreadRead(int i) { for (;;) { stmux.lock_shared(); std::cout << i << " Read" << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(500)); stmux.unlock_shared(); std::this_thread::sleep_for(std::chrono::milliseconds(1)); } } void ThreadWrite(int i) { for (;;) { stmux.lock_shared(); // 读取数据 stmux.unlock_shared(); stmux.lock(); // 互斥锁 写入 std::cout << i << " Write" << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(300)); stmux.unlock(); std::this_thread::sleep_for(std::chrono::milliseconds(1)); } } int main(int argc, char* argv[]) { for (int i = 0; i < 3; i++) { std::thread th(ThreadWrite, i + 1); th.detach(); } for (int i = 0; i < 3; i++) { std::thread th(ThreadRead, i + 1); th.detach(); } std::cin.get(); return 0; } 共享锁中主要包含两个锁，一个是共享锁，一个是互斥锁。cppreference.com示例代码：其实这个示例更多的是学习两个锁的初始化方式..#include <iostream> #include <mutex> // 对于 std::unique_lock #include <shared_mutex> #include <thread> class ThreadSafeCounter { public: ThreadSafeCounter() = default; // 多个线程/读者能同时读计数器的值。 unsigned int get() const { std::shared_lock<std::shared_mutex> lock(mutex_); // 互斥元保护value_的读写，防止多个线程同时写value_,允许多个线程同时读value_ return value_; } // 只有一个线程/写者能增加/写线程的值。 void increment() { std::unique_lock<std::shared_mutex> lock(mutex_); // unique_lock和shared_lock都是互斥元，但是unique_lock可以在构造时指定锁的类型，而shared_lock只能是共享锁 value_++; } // 只有一个线程/写者能重置/写线程的值。 void reset() { std::unique_lock<std::shared_mutex> 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 // ，再运行剩下二个循环。 // 多核机器上，没有线程被置于休眠，且输出更可能为递增顺序。 /* 可能的输出： 123084176803584 2 123084176803584 3 123084176803584 4 123084185655040 1 123084185655040 5 123084185655040 6 */ 其实上述代码很巧妙，也很可能是开发过程中难以调试的bug。1.12. 手动实现RAII管理mutex资源_锁自动释放这种方式是手动方式，c++11提供了RAII控制锁lock_guard，见：c++11RAII控制锁lock\_guard#include <thread> #include <iostream> #include <string> #include <mutex> #include <shared_mutex> // RAII class XMutex { // 这种简化的类的构造方式值得借鉴 哈 public: XMutex(mutex& mux) : mux_(mux) { std::cout << "Lock" << std::std::endl; mux.lock(); } ~XMutex() { std::cout << "Unlock" << std::std::endl; mux_.unlock(); // 借助析构函数释放资源，RAII主要就是这个思想 } private: mutex& mux_; }; static mutex mux; void TestMutex(int status) { XMutex lock(mux); if (status == 1) { std::cout << "=1" << std::std::endl; return; } else { std::cout << "!=1" << std::std::endl; return; } } int main(int argc, char* argv[]) { TestMutex(1); TestMutex(2); std::cin.get(); return 0; }1.13. c++11RAII控制锁lock_guardcppreference.com： 资源获取即初始化（Resource Acquisition Is Initialization），或称 RAII，是一种 C++ 编程技术，它将必须在使用前请求的资源（分配的堆内存、执行线程、打开的套接字、打开的文件、锁定的互斥体、磁盘空间、数据库连接等——任何存在受限供给中的事物）的生命周期与一个对象的生存期相绑定。RAII 保证资源能够用于任何会访问该对象的函数（资源可用性是一种类不变式，这会消除冗余的运行时测试）。它也保证对象在自己生存期结束时会以获取顺序的逆序释放它控制的所有资源。类似地，如果资源获取失败（构造函数以异常退出），那么已经构造完成的对象和基类子对象所获取的所有资源就会以初始化顺序的逆序释放。这有效地利用了语言特性（对象生存期、退出作用域、初始化顺序以及栈回溯）以消除内存泄漏并保证异常安全。根据 RAII 对象的生存期在退出作用域时结束这一基本状况，此技术也被称为作用域界定的资源管理（Scope-Bound Resource Management，SBRM）。RAII 可以总结如下:将每个资源封装入一个类，其中：构造函数请求资源，并建立所有类不变式，或在它无法完成时抛出异常，析构函数释放资源并且决不会抛出异常；在使用资源时始终通过 RAII 类的满足以下要求的实例：自身拥有自动存储期或临时生存期，或具有与自动或临时对象的生存期绑定的生存期移动语义使得在对象间，跨作用域，以及在线程内外安全地移动所有权，而同时维护资源安全成为可能。(C++11 起)拥有 open()/close()、lock()/unlock()，或 init()/copyFrom()/destroy() 成员函数的类是典型的非 RAII 类的例子：std::mutex m; void bad() { m.lock(); // 请求互斥体 f(); // 如果 f() 抛出异常，那么互斥体永远不会被释放 if(!everything_ok()) return; // 提早返回，互斥体永远不会被释放 m.unlock(); // 只有 bad() 抵达此语句，互斥体才会被释放 } void good() { std::lock_guard<std::mutex> lk(m); // RAII类：互斥体的请求即是初始化 f(); // 如果 f() 抛出异常，那么就会释放互斥体 if(!everything_ok()) return; // 提早返回也会释放互斥体 } // 如果 good() 正常返回，那么就会释放互斥体std::lock_guard示例代码：#include <thread> #include <mutex> #include <iostream> int g_i = 0; std::mutex g_i_mutex; // 保护 g_i void safe_increment() { std::lock_guard<std::mutex> lock(g_i_mutex); ++g_i; std::cout << std::this_thread::get_id() << ": " << g_i << '\n'; // g_i_mutex 在锁离开作用域时自动释放 } int main() { std::cout << "main: " << g_i << '\n'; std::thread t1(safe_increment); std::thread t2(safe_increment); t1.join(); t2.join(); std::cout << "main: " << g_i << '\n'; }lock_guard建立在栈上，离开大括号后自动释放锁，查看lock_guard源码，对应析构函数可以看到。~lock_guard() _LIBCPP_THREAD_SAFETY_ANNOTATION(release_capability()) {__m_.unlock();}另一例：#include <thread> #include <iostream> #include <string> #include <mutex> #include <shared_mutex> // RAII class XMutex { public: XMutex(std::mutex& mux) : mux_(mux) { std::cout << "Lock" << std::endl; mux.lock(); } ~XMutex() { std::cout << "Unlock" << std::endl; mux_.unlock(); } private: std::mutex& mux_; }; static std::mutex mux; void TestMutex(int status) { XMutex lock(mux); if (status == 1) { std::cout << "=1" << std::endl; return; } else { std::cout << "!=1" << std::endl; return; } } static std::mutex gmutex; void TestLockGuard(int i) { gmutex.lock(); { // 已经拥有锁，不lock std::lock_guard<std::mutex> lock(gmutex, std::adopt_lock); // 结束释放锁 } { std::lock_guard<std::mutex> lock(gmutex); std::cout << "begin thread " << i << std::endl; } for (;;) { { std::lock_guard<std::mutex> lock(gmutex); std::cout << "In " << i << std::endl; } std::this_thread::sleep_for(std::chrono::milliseconds(500)); } } int main(int argc, char* argv[]) { for (int i = 0; i < 3; i++) { std::thread th(TestLockGuard, i + 1); th.detach(); } TestMutex(1); TestMutex(2); std::cin.get(); return 0; }lock_guard什么时候释解锁，如果将其放在大括号内，则互斥量充满整个大括号，那么着是不是我们理想的效果，需要注意。另外，临界区内不应该包含sleep()，sleep()的时候，锁还在占用，资源还在占用状态，这样会存在巨大风险。lock_guard构造函数支持两种： explicit lock_guard(mutex_type& __m) _LIBCPP_THREAD_SAFETY_ANNOTATION(acquire_capability(__m)) : __m_(__m) {__m_.lock();} lock_guard(mutex_type& __m, adopt_lock_t) _LIBCPP_THREAD_SAFETY_ANNOTATION(requires_capability(__m)) : __m_(__m) {}第一种直接mutex类型即可完成构造，也就是有lock()和unlock()就可以。第二种知识将锁存下来，但是并未进行锁操作，相同的，析构函数都会unlock()private: lock_guard(lock_guard const&) = delete; lock_guard& operator=(lock_guard const&) = delete;lock_guard无法转移，不能由一个lock_guard到另一个lock_guard。需要注意的是，lock_guard默认互斥资源没有被锁。static std::mutex gmutex; void TestLockGuard(int i) { gmutex.lock(); { std::lock_guard<std::mutex> lock(gmutex); } }gmutex已经在lock_guard之外锁住了资源，上述代码会抛出异常，如果不想锁相关资源，那么利用重载，调用不同构造，传入一个常量参数即可。1.14. unique_lock可临时解锁控制超时的互斥体包装器unique_lock C++11 实现可移动的互斥体所有权包装器支持临时释放锁 unlock支持 adopt_lock（已经拥有锁，不加锁，出栈区会释放）支持 defer_lock （延后拥有，不加锁，出栈区不释放）支持 try_to_lock 尝试获得互斥的所有权而不阻塞 ，获取失败退出栈区不会释放，通过 owns_lock()函数判断支持超时参数，超时不拥有锁#include <thread> #include <iostream> #include <string> #include <mutex> #include <shared_mutex> // RAII class XMutex { public: XMutex(std::mutex& mux) : mux_(mux) { std::cout << "Lock" << std::endl; mux.lock(); } ~XMutex() { std::cout << "Unlock" << std::endl; mux_.unlock(); } private: std::mutex& mux_; }; static std::mutex mux; void TestMutex(int status) { XMutex lock(mux); if (status == 1) { std::cout << "=1" << std::endl; return; } else { std::cout << "!=1" << std::endl; return; } } static std::mutex gmutex; void TestLockGuard(int i) { gmutex.lock(); { // 已经拥有锁，不lock std::lock_guard<std::mutex> lock(gmutex, std::adopt_lock); // 结束释放锁 } { std::lock_guard<std::mutex> lock(gmutex); std::cout << "begin thread " << i << std::endl; } for (;;) { { std::lock_guard<std::mutex> lock(gmutex); std::cout << "In " << i << std::endl; } std::this_thread::sleep_for(std::chrono::milliseconds(500)); } } int main(int argc, char* argv[]) { { static std::mutex mux; { // 基础用法示例，可以先unlock()，然后lock() std::unique_lock<std::mutex> lock(mux); lock.unlock(); // 临时释放锁 lock.lock(); // 退出大括号后依旧会释放锁，和 unique_lock 一样 } { // 已经拥有锁 不锁定，退出栈区解锁 mux.lock(); std::unique_lock<std::mutex> lock(mux, std::adopt_lock); } { // 延后加锁 不拥有 退出栈区不解锁 std::unique_lock<std::mutex> lock(mux, std::defer_lock); // 加锁 退出栈区解锁 lock.lock(); } { // mux.lock(); // 尝试加锁 不阻塞 失败不拥有锁 std::unique_lock<std::mutex> lock(mux, std::try_to_lock); if (lock.owns_lock()) { std::cout << "owns_lock" << std::endl; } else { std::cout << "not owns_lock" << std::endl; } } } for (int i = 0; i < 3; i++) { std::thread th(TestLockGuard, i + 1); th.detach(); } TestMutex(1); TestMutex(2); std::cin.get(); return 0; }注意： defer_lock功能时，__owns_(false)，同步析构时不会解锁unique_lock(mutex_type& __m, defer_lock_t) _NOEXCEPT : __m_(_VSTD::addressof(__m)), __owns_(false) {} ... ~unique_lock() { if (__owns_) __m_->unlock(); }只能手动去lock或unlock1.15. C++14shared_lock共享锁包装器以下这段代码值得商榷，主要看main函数中前两个大括号内的使用方法。#include <thread> #include <iostream> #include <string> #include <mutex> #include <shared_mutex> // Linux -lpthread // RAII class XMutex { public: XMutex(std::mutex& mux) : mux_(mux) { std::cout << "Lock" << std::endl; mux.lock(); } ~XMutex() { std::cout << "Unlock" << std::endl; mux_.unlock(); } private: std::mutex& mux_; }; static std::mutex mux; void TestMutex(int status) { XMutex lock(mux); if (status == 1) { std::cout << "=1" << std::endl; return; } else { std::cout << "!=1" << std::endl; return; } } static std::mutex gmutex; void TestLockGuard(int i) { gmutex.lock(); { // 已经拥有锁，不lock std::lock_guard<std::mutex> lock(gmutex, std::adopt_lock); // 结束释放锁 } { std::lock_guard<std::mutex> lock(gmutex); std::cout << "begin thread " << i << std::endl; } for (;;) { { std::lock_guard<std::mutex> lock(gmutex); std::cout << "In " << i << std::endl; } std::this_thread::sleep_for(std::chrono::milliseconds(500)); } } int main(int argc, char* argv[]) { { // 共享锁 static std::shared_timed_mutex tmux; // 读取锁 共享锁 { // 调用共享锁 std::shared_lock<std::shared_timed_mutex> lock(tmux); std::cout << "read data" << std::endl; // 退出栈区 释放共享锁 } // 写入锁 互斥锁 { // 这里不能先锁共享锁，然后锁互斥锁，死锁 std::unique_lock<std::shared_timed_mutex> lock(tmux); std::cout << "write data" << std::endl; } } { static std::mutex mux; { std::unique_lock<std::mutex> lock(mux); lock.unlock(); // 临时释放锁 lock.lock(); } { // 已经拥有锁 不锁定，退出栈区解锁 mux.lock(); std::unique_lock<std::mutex> lock(mux, std::adopt_lock); } { // 延后加锁 不拥有 退出栈区不解锁 std::unique_lock<std::mutex> lock(mux, std::defer_lock); // 加锁 退出栈区解锁 lock.lock(); } { // mux.lock(); // 尝试加锁 不阻塞 失败不拥有锁 // unique_lock<mutex> lock(mux, try_to_lock); // 锁超时 阻塞指定时间 失败不拥有锁 std::unique_lock<std::mutex> lock(mux, std::chrono::milliseconds(500)); if (lock.owns_lock()) { std::cout << "owns_lock" << std::endl; } else { std::cout << "not owns_lock" << std::endl; } } } for (int i = 0; i < 3; i++) { std::thread th(TestLockGuard, i + 1); th.detach(); } TestMutex(1); TestMutex(2); std::cin.get(); return 0; }1.16. c++17scoped_lock解决互锁造成的死锁问题使用场景：在一个临界区代码中需要使用到两个锁，先用第一个锁lock，再用第二个锁lock。 会出现问题：第一个线程把第一个锁锁住，另外一个线程把第二个锁锁住。 紧接着要访问第二个锁，会造成阻塞。 因为业务代码的问题，很难将锁的顺序定死，在这里用到scoped_lock 测试很难复线，很难测示例代码：#include <thread> #include <iostream> #include <string> #include <mutex> #include <shared_mutex> // Linux -lpthread // RAII class XMutex { public: XMutex(std::mutex& mux) : mux_(mux) { std::cout << "Lock" << std::endl; mux.lock(); } ~XMutex() { std::cout << "Unlock" << std::endl; mux_.unlock(); } private: std::mutex& mux_; }; static std::mutex mux; void TestMutex(int status) { XMutex lock(mux); if (status == 1) { std::cout << "=1" << std::endl; return; } else { std::cout << "!=1" << std::endl; return; } } static std::mutex gmutex; void TestLockGuard(int i) { gmutex.lock(); { // 已经拥有锁，不lock std::lock_guard<std::mutex> lock(gmutex, std::adopt_lock); // 结束释放锁 } { std::lock_guard<std::mutex> lock(gmutex); std::cout << "begin thread " << i << std::endl; } for (;;) { { std::lock_guard<std::mutex> lock(gmutex); std::cout << "In " << i << std::endl; } std::this_thread::sleep_for(std::chrono::milliseconds(500)); } } static std::mutex mux1; static std::mutex mux2; void TestScope1() { // 模拟死锁 停100ms等另一个线程锁mux2 std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::cout << std::this_thread::get_id() << " begin mux1 lock" << std::endl; // mux1.lock(); std::cout << std::this_thread::get_id() << " begin mux2 lock" << std::endl; // mux2.lock(); //死锁 // c++11 // lock(mux1, mux2); // c++17 std::scoped_lock lock(mux1, mux2); // 解决死锁 std::cout << "TestScope1" << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(1000)); // 实际中临界资源中不要sleep // mux1.unlock(); // mux2.unlock(); } void TestScope2() { std::cout << std::this_thread::get_id() << " begin mux2 lock" << std::endl; mux2.lock(); std::this_thread::sleep_for(std::chrono::milliseconds(500)); std::cout << std::this_thread::get_id() << " begin mux1 lock" << std::endl; mux1.lock(); // 死锁 std::cout << "TestScope2" << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(1500)); mux1.unlock(); mux2.unlock(); } int main(int argc, char* argv[]) { { // 演示死锁情况 { std::thread th(TestScope1); th.detach(); } { std::thread th(TestScope2); th.detach(); } } std::cin.get(); { // 共享锁 static std::shared_timed_mutex tmux; // 读取锁 共享锁 { // 调用共享锁 std::shared_lock<std::shared_timed_mutex> lock(tmux); std::cout << "read data" << std::endl; // 退出栈区 释放共享锁 } // 写入锁 互斥锁 { std::unique_lock<std::shared_timed_mutex> lock(tmux); std::cout << "write data" << std::endl; } } { static std::mutex mux; { std::unique_lock<std::mutex> lock(mux); lock.unlock(); // 临时释放锁 lock.lock(); } { // 已经拥有锁 不锁定，退出栈区解锁 mux.lock(); std::unique_lock<std::mutex> lock(mux, std::adopt_lock); } { // 延后加锁 不拥有 退出栈区不解锁 std::unique_lock<std::mutex> lock(mux, std::defer_lock); // 加锁 退出栈区解锁 lock.lock(); } { // mux.lock(); // 尝试加锁 不阻塞 失败不拥有锁 std::unique_lock<std::mutex> lock(mux, std::try_to_lock); if (lock.owns_lock()) { std::cout << "owns_lock" << std::endl; } else { std::cout << "not owns_lock" << std::endl; } } } for (int i = 0; i < 3; i++) { std::thread th(TestLockGuard, i + 1); th.detach(); } TestMutex(1); TestMutex(2); std::cin.get(); return 0; } 析构函数(__unlock_unpack)： ~scoped_lock() { typedef typename __make_tuple_indices<sizeof...(_MArgs)>::type _Indices; __unlock_unpack(_Indices{}, __t_); }1.17. 项目案例线程通信使用互斥锁和list实现线程通信thread_msg_server.md xmsg_server.md xthread.md xmsg_server.md xthread.md1.18. 条件变量应用场景_生产者消费者信号处理步骤（一）改变共享变量的线程步骤准备好信号量std::condition_variable cv;获得 std::mutex （常通过 std::unique_lock ）unique_lock lock(mux);在获取锁时进行修改msgs_.push_back(data);释放锁并通知读取线程lock.unlock(); cv.notify_one(); // 通知一个等待信号线程 cv.notify_all(); // 通知所有等待信号线程 一般是程序或者业务结束时候通知所有（二）等待信号读取共享变量的线程步骤获得与改变共享变量线程共同的mutexunique_lock lock(mux);wait() 等待信号通知 2.1 无lambada 表达式//解锁lock,并阻塞等待 notify_one notify_all 通知 cv.wait(lock); //接收到通知会再次获取锁标注，也就是说如果此时mux资源被占用，wait函数会阻塞 msgs_.front(); //处理数据 msgs_.pop_front();2.2 lambada 表 达 式 cv.wait(lock, [] {return !msgs_.empty();});只在 std::unique_lock<std::mutex> 上工作的 std::condition_variablevoid wait(unique_lock<mutex>& _Lck) { // wait for signal // Nothing to do to comply with // LWG‐2135 because std::mutex lock/unlock are nothrow _Check_C_return(_Cnd_wait(_Mycnd(), _Lck.mutex()‐> _Mymtx())); } template <class _Predicate> void wait(unique_lock<mutex>& _Lck, _Predicate _Pred) { // wait for signal and test predicate while (!_Pred()) { wait(_Lck); } }1.19. condition_variable代码示例读写线程同步一个生产者线程，写入，然后通知多个线程处理。重点是看wait函数，是否锁定的条件。#include <thread> #include <iostream> #include <mutex> #include #include <string> #include <sstream> std::list<std::string> msgs_; std::mutex mux; std::condition_variable cv; void ThreadWrite() { for (int i = 0;; i++) { std::stringstream ss; ss << "Write msg " << i; std::unique_lock<std::mutex> lock(mux); msgs_.push_back(ss.str()); lock.unlock(); // 要先解锁，再发送信号，否则会造成死锁(在cv.wait时会对互斥量上锁) cv.notify_one(); // 发送信号 // cv.notify_all(); std::this_thread::sleep_for(std::chrono::seconds(3)); } } void ThreadRead(int i) { for (;;) { std::cout << "read msg" << std::endl; std::unique_lock<std::mutex> lock(mux); // cv.wait(lock);//解锁、阻塞等待信号 cv.wait(lock, [i] { std::cout << i << " wait" << std::endl; return !msgs_.empty(); }); // 获取信号后锁定 while (!msgs_.empty()) { std::cout << i << " read " << msgs_.front() << std::endl; msgs_.pop_front(); } } } int main(int argc, char* argv[]) { std::thread th(ThreadWrite); th.detach(); for (int i = 0; i < 3; i++) { std::thread th(ThreadRead, i + 1); th.detach(); } std::cin.get(); return 0; }1.20. 条件变量应用线程通信解决线程退出时的阻塞问题thread_msg_server_condition.cpp xmsg_server.cpp xthread.cpp xmsg_server.h xthread.h1.21. promise和future多线程异步传值何时获取线程结果是不确定的，也就是启动线程和获取结果是在两个接口当中。std::promise和std::future在C++中是用于同步两个或更多并发任务的机制。试想一下，你有两个线程，这两个线程需要共享数据。在这种情况下，你可能会使用std::promise和std::future来进行同步。让我们先来看一下每一个的基本定义：std::promise：在某个线程中储存一个值或异常，这个储存的值或异常可被另一个关联的std::future对象获取。std::future：在某个线程中获取一个值或异常，这个值或异常是由之前关联的std::promise对象设置的。下面是一些具体的使用场景：多线程数据交换：你可以在一个线程内设置promise，然后在另一个线程中通过与此promise对应的future来获取这个promise设定的结果。异步操作：当你要在另一个线程中异步执行一些操作并获取结果时，可以使用promise和future机制。你先创建一个promise，然后将这个promise的future交给异步执行的函数。这个异步函数在完成计算后会设置promise的值。之后，主线程可以通过检查future的状态，或者直接调用future的get函数来获取异步操作的结果。将任务分离为生产者和消费者：这是一种经典的并发设计模式，生产者生产数据并设置promise的值，消费者通过future获取这些数据进行处理。这样可以有效地分离两种类型的任务，提高程序的结构化和并发性能。线程间的同步：Future对象的get()会阻塞主线程，直到promised的值被设置，这样就能保证在继续执行程序前，必要的数据或计算结果已经准备好。一个demo： // #define _DEBUG #ifdef _DEBUG #include <iostream> #include <cstddef> #include <utility> #include <iostream> #include <future> #include <thread> int main() { // 来自 packaged_task 的 future std::packaged_task<int()> task([]() { return 7; }); // 包装函数 std::future<int> f1 = task.get_future(); // 获取 future std::thread(std::move(task)).detach(); // 在线程上运行 // 来自 async() 的 future std::future<int> f2 = std::async(std::launch::async, []() { return 8; }); // 来自 promise 的 future std::promise<int> p; std::future<int> f3 = p.get_future(); std::thread([&p] { p.set_value_at_thread_exit(9); }).detach(); std::cout << "Waiting..." << std::flush; f1.wait(); f2.wait(); f3.wait(); std::cout << "Done!\nResults are: " << f1.get() << ' ' << f2.get() << ' ' << f3.get() << '\n'; } #endif #ifndef _DEBUG #include <thread> #include <iostream> #include <future> #include <string> void TestFuture(std::promise<std::string> p) { std::cout << "begin TestFuture" << std::endl; std::this_thread::sleep_for(std::chrono::seconds(3)); std::cout << "begin set value" << std::endl; p.set_value("TestFuture value"); std::this_thread::sleep_for(std::chrono::seconds(3)); std::cout << "end TestFuture" << std::endl; } int main(int argc, char* argv[]) { // 异步传输变量存储 std::promise<std::string> p; // 用来获取线程异步值获取 auto future = p.get_future(); auto th = std::thread(TestFuture, std::move(p)); std::cout << "begin future.get()" << std::endl; std::cout << "future get() = " << future.get() << std::endl; std::cout << "end future.get()" << std::endl; th.join(); std::cin.get(); return 0; } #endifclang编译器：begin future.get() future get() = begin TestFuture begin set value TestFuture value end future.get() end TestFutureMSVC编译器：begin future.get() begin TestFuture begin set value future get() = TestFuture value end future.get() end TestFuture总之end future.get()和end TestFuture的顺序可以看出，get返回不受线程是否退出影响的，另外get是阻塞的，一旦set_value就会取消。1.22. packaged_task 异步调用函数打包#include <thread> #include <iostream> #include <future> #include <string> std::string TestPack(int index) { std::cout << "begin Test Pack " << index << std::endl; std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟阻塞 return "Test Pack return"; } int main(int argc, char *argv[]) { std::packaged_task<std::string(int)> task(TestPack); auto result = task.get_future(); std::thread th(std::move(task), 101); // std::move将task的所有权从当前线程移交给新创建的线程th，因为std::packaged_task对象不可以被复制，只能被移动 std::cout << "begin result get" << std::endl; // 测试是否超时 for (int i = 0; i < 30; i++) { if (result.wait_for(std::chrono::milliseconds(100)) != std::future_status::ready) { continue; } } if (result.wait_for(std::chrono::milliseconds(100)) == std::future_status::timeout) { std::cout << "wait result timeout" << std::endl; } else std::cout << "result get " << result.get() << std::endl; th.join(); std::cin.get(); return 0; }这个代码演示了使用线程（特别是使用std::packaged_task）的几个理由和其优点：并发执行任务：这段代码中的TestPack函数需要耗费2秒（由于std::this_thread::sleep_for(std::chrono::seconds(2))）。如果不使用线程，main()函数会被阻塞，直到TestPack函数执行完成。但是，通过在新线程中运行TestPack，main()函数可以同时执行其它任务。这样，你就可以充分利用多核CPU，并且在等待一个耗时操作（比如I/O操作）完成的同时，运行其他任务。获取异步任务的结果： std::packaged_task将任务的结果存储在std::future对象中。result.get()允许你在任务完成后获得其结果。这意味着你可以在任何你想获取结果的地方获取到它，而不管它是在哪个线程中被计算出来的。方便处理耗时操作：std::packaged_task和std::future在处理耗时操作时非常有用。例如，你可以通过检查std::future的状态来查看操作是否已完成，而不是不断轮询或者阻塞直到操作完成。在上述代码中，我们通过不断检查result的状态来查看TestPack函数是否已完成。总的来说，使用线程和std::packaged_task，可以让代码并发执行任务，不阻塞主线程，同时便于获取异步任务的结果，处理耗时操作。 cppreference类模板 std::packaged_task 包装任何可调用 (Callable) 目标（函数、 lambda 表达式、 bind 表达式或其他函数对象），使得能异步调用它。其返回值或所抛异常被存储于能通过 std::future 对象访问的共享状态中。正如 std::function ， std::packaged_task 是多态、具分配器的容器：可在堆上或以提供的分配器分配存储的可调用对象。#include <iostream> #include <cmath> #include <thread> #include <future> #include <functional> // 避免对 std::pow 重载集消歧义的独有函数 int f(int x, int y) { return std::pow(x, y); } void task_lambda() { std::packaged_task<int(int, int)> task( [](int a, int b) { return std::pow(a, b); }); std::future<int> result = task.get_future(); task(2, 9); std::cout << "task_lambda:\t" << result.get() << '\n'; } void task_bind() { std::packaged_task<int()> task(std::bind(f, 2, 11)); std::future<int> result = task.get_future(); task(); std::cout << "task_bind:\t" << result.get() << '\n'; } void task_thread() { std::packaged_task<int(int, int)> task(f); std::future<int> result = task.get_future(); std::thread task_td(std::move(task), 2, 10); task_td.join(); std::cout << "task_thread:\t" << result.get() << '\n'; } int main() { task_lambda(); task_bind(); task_thread(); }1.23. async创建异步线程替代thread这段代码让我们来看看 C++ 中的异步编程。"async" 和 "future" 使用来创建和同步异步操作。这里的 "async" 函数有两个主要作用：创建一个异步任务。该任务将在单独的线程（可能是现有的线程）上执行。通过这种方式，代码可以同时在多个线程上运行，从而改善程序的性能。返回一个 "future" 对象。这个对象代表了异步任务的结果。当异步任务完成时，可以使用 get() 函数从 "future" 对象中获取结果。如果调用 get() 时任务仍然在运行，那么调用 get() 的线程将阻塞，直到任务完成为止。现在，让我们来具体看看这段代码中的异步操作。在这段代码中，我们创建了两个异步任务。我们使用了两种不同的方式来使用 std::async 函数：对于第一个异步任务，我们使用 async(std::launch::deferred, TestAsync, 100)。这里的 std::launch::deferred 参数意味着异步任务将会在 future.get() 被调用时才执行，而不是立即执行。这种方式下，并没有创建新的线程，而是复用了主线程。对于第二个异步任务，我们使用 async(TestAsync, 101)。在这个调用方式中， std::async 函数将立即创建一个新的线程（除非系统决定复用现有的线程）并开始执行 TestAsync。 TestAsync 任务的执行跟主线程是并行的，因此这是真正的异步调用。你可以通过 std::this_thread::get_id() 的不同输出来区分是否创建了新的线程。在不创建新线程的情况下，std::this_thread::get_id() 的输出将会和主线程的线程 ID 相同。若是创建了新线程，则线程 ID 将会不同。#include <thread> #include <iostream> #include <future> #include <string> std::string TestAsync(int index) { std::cout << index << " begin in TestAsync " << std::this_thread::get_id() << std::endl; std::this_thread::sleep_for(std::chrono::seconds(2)); return "TestAsync string return"; } int main(int argc, char* argv[]) { std::cout << "main thread id " << std::this_thread::get_id() << std::endl; // 不创建线程启动异步任务 std::cout << "==不创建线程启动异步任务==" << std::endl; auto future = async(std::launch::deferred, TestAsync, 100); std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::cout << "begin future get " << std::endl; std::cout << "future.get() = " << future.get() << std::endl; std::cout << "end future get" << std::endl; // 创建异步线程 std::cout << "=====创建异步线程====" << std::endl; auto future2 = async(TestAsync, 101); std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::cout << "begin future2 get " << std::endl; std::cout << "future2.get() = " << future2.get() << std::endl; std::cout << "end future2 get" << std::endl; // 注意get只能调用一次，为了防止出错，用try catch包住 getchar(); return 0; }std::async 和 std::thread 都是 C++11 中用于创建并发任务的方式。不过它们的使用方式和关注点有些不同。std::thread: 它是比较底层的线程创建和操作方式。创建一个 std::thread 会产生一个新的操作系统线程和对应的栈空间，并立即开始并发执行。std::thread 提供了更多的控制，你可以细粒度地管理和调度线程，包括线程优先级、线程间同步、线程局部存储等。但是，使用 std::thread 需要负责处理所有线程同步的问题，比如数据竞争、死锁等，并且还需要手动管理线程的生命周期，比如合适的调用 join() 或 detach()。std::thread 的使用场景通常是在性能优先，且你愿意并熟悉如何处理多线程编程中各种复杂问题的时候。std::async和std::future：这是一种更高层次的并发抽象，主要关注的是任务并发，而不是线程并发。std::async 在创建任务的同时，返回一个std::future，代表异步任务的结果。std::future::get将阻塞等待任务完成并返回结果。任务在何时何地运行，是由实现决定的，可以是新建的线程，也可以是任务队列中的现有线程，甚至可以是调用 get() 的线程（当启动策略是 std::launch::deferred 时）。std::async 自动处理线程的生命周期管理，并为你处理部分线程同步问题。同时，由于使用 std::future 获取结果，也帮你处理了结果的返回和异常处理等问题。std::async 通常适用于需要并发并希望简化多线程编程的场景。所以，你要根据实际的需求和上下文来选择使用 std::async 还是 std::thread，如果你需要对线程有更细的控制，或者需要优化到OS级别的并发，那么 std::thread 可能是一个更好的选择。如果你更关注任务结果，并愿意将具体的运行方式交给系统来决定，希望能简化并发编程，确保异常安全，那么 std::async 可能是更合适的选择。1.23.1. std::launch::async和std::launch::deferred在C++中，std::launch是一个枚举类型，它与std::async函数一起使用，以控制异步操作的行为。这个函数主要用于异步的执行一个任务，返回一个std::future对象，你可以通过这个对象来获取任务的结果。std::launch有两个可能的值：std::launch::async和std::launch::deferred，这两个选项告诉std::async如何执行给定的任务。std::launch::async：当使用该选项时，std::async会尝试在另一个线程上立即启动所提供的任务。这意味着任务会并行执行，与主线程同时进行。当你需要任务并行执行，不阻塞当前线程时使用std::launch::async。std::future对象会与一个正在运行的异步任务相关联，你可以等待或获取这个任务的结果。std::launch::deferred：当使用该选项时，任务不会立即启动，而是延迟到对应的std::future对象的get()或wait()成员函数被调用时才执行。这意味着任务会在这些函数被调用时在调用者线程上执行，也就是说，它实际上变成了惰性求值。当你不需要立刻执行任务，或者希望在将来的某个点上，根据需要执行任务时使用std::launch::deferred。如果从未调用get()或wait()，则对应的任务可能永远不会执行。使用std::async时可以选择给这两个选项中的一个，或者不给，不给的话，实现可以自行选择是立即执行还是延迟执行。如果你想要确保异步任务的执行方式，你应该指定std::launch::async或std::launch::deferred。这是一个简单的例子，展示了如何使用std::async和std::launch::async启动一个异步任务：#include <iostream> #include <future> #include <thread> void task() { // 假设的耗时任务 std::this_thread::sleep_for(std::chrono::seconds(2)); std::cout << "Task completed" << std::endl; } int main() { auto start = std::chrono::high_resolution_clock::now(); // 启动异步任务 std::future<void> result = std::async(std::launch::deferred, task); std::this_thread::sleep_for(std::chrono::seconds(1)); std::cout << "Main thread continues" << std::endl; // 等待异步任务完成 result.wait(); auto end = std::chrono::high_resolution_clock::now(); // 计算时间差 auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start) .count(); std::cout << "Function execution took " << duration << " microseconds." << std::endl; return 0; } 在这个例子中，任务会在一个新的线程中异步地启动。当main函数继续执行时，任务也在并行运行。一旦调用了result.wait()，主线程将会等待直到异步任务完成。如果改用std::launch::deferred，则任务会在调用result.wait()或result.get()时在主线程上同步执行。1.24. c++多核计算分析并实现base16编码一个简单实例#include <thread> #include <iostream> #include <string> #include <vector> #include <chrono> static const char base16[] = "0123456789abcdef"; void Base16Encode(const unsigned char* data, int size, unsigned char* out) { for (int i = 0; i < size; i++) { unsigned char d = data[i]; // 0000 0000 // 1234 5678 >>4 0000 1234 // 1234 5678 & 0000 1111 0000 5678 char a = base16[d >> 4]; char b = base16[d & 0x0F]; out[i * 2] = a; out[i * 2 + 1] = b; } } // C++11 多核base16编码 void Base16EncodeThread(const std::vector<unsigned char>& data, std::vector<unsigned char>& out) { int size = data.size(); int th_count = std::thread::hardware_concurrency(); // 系统支持的线程核心数 // 切片数据 int slice_count = size / th_count; // 余数丢弃 if (size < th_count) // 只切一片 { th_count = 1; slice_count = size; } // 准备好线程 std::vector<std::thread> ths; ths.resize(th_count); // 任务分配到各个线程 for (int i = 0; i < th_count; i++) { // 1234 5678 9abc defg hi int offset = i * slice_count; int count = slice_count; // 最后一个线程 if (th_count > 1 && i == th_count - 1) { count = slice_count + size % th_count; } // cout << offset << ":" << count << endl; ths[i] = std::thread(Base16Encode, data.data() + offset, count, out.data()); } // 等待所有线程处理结束 for (auto& th : ths) { th.join(); } } int main(int argc, char* argv[]) { std::string test_data = "测试base16编码"; unsigned char out[1024] = {0}; Base16Encode((unsigned char*)test_data.data(), test_data.size(), out); std::cout << "base16:" << out << std::endl; // 初始化测试数据 std::vector<unsigned char> in_data; in_data.resize(1024 * 1024 * 20); // 20M // in_data.data(); for (int i = 0; i < in_data.size(); i++) { in_data[i] = i % 256; } std::vector<unsigned char> out_data; out_data.resize(in_data.size() * 2); // 测试单线程base16编码效率 { std::cout << "单线程base16开始计算" << std::endl; auto start = std::chrono::system_clock::now(); Base16Encode(in_data.data(), in_data.size(), out_data.data()); auto end = std::chrono::system_clock::now(); auto duration = duration_cast<std::chrono::milliseconds>(end - start); std::cout << "编码：" << in_data.size() << "字节数据花费" << double(duration.count()) << "毫秒" << std::endl; // cout << out_data.data() << endl; } // 测试c++11 多线程Base16编码 { std::cout << "c++11 多线程Base16编码 开始计算" << std::endl; auto start = std::chrono::system_clock::now(); Base16EncodeThread(in_data, out_data); auto end = std::chrono::system_clock::now(); auto duration = duration_cast<std::chrono::milliseconds>(end - start); std::cout << "编码：" << in_data.size() << "字节数据花费" << double(duration.count()) << "毫秒" << std::endl; // cout << out_data.data() << endl; } std::cin.get(); return 0; }c++11实现多核base16编码并与单核性能测试对比请注意一点，hardware_concurrency，如果线程数多余系统硬件线程数，如果您在一个仅支持两个硬件线程的计算机上创建四个线程，这可能不会产生立即明显的负面影响，但以下是一些可能的影响和考虑因素：上下文切换： 当有多个线程可运行时，操作系统需要在它们之间进行上下文切换。每次切换都涉及保存当前线程的状态并加载下一个线程的状态，这些操作会产生开销。如果线程数量远多于处理器核心，这种上下文切换的成本可能会显著影响性能。资源竞争： 如果线程需要共享资源（如内存、I/O设备等），那么多个线程可能会导致竞争条件和锁争用。这些因素会降低并发性能。线程调度开销： 线程调度器需要跟踪更多的线程，分配CPU时间可能会变得更复杂，并导致额外的开销。应用逻辑复杂性： 编写多线程代码需要仔细的设计来避免死锁、竞态条件和其他多线程问题。线程越多，管理这些问题的难度就越大。然而，如果这些线程不会经常同时运行（即它们大部分时间都在等待外部事件或I/O操作完成），那么创建多于处理器核心数量的线程并不一定会对性能产生负面影响。实际上，这种情况下添加更多的线程可能会帮助提高CPU利用率，因为当其中的一些线程由于I/O操作而阻塞时，其他线程可以继续执行处理。对于I/O密集型应用（比如网络服务器或数据库应用），通常可以通过创建远多于CPU核心数的线程来提高并发性和吞吐量，因为这些线程大部分时间都在等待I/O操作的完成。总之，创建的线程数量是否会对应用程序产生负面影响取决于多种因素，包括应用程序的性能要求、线程的实际工作负载，以及系统的整体行为。通常，在设计多线程应用时，开发者需要仔细评估和测试以确保理想的性能。#include <thread> #include <iostream> #include <string> #include <vector> #include <chrono> #include <execution> using namespace std; using namespace chrono; static const char base16[] = "0123456789abcdef"; void Base16Encode(const unsigned char* data, int size, unsigned char* out) { for (int i = 0; i < size; i++) { unsigned char d = data[i]; // 0000 0000 // 1234 5678 >>4 0000 1234 // 1234 5678 & 0000 1111 0000 5678 char a = base16[d >> 4]; char b = base16[d & 0x0F]; out[i * 2] = a; out[i * 2 + 1] = b; } } // C++11 多核base16编码 void Base16EncodeThread(const vector<unsigned char>& data, vector<unsigned char>& out) { int size = data.size(); int th_count = thread::hardware_concurrency(); // 系统支持的线程核心数 // 切片数据 int slice_count = size / th_count; // 余数丢弃 if (size < th_count) // 只切一片 { th_count = 1; slice_count = size; } // 准备好线程 vector<thread> ths; ths.resize(th_count); // 任务分配到各个线程 for (int i = 0; i < th_count; i++) { // 1234 5678 9abc defg hi int offset = i * slice_count; int count = slice_count; // 最后一个线程 if (th_count > 1 && i == th_count - 1) { count = slice_count + size % th_count; } // cout << offset << ":" << count << endl; ths[i] = thread(Base16Encode, data.data() + offset, count, out.data()); } // 等待所有线程处理结束 for (auto& th : ths) { th.join(); } } int main(int argc, char* argv[]) { string test_data = "测试base16编码"; unsigned char out[1024] = {0}; Base16Encode((unsigned char*)test_data.data(), test_data.size(), out); cout << "base16:" << out << endl; // 初始化测试数据 vector<unsigned char> in_data; in_data.resize(1024 * 1024 * 200); // 20M // in_data.resize(32); //20M // in_data.data(); // for (int i = 0; i < in_data.size(); i++) //{ // in_data[i] = i % 256; // } vector<unsigned char> out_data; out_data.resize(in_data.size() * 2); // 测试单线程base16编码效率 { cout << "单线程base16开始计算" << endl; auto start = system_clock::now(); Base16Encode(in_data.data(), in_data.size(), out_data.data()); auto end = system_clock::now(); auto duration = duration_cast<milliseconds>(end - start); cout << "编码：" << in_data.size() << "字节数据花费" << double(duration.count()) << "毫秒" << endl; // cout << out_data.data() << endl; } // 测试c++11 多线程Base16编码 { cout << "c++11 多线程Base16编码 开始计算" << endl; auto start = system_clock::now(); Base16EncodeThread(in_data, out_data); auto end = system_clock::now(); auto duration = duration_cast<milliseconds>(end - start); cout << "编码：" << in_data.size() << "字节数据花费" << double(duration.count()) << "毫秒" << endl; // cout << out_data.data() << endl; } // 测试C++17 多线程base16编码 { cout << "C++17 多线程base16编码 开始计算" << endl; auto start = system_clock::now(); unsigned char* idata = in_data.data(); unsigned char* odata = out_data.data(); // #include <execution> c++17 std::for_each(std::execution::par, // 并行计算 多核 in_data.begin(), in_data.end(), [&](auto& d) // 多线程进入此函数 { char a = base16[(d >> 4)]; char b = base16[(d & 0x0F)]; int index = &d - idata; odata[index * 2] = a; odata[index * 2 + 1] = b; }); auto end = system_clock::now(); auto duration = duration_cast<milliseconds>(end - start); cout << "编码：" << in_data.size() << "字节数据花费" << double(duration.count()) << "毫秒" << endl; // cout << out_data.data() << endl; } getchar(); return 0; }c++17for_each多核运算示例编码base16clang的c++2017暂未支持execution#include <thread> #include <iostream> #include <string> #include <vector> #include <chrono> #include <execution> using namespace std; using namespace chrono; static const char base16[] = "0123456789abcdef"; void Base16Encode(const unsigned char* data, int size, unsigned char* out) { for (int i = 0; i < size; i++) { unsigned char d = data[i]; // 0000 0000 // 1234 5678 >>4 0000 1234 // 1234 5678 & 0000 1111 0000 5678 char a = base16[d >> 4]; char b = base16[d & 0x0F]; out[i * 2] = a; out[i * 2 + 1] = b; } } // C++11 多核base16编码 void Base16EncodeThread(const vector<unsigned char>& data, vector<unsigned char>& out) { int size = data.size(); int th_count = thread::hardware_concurrency(); // 系统支持的线程核心数 // 切片数据 int slice_count = size / th_count; // 余数丢弃 if (size < th_count) // 只切一片 { th_count = 1; slice_count = size; } // 准备好线程 vector<thread> ths; ths.resize(th_count); // 任务分配到各个线程 for (int i = 0; i < th_count; i++) { // 1234 5678 9abc defg hi int offset = i * slice_count; int count = slice_count; // 最后一个线程 if (th_count > 1 && i == th_count - 1) { count = slice_count + size % th_count; } // cout << offset << ":" << count << endl; ths[i] = thread(Base16Encode, data.data() + offset, count, out.data()); } // 等待所有线程处理结束 for (auto& th : ths) { th.join(); } } int main(int argc, char* argv[]) { string test_data = "测试base16编码"; unsigned char out[1024] = {0}; Base16Encode((unsigned char*)test_data.data(), test_data.size(), out); cout << "base16:" << out << endl; // 初始化测试数据 vector<unsigned char> in_data; in_data.resize(1024 * 1024 * 200); // 20M // in_data.resize(32); //20M // in_data.data(); // for (int i = 0; i < in_data.size(); i++) //{ // in_data[i] = i % 256; // } vector<unsigned char> out_data; out_data.resize(in_data.size() * 2); // 测试单线程base16编码效率 { cout << "单线程base16开始计算" << endl; auto start = system_clock::now(); Base16Encode(in_data.data(), in_data.size(), out_data.data()); auto end = system_clock::now(); auto duration = duration_cast<milliseconds>(end - start); cout << "编码：" << in_data.size() << "字节数据花费" << double(duration.count()) << "毫秒" << endl; // cout << out_data.data() << endl; } // 测试c++11 多线程Base16编码 { cout << "c++11 多线程Base16编码 开始计算" << endl; auto start = system_clock::now(); Base16EncodeThread(in_data, out_data); auto end = system_clock::now(); auto duration = duration_cast<milliseconds>(end - start); cout << "编码：" << in_data.size() << "字节数据花费" << double(duration.count()) << "毫秒" << endl; // cout << out_data.data() << endl; } // 测试C++17 多线程base16编码 { cout << "C++17 多线程base16编码 开始计算" << endl; auto start = system_clock::now(); unsigned char* idata = in_data.data(); unsigned char* odata = out_data.data(); // #include <execution> c++17 std::for_each(std::execution::par, // 并行计算 多核 in_data.begin(), in_data.end(), [&](auto& d) // 多线程进入此函数 { char a = base16[(d >> 4)]; char b = base16[(d & 0x0F)]; int index = &d - idata; odata[index * 2] = a; odata[index * 2 + 1] = b; }); auto end = system_clock::now(); auto duration = duration_cast<milliseconds>(end - start); cout << "编码：" << in_data.size() << "字节数据花费" << double(duration.count()) << "毫秒" << endl; // cout << out_data.data() << endl; } getchar(); return 0; }线程池实现步骤说明高并发运算时，线程频繁创建和销毁会有性能损耗，我们实现线程池直接在里面取即可。初始化线程池启动所有线程准备好任务处理基类和插入任务获取任务接口执行任务线程入口函数完成线程池的初始化和启动demo里仅有初始化和启动CMakeLists main.cpp XThreadPool.cpp XThreadPool.h完成线程池任务调度cv_.wait(lock, predicate)实际上类似于下面这样：while (!predicate()) { cv_.wait(lock); }在这个重载版本的wait()调用中，当线程被唤醒时（不管是因为notify_*调用还是假唤醒），它会自动检查lambda表达式。只有当lambda表达式返回true（这里是!tasks_.empty()），wait()函数才会返回。如果谓词返回false，wait()会再次阻塞线程，这实现了内部的循环检查机制，而不需要显式的while循环。 std::unique_lock<std::mutex> lock(mux_); cv_.wait(lock, [this]() { return !tasks_.empty(); });有哪些优化：针对减少虚假唤醒带来的性能影响，你可以考虑通过减少条件变量的使用或者增加批量处理来实现。由于没有具体的任务处理代码，以下是一段示例代码，说明如何在接收到一个任务时尝试处理多个任务：#include <vector> #include <mutex> #include <condition_variable> #include <functional> #include <queue> // 假设有一个任务类型 using Task = std::function<void()>; // 任务队列和同步原语 std::queue<Task> tasks_; std::mutex mux_; std::condition_variable cv_; void processTaskBatch() { // 在拿锁之前先定义要处理的任务 std::vector<Task> tasksToProcess; { std::unique_lock<std::mutex> lock(mux_); cv_.wait(lock, []() { return !tasks_.empty(); }); // 等待直到有任务 // 一次处理多个任务 while (!tasks_.empty()) { tasksToProcess.emplace_back(std::move(tasks_.front())); tasks_.pop(); // 可以根据需要在这里设置一个批处理的最大值来限制每次处理的任务数量 // 如果性能测试表明处理太多任务会导致锁竞争导致的性能瓶颈 } } // 解锁mutex // 现在已经不持有锁了，在没有阻塞其他线程的情况下处理所有任务 for (auto &task : tasksToProcess) { task(); // 执行任务 } } // 示例：工作线程函数 void workerThread() { while (true) { processTaskBatch(); } } // 假设有一个函数来添加任务到队列中 void addTask(const Task &task) { { std::unique_lock<std::mutex> lock(mux_); tasks_.push(task); } // 已经解锁 // 通知一个等待中的线程有新任务到来 cv_.notify_one(); } 这个代码示例展示了如何让工作线程不只是处理单个任务，而是尝试一次性处理多个任务，以此来减少锁的频繁请求和条件变量的多次等待。这样，拿到锁的线程会尽可能多地完成任务，减少下次需要等待和锁定的概率，同时减少了对条件变量的依赖，也就减少了虚假唤醒所造成的性能影响。对于C++线程池，有一些成熟的库和框架可供选择。其中一些包括：ThreadPool：这是一个轻量级且易于使用的开源C++11线程池库，可在GitHub上找到。Intel TBB（Threading Building Blocks）：由英特尔开发的TBB库提供了丰富的并行算法和数据结构，包括线程池。它是一个高度成熟的并行编程库，可用于各种并行化需求。PPL（Parallel Patterns Library）：PPL是Microsoft Visual C++中的库，提供了用于并行编程的线程池和并行算法。它提供了一种简单的方式来利用多核处理器的能力。以上是一些常用和成熟的C++线程池库和框架，可以根据项目需求和偏好选择合适的库。
《C++ Concurrency In Action 2ed》（第3章 共享数据）
《C++ Concurrency In Action 2ed》（第3章 共享数据）第2 章 线程管理3.1 使用互斥量当多个线程同时访问共享数据时，如果数据只读，则不会出现问题；但如果有线程需要修改共享数据，则可能引发复杂问题。1. 不变量的破坏不变量是数据结构在某一状态下的规则或约束。例如，双链表中每个节点的前后指针必须保持一致。在修改共享数据时，不变量可能会暂时被破坏，直到修改完成后才恢复稳定。如果在不变量被破坏的过程中，其他线程访问了共享数据，就可能导致错误。2. 条件竞争条件竞争是指多个线程对共享数据的访问顺序影响最终结果。良性条件竞争：线程执行顺序不同，但结果仍然可接受。恶性条件竞争：线程执行顺序导致不变量被破坏，可能导致数据损坏或程序崩溃。数据竞争是条件竞争的一种特殊形式，指多个线程同时修改一个独立对象，可能引发未定义行为。3. 恶性条件竞争的特性通常发生在对多个数据块的修改中，如双链表中两个指针的更新。错误难以复现，因为其发生概率较低，且与执行时间敏感。在系统负载高或执行次数增加时，问题出现的概率会提高。4. 避免恶性条件竞争的方法保护机制：使用某种机制（如互斥量）保护数据结构，确保其他线程无法看到不变量的中间状态。无锁编程：通过修改数据结构和不变量，使其变化过程不可分割，但这种方法复杂且容易出错。事务机制：通过事务日志记录和提交数据更新，避免条件竞争（如软件事务内存，STM）。但C++标准目前未直接支持这种方法。互斥量：C++标准库提供的最基本的方式，用于保护共享数据结构。5. 调试难点条件竞争问题可能在调试模式下消失，因为调试模式会影响程序的执行时间。通过这些方法和工具，可以有效避免恶性条件竞争，确保多线程程序的正确性和稳定性。[![top] Goto Top](#table-of-contents)3.2 使用互斥量3.2.1 互斥量核心概念互斥量（std::mutex）是C++中保护共享数据的最基础机制。它的作用是确保同一时刻只有一个线程可以访问共享数据，从而避免条件竞争和数据不一致的问题。使用方式手动锁与解锁 通过std::mutex的成员函数lock()和unlock()，可以手动对互斥量进行加锁和解锁操作，但这种方式容易出错，尤其是在代码中有多个出口（如异常处理）时，可能会导致忘记解锁。RAII机制：std::lock_guard 为了避免手动管理锁的复杂性，C++标准库提供了std::lock_guard，它是一种RAII（资源获取即初始化）机制的封装类。在构造时自动加锁，在析构时自动解锁，从而确保互斥量在任何情况下都能正确解锁。示例代码：使用std::mutex和std::lock_guard#include #include <mutex> #include <algorithm> std::list<int> some_list; // 1. 全局共享数据 std::mutex some_mutex; // 2. 保护共享数据的互斥量 // 添加元素到列表 void add_to_list(int new_value) { std::lock_guard<std::mutex> guard(some_mutex); // 3. 自动加锁 some_list.push_back(new_value); } // 检查列表中是否包含某个值 bool list_contains(int value_to_find) { std::lock_guard<std::mutex> guard(some_mutex); // 4. 自动加锁 return std::find(some_list.begin(), some_list.end(), value_to_find) != some_list.end(); }代码说明：some_list 是一个全局共享的std::list。some_mutex 是保护some_list的互斥量。在add_to_list()和list_contains()函数中，使用std::lock_guard对some_mutex进行加锁，确保线程安全。当线程访问共享数据时，互斥量会将其他线程阻塞，直到当前线程完成操作并解锁。C++17 的改进模板参数推导 在C++17中，std::lock_guard支持模板参数推导，因此可以省略类型声明，代码更加简洁。例如：std::lock_guard guard(some_mutex);std::scoped_lock C++17引入了std::scoped_lock，它是std::lock_guard的增强版，可以同时锁住多个互斥量，适用于需要同时操作多个共享数据的场景。例如：std::scoped_lock guard(some_mutex1, some_mutex2);面向对象的设计在大多数情况下，互斥量通常与需要保护的数据一起放在同一个类中，而不是定义为全局变量。这种设计符合面向对象的封装原则。示例：class ProtectedList { private: std::list<int> some_list; std::mutex some_mutex; public: void add_to_list(int new_value) { std::lock_guard<std::mutex> guard(some_mutex); some_list.push_back(new_value); } bool list_contains(int value_to_find) { std::lock_guard<std::mutex> guard(some_mutex); return std::find(some_list.begin(), some_list.end(), value_to_find) != some_list.end(); } };注意事项不要返回受保护数据的指针或引用 如果成员函数返回受保护数据的指针或引用，调用者可以绕过互斥量直接操作数据，从而破坏数据的安全性。总结std::mutex 是保护共享数据的核心机制。使用std::lock_guard可以简化锁的管理，避免忘记解锁。C++17 提供了std::scoped_lock，进一步增强了多互斥量场景的支持。面向对象的设计可以将互斥量与数据绑定在一起，方便封装和管理。3.2.2 保护共享数据核心概念使用互斥量保护共享数据时，仅仅在每个成员函数中添加一个std::lock_guard对象并不总是足够的。需要仔细检查代码，确保没有通过指针或引用绕过互斥量的保护。否则，即使看似加锁了共享数据，实际上却可能存在安全隐患。主要问题指针或引用泄露：如果某个成员函数返回了受保护数据的指针或引用，调用者可以直接操作数据，而不受互斥量的保护。这种情况下，互斥量的保护形同虚设。通过外部代码间接访问：某些成员函数可能会将受保护的数据传递给外部函数或代码（通过参数或返回值），从而导致数据暴露在未加锁的情况下。运行时参数传递问题：如果受保护数据被传递到一个用户提供的函数中，可能会导致该函数在没有互斥量保护的情况下访问数据，从而引发条件竞争。示例代码：无意中泄露受保护数据以下代码展示了一个常见的错误：将受保护数据的引用传递给外部函数，导致保护失效。#include <mutex> #include <string> #include <iostream> class some_data { int a; std::string b; public: void do_something() { std::cout << "Doing something with data" << std::endl; } }; class data_wrapper { private: some_data data; // 受保护的数据 std::mutex m; // 互斥量 public: template<typename Function> void process_data(Function func) { std::lock_guard<std::mutex> lock(m); // 加锁保护数据 func(data); // 传递数据给用户提供的函数 } }; // 恶意函数：将受保护数据的地址泄露 some_data* unprotected = nullptr; void malicious_function(some_data& protected_data) { unprotected = &protected_data; // 保存受保护数据的地址 } void example_usage() { data_wrapper x; x.process_data(malicious_function); // 传递恶意函数 unprotected->do_something(); // 在没有互斥量保护的情况下访问数据 }问题分析问题描述：process_data函数通过模板参数允许用户传入任意函数，并将受保护数据data传递给该函数。恶意函数malicious_function将data的地址保存到全局指针unprotected中。之后，程序可以通过unprotected直接访问data，而无需加锁，破坏了互斥量的保护。根本原因：process_data函数没有限制用户提供的函数func的行为。数据data在互斥量的作用范围之外被泄露。后果：其他线程可以在没有互斥量保护的情况下访问或修改data，导致数据竞争和未定义行为。解决方案不要返回指针或引用：确保成员函数不会将受保护数据的指针或引用通过返回值或参数传递给调用者。例如：// 错误：返回受保护数据的引用 some_data& get_data() { std::lock_guard<std::mutex> lock(m); return data; // 这样调用者可以直接操作受保护数据 }限制外部访问：避免将受保护数据直接暴露给外部函数或代码。如果必须传递数据，考虑使用只读的副本或深拷贝。封装逻辑：将所有对受保护数据的操作封装在类的成员函数中，避免外部代码直接操作数据。示例代码：改进后的设计以下代码展示了如何避免泄露受保护数据的引用或指针：#include <mutex> #include <string> #include <iostream> class some_data { int a; std::string b; public: void do_something() const { std::cout << "Doing something with data" << std::endl; } }; class data_wrapper { private: some_data data; // 受保护的数据 std::mutex m; // 互斥量 public: void safely_process_data() { std::lock_guard<std::mutex> lock(m); // 加锁保护数据 data.do_something(); // 在锁的保护下操作数据 } };改进点：数据data的操作被封装在data_wrapper的成员函数中。外部代码无法直接访问data，只能通过data_wrapper提供的接口进行操作。总结使用互斥量保护数据时，需要特别注意不要泄露受保护数据的指针或引用。确保所有对受保护数据的访问都在互斥量的作用范围内。封装数据访问逻辑，将互斥量与数据绑定在同一个类中，以减少错误发生的可能性。[![top] Goto Top](#table-of-contents)
《C++ Concurrency In Action 2ed》（第2章 线程管理）
《C++ Concurrency In Action 2ed》（第2章 线程管理）2.1 线程的基本操作线程的基本概念每个C++程序至少有一个主线程，负责执行main()函数。新线程可以通过std::thread对象启动，线程的生命周期由其关联的函数或任务决定。当线程的任务函数执行完毕，线程会自动退出。线程的启动使用std::thread对象启动线程：void do_some_work(); std::thread my_thread(do_some_work);线程启动后会执行do_some_work()。也可以使用可调用对象（如类的operator()）启动线程：class background_task { public: void operator()() const { do_something(); do_something_else(); } }; background_task f; std::thread my_thread(f);注意避免“最令人头痛的语法解析问题”（Most Vexing Parse）：std::thread my_thread(background_task()); // 被解析为函数声明解决方法：使用额外的括号：std::thread my_thread((background_task()));使用统一初始化语法：std::thread my_thread{background_task()};使用Lambda表达式：std::thread my_thread([] { do_something(); do_something_else(); });线程的等待与分离等待线程完成 (join)使用join()可以等待线程完成：std::thread t(do_some_work); t.join(); // 阻塞当前线程，直到`t`完成join()会清理线程相关的资源，调用后std::thread对象不再与线程关联。一个线程只能join一次。分离线程 (detach)使用detach()将线程与std::thread对象分离，线程会在后台运行：std::thread t(do_background_work); t.detach();分离的线程无法再通过std::thread对象控制，线程的资源会在其完成后自动回收。注意在std::thread对象销毁前，必须对其调用join()或detach()，否则会导致程序终止（调用std::terminate()）。线程生命周期管理避免局部变量的引用问题如果线程函数中使用了局部变量的引用，而线程未完成时局部变量已经销毁，会导致未定义行为：struct func { int& i; func(int& i_) : i(i_) {} void operator()() { for (unsigned j = 0; j < 1000000; ++j) { do_something(i); // 可能访问已销毁的局部变量 } } }; void oops() { int some_local_state = 0; func my_func(some_local_state); std::thread my_thread(my_func); my_thread.detach(); // 分离线程 } // some_local_state 被销毁，但线程仍在运行解决方法：将局部变量复制到线程中，而不是传引用。使用join()确保线程结束后再销毁局部变量。使用RAII管理线程RAII（资源获取即初始化）模式可以确保线程在异常或函数退出时正确管理：class thread_guard { std::thread& t; public: explicit thread_guard(std::thread& t_) : t(t_) {} ~thread_guard() { if (t.joinable()) { t.join(); } } thread_guard(const thread_guard&) = delete; thread_guard& operator=(const thread_guard&) = delete; }; void f() { int some_local_state = 0; func my_func(some_local_state); std::thread t(my_func); thread_guard g(t); // RAII管理线程 do_something_in_current_thread(); } // thread_guard析构时会调用t.join()异常安全在异常处理中也要确保线程正确join：void f() { int some_local_state = 0; func my_func(some_local_state); std::thread t(my_func); try { do_something_in_current_thread(); } catch (...) { t.join(); // 确保线程结束 throw; } t.join(); }后台线程（守护线程）使用detach()可以创建后台线程（守护线程），这种线程通常用于执行长期任务或后台监控：void edit_document(std::string const& filename) { open_document_and_display_gui(filename); while (!done_editing()) { user_command cmd = get_user_input(); if (cmd.type == open_new_document) { std::string const new_name = get_filename_from_user(); std::thread t(edit_document, new_name); // 启动新线程 t.detach(); // 分离线程 } else { process_user_input(cmd); } } }适用场景：长期运行的任务（如文件监控、缓存清理等）。不需要主线程直接交互的任务。总结使用std::thread可以方便地启动线程，但需要注意线程的生命周期管理。等待线程：使用join()确保线程完成后释放资源。使用RAII模式或异常处理机制避免线程未正确结束。分离线程：使用detach()让线程在后台运行，但要确保线程的任务不会依赖已销毁的资源。避免问题：避免线程函数中持有局部变量的引用。谨慎处理异常，确保线程安全。[![top] Goto Top](#table-of-contents)2.2 传递参数在C++中，使用 std::thread 创建线程时，可以为线程函数传递参数。传递参数的方式很灵活，但需要注意一些细节，避免潜在的陷阱。基本用法在创建线程时，可以将参数直接传递给 std::thread 的构造函数。参数会被拷贝到新线程的上下文中。void f(int i, std::string const& s); // 一个线程函数 std::thread t(f, 3, "hello"); // 创建线程，传递参数3和"hello"参数拷贝：在例子中，std::thread 将参数 3 和 "hello" 传递给函数 f。注意：std::thread 会将参数拷贝到新线程中，即使函数参数是引用类型。字符串字面量的隐式转换问题如果将字符串字面量（如 "hello"）传递到一个期望 std::string 类型的参数中， std::thread 会隐式地将其转换为 std::string`。但需要小心动态分配的临时对象。错误示例：void f(int i, std::string const& s); void oops(int some_param) { char buffer[1024]; // 临时字符数组 sprintf(buffer, "%i", some_param); // 格式化字符串 std::thread t(f, 3, buffer); // 错误！buffer可能在线程运行前被销毁 t.detach(); }问题：buffer 是局部变量，在线程启动前可能已经销毁。线程可能访问到无效的指针，导致未定义行为。解决办法：在传递参数前，显式地将 buffer 转换为 std::string，以确保线程拥有独立的拷贝。std::thread t(f, 3, std::string(buffer)); // 显式转换为std::string传递引用参数如果线程函数需要引用参数，直接传递会导致 std::thread 拷贝参数，而不是传递引用。这会导致编译错误。错误示例：void update_data(int& data); // 线程函数需要引用参数 int value = 42; std::thread t(update_data, value); // 错误！期望引用，但传递了拷贝解决办法：使用 std::ref 包装引用参数，显式地告诉 std::thread 传递引用。std::thread t(update_data, std::ref(value)); // 使用std::ref传递引用std::ref 是一个辅助函数，用于创建 std::reference_wrapper 对象，保证参数以引用的形式传递。传递成员函数指针可以将类的成员函数作为线程函数传递，但需要同时提供该成员函数所属的对象。示例：class X { public: void do_work(int value) { // 成员函数 } }; X x; std::thread t(&X::do_work, &x, 42); // 调用x.do_work(42)第一个参数是成员函数指针（&X::do_work）。第二个参数是对象指针（&x）。后续参数是传递给成员函数的参数（42）。传递只支持移动的对象有些对象类型（如 std::unique_ptr）只能通过移动语义传递，而不能拷贝。对于这些类型，需要显式地使用 std::move。示例：void process(std::unique_ptr<int> p); std::unique_ptr<int> ptr(new int(42)); std::thread t(process, std::move(ptr)); // 使用std::move转移所有权std::move 将 ptr 的所有权转移到线程中。线程函数 process 将接管 std::unique_ptr 的所有权。总结参数传递方式：默认情况下，std::thread 会拷贝参数到新线程中。使用 std::ref 可以传递引用。使用 std::move 可以传递只支持移动的对象。注意事项：避免传递局部变量的指针或引用，防止线程访问到销毁的变量。显式转换动态分配的临时对象，确保线程拥有独立的拷贝。[![top] Goto Top](#table-of-contents)2.3 转移所有权背景在C++中，std::thread 对象拥有线程的所有权。这意味着每个 std::thread 实例负责管理一个线程的生命周期。为了确保线程的所有权是唯一的，std::thread 对象是不可复制的（non-copyable），但它是可移动的（movable）。这允许我们在程序中将线程的所有权从一个 std::thread 对象转移到另一个对象。时需要转移所有权？转移线程所有权的场景包括：将新创建的线程返回给调用者：一个函数创建线程并将其返回给调用者，而不是自己管理线程。将线程的管理权交给另一个函数：一个函数创建线程后，将其所有权转交给另一个函数进行管理。容器管理线程：当需要将多个线程存储在容器（如 std::vector）中时，线程对象需要支持移动操作。示例代码以下是一个展示如何转移线程所有权的代码示例：void some_function(); void some_other_function(); std::thread t1(some_function); // 创建线程 t1 std::thread t2 = std::move(t1); // 将 t1 的线程所有权转移给 t2 t1 = std::thread(some_other_function); // t1 现在管理一个新线程 std::thread t3; // 创建一个空的线程对象 t3 = std::move(t2); // 将 t2 的线程所有权转移给 t3 t1 = std::move(t3); // 将 t3 的线程所有权转移回 t1代码解析创建线程并转移所有权std::thread t1(some_function);：创建一个线程 t1，它运行 some_function。std::thread t2 = std::move(t1);：将 t1 的线程所有权转移给 t2。此时，t1 不再管理任何线程。重新赋值线程t1 = std::thread(some_other_function);：将 t1 重新赋值为一个新线程，运行 some_other_function。空线程对象std::thread t3;：创建一个空的线程对象 t3。t3 = std::move(t2);：将 t2 的线程所有权转移给 t3。防止未管理线程t1 = std::move(t3);：将 t3 的线程所有权转移回 t1。注意：如果 t1 在转移之前已经管理了一个线程，而该线程未被显式 join() 或 detach()，则会导致程序异常终止（调用 std::terminate()）。注意事项线程对象的生命周期在 std::thread 的生命周期结束时，必须确保线程已被 join() 或 detach()，否则程序会调用 std::terminate()。如果需要转移所有权，务必在转移前处理旧的线程对象。std::move 的使用转移所有权时，必须使用 std::move()，因为线程对象是不可复制的。线程对象的空状态转移所有权后，原线程对象进入“空状态”（即不再管理任何线程）。可以通过调用 joinable() 检查线程对象是否仍然管理线程。函数中转移线程所有权线程所有权的转移也可以通过函数返回值实现。例如：std::thread create_thread() { void some_function(); return std::thread(some_function); // 返回一个线程对象，调用者接管所有权 } void use_thread() { std::thread t = create_thread(); // 接管线程所有权 t.join(); // 确保线程完成 }在这个例子中，create_thread() 创建一个线程并将其返回，调用者负责管理线程的生命周期。当所有权可以在函数内部传递，就允许std::thread实例作为参数进行传递，代码如下：void f(std::thread t); void g() { void some_function(); f(std::thread(some_function)); std::thread t(some_function); f(std::move(t)); }封装线程所有权可以通过封装来简化线程所有权的管理，例如使用 scoped_thread：class scoped_thread { std::thread t; public: explicit scoped_thread(std::thread t_) : t(std::move(t_)) { if (!t.joinable()) { throw std::logic_error("No thread"); } } ~scoped_thread() { t.join(); // 确保线程完成 } scoped_thread(scoped_thread const&) = delete; scoped_thread& operator=(scoped_thread const&) = delete; };此类会在析构时自动调用 join()，确保线程被正确管理。量产线程void do_work(unsigned id); void f() { std::vector<std::thread> threads; for (unsigned i = 0; i < 20; ++i) { threads.emplace_back(do_work,i); // 产生线程 } for (auto& entry : threads) // 对每个线程调用 join() entry.join(); }我们有时需要线程去分割一个算法的工作总量，所以在算法结束的之前，所有的线程必须结束。代码2.8中线程所做的工作都是独立的，并且结果仅会受到共享数据的影响。如果f()有返回值，这个返回值就依赖于线程得到的结果。写入返回值之前，程序会检查使用共享数据的线程是否终止。结果在不同线程中转移的方案，会在第4章中再次讨论。将std::thread放入std::vector是向线程自动化管理迈出的第一步：并非为这些线程创建独立的变量，而是把它们当做一个组。创建一组线程(数量在运行时确定)，而非代码2.8那样创建固定数量的线程。总结C++ 中的 std::thread 是 不可复制 的，但可以通过 std::move 转移所有权。转移线程所有权时，确保线程对象未管理线程，或已正确处理（join() 或 detach()）。使用封装类（如 scoped_thread）可以简化线程管理，避免资源泄漏。[![top] Goto Top](#table-of-contents)2.4 确定线程数量这个例子主要是实现一个并行版本的std::accumulate，通过多线程加速累加操作。背景知识std::accumulate：是标准库中的一个函数，用于对一个范围内的元素进行累加。它是串行的（单线程），所以在处理大量数据时性能会受到限制。并行化的目标：将数据分成多个块，每个线程处理一部分数据，最后汇总所有线程的结果。通过并行化，充分利用多核 CPU 的性能。std::accumulate 是 C++ 标准库 <numeric> 头文件中提供的一个函数模板，用于对范围内的元素进行累积操作（即求和、求积或其他自定义操作）。它是一个非常灵活且强大的工具。基本语法#include <numeric> template<class InputIterator, class T> T accumulate(InputIterator first, InputIterator last, T init); template<class InputIterator, class T, class BinaryOperation> T accumulate(InputIterator first, InputIterator last, T init, BinaryOperation op);参数说明first 和 last:表示输入范围的起始迭代器和结束迭代器，范围是 [first, last)，即包含 first 指向的元素，但不包含 last。init:累积操作的初始值，累积操作从这个值开始。op（可选）:自定义的二元操作函数或函数对象。默认情况下是加法操作。返回值返回累积操作的结果，类型与 init 相同。使用示例1. 简单求和默认情况下，std::accumulate 使用加法操作：#include <iostream> #include <numeric> #include <vector> int main() { std::vector<int> vec = {1, 2, 3, 4, 5}; int sum = std::accumulate(vec.begin(), vec.end(), 0); // 初始值为 0 std::cout << "Sum: " << sum << std::endl; // 输出 15 return 0; }2. 求积通过自定义操作符可以实现其他累积操作，例如求积：#include <iostream> #include <numeric> #include <vector> int main() { std::vector<int> vec = {1, 2, 3, 4, 5}; int product = std::accumulate(vec.begin(), vec.end(), 1, std::multiplies<int>()); std::cout << "Product: " << product << std::endl; // 输出 120 return 0; }3. 字符串连接std::accumulate 不仅可以用于数值，还可以用于字符串等其他类型：#include <iostream> #include <numeric> #include <vector> #include <string> int main() { std::vector<std::string> words = {"Hello", " ", "World", "!"}; std::string result = std::accumulate(words.begin(), words.end(), std::string("")); std::cout << "Result: " << result << std::endl; // 输出 "Hello World!" return 0; }4. 自定义操作可以使用自定义的二元函数：#include <iostream> #include <numeric> #include <vector> int main() { std::vector<int> vec = {1, 2, 3, 4, 5}; int custom = std::accumulate(vec.begin(), vec.end(), 0, [](int a, int b) { return a + b * 2; // 每个元素乘以 2 后再累加 }); std::cout << "Custom result: " << custom << std::endl; // 输出 30 return 0; }2. 代码结构解析完整代码如下：template<typename Iterator,typename T> struct accumulate_block { void operator()(Iterator first,Iterator last,T& result) { result=std::accumulate(first,last,result); } }; template<typename Iterator,typename T> T parallel_accumulate(Iterator first,Iterator last,T init) { unsigned long const length=std::distance(first,last); if(!length) // 1 return init; unsigned long const min_per_thread=25; unsigned long const max_threads= (length+min_per_thread-1)/min_per_thread; // 2 unsigned long const hardware_threads= std::thread::hardware_concurrency(); unsigned long const num_threads= // 3 std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads); unsigned long const block_size=length/num_threads; // 4 std::vector<T> results(num_threads); std::vector<std::thread> threads(num_threads-1); // 5 Iterator block_start=first; for(unsigned long i=0; i < (num_threads-1); ++i) { Iterator block_end=block_start; std::advance(block_end,block_size); // 6 threads[i]=std::thread( // 7 accumulate_block<Iterator,T>(), block_start,block_end,std::ref(results[i])); block_start=block_end; // 8 } accumulate_block<Iterator,T>()( block_start,last,results[num_threads-1]); // 9 for (auto& entry : threads) entry.join(); // 10 return std::accumulate(results.begin(),results.end(),init); // 11 }以下是代码的核心部分及其功能解释：(1) accumulate_block 结构体template<typename Iterator, typename T> struct accumulate_block { void operator()(Iterator first, Iterator last, T& result) { result = std::accumulate(first, last, result); } };功能：这是一个可调用对象（仿函数），用于对指定范围的元素进行累加。参数：first 和 last：范围的起始和结束迭代器。result：存储累加结果的变量。实现：调用标准库的 std::accumulate，对范围 [first, last) 内的元素求和，并将结果存储到 result 中。(2) parallel_accumulate 函数这是整个并行化逻辑的核心部分。a. 输入检查unsigned long const length = std::distance(first, last); if (!length) // 如果范围为空 return init;功能：检查输入范围是否为空。如果为空，直接返回初始值 init。b. 计算线程数unsigned long const min_per_thread = 25; unsigned long const max_threads = (length + min_per_thread - 1) / min_per_thread; unsigned long const hardware_threads = std::thread::hardware_concurrency(); unsigned long const num_threads = std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);目的：确定需要启动的线程数。逻辑：min_per_thread：每个线程处理的最小任务数，避免创建太多线程。max_threads：根据数据量和 min_per_thread 计算出理论上的最大线程数。hardware_threads：获取硬件支持的并发线程数。如果无法获取（返回值为 0），默认使用 2。num_threads：取硬件线程数和 max_threads 的较小值，确保线程数合理。在这段代码中，使用 unsigned long 而不是 int 类型的原因主要与以下几个方面有关：避免负值unsigned long 是无符号类型，而 int 是有符号类型。在这段代码中，min_per_thread 和 max_threads 都是表示线程数量或长度的值，这些值本质上是非负的。如果使用 int，可能会引入负数的风险（例如，由于计算错误或溢出），而这些值本身不应该为负数。使用 unsigned long 可以明确表达这些值只能是非负的，避免负值带来的逻辑问题。防止溢出unsigned long 通常比 int 能表示更大的范围（具体范围取决于平台和编译器）。例如：在 32 位系统上，int 通常是 32 位，范围为 -2,147,483,648 到 2,147,483,647。而 unsigned long 通常是 32 位或更大，范围为 0 到 4,294,967,295（或更大）。如果 length 是一个非常大的值（例如表示一个超长数组的长度），使用 int 可能会导致溢出，而 unsigned long 能更安全地处理大范围的值。与硬件线程数量的兼容性std::thread::hardware_concurrency() 返回值的类型是 unsigned int，表示硬件支持的并发线程数。为了兼容性，代码中使用了一个更宽泛的类型 unsigned long，以确保在不同平台上不会因为类型不匹配而导致问题。表达意图使用 unsigned long 明确表达了这些值是非负的，并且可能需要处理较大的范围。这是一种编码风格上的选择，旨在提高代码的可读性和可维护性。性能影响在大多数情况下，unsigned long 和 int 的性能差异可以忽略不计。选择 unsigned long 是为了更好地适应可能的大范围值，而不是因为性能原因。c. 划分任务块unsigned long const block_size = length / num_threads; std::vector<T> results(num_threads); std::vector<std::thread> threads(num_threads - 1); Iterator block_start = first; for (unsigned long i = 0; i < (num_threads - 1); ++i) { Iterator block_end = block_start; std::advance(block_end, block_size); threads[i] = std::thread( accumulate_block<Iterator, T>(), block_start, block_end, std::ref(results[i])); block_start = block_end; }核心逻辑：每个线程处理的任务块大小：block_size = length / num_threads。存储中间结果：用 std::vector<T> results 存储每个线程的计算结果。存储线程对象：用 std::vector<std::thread> threads 保存所有线程。任务分配：使用 std::advance 将迭代器 block_start 移动到当前块的末尾。创建线程，调用 accumulate_block 对当前块进行累加。将累加结果存储到 results[i] 中。d. 处理最后一个任务块accumulate_block<Iterator, T>()( block_start, last, results[num_threads - 1]);功能：主线程处理最后一个任务块。原因：线程数比任务块数少 1，因此最后一个任务块由主线程完成。e. 等待线程完成for (auto& entry : threads) entry.join();功能：等待所有线程完成任务（调用 join）。f. 汇总结果return std::accumulate(results.begin(), results.end(), init);功能：对 results 中的中间结果进行累加，得到最终结果。3. 总结代码流程检查输入范围是否为空。根据数据量和硬件支持，计算需要启动的线程数。将数据划分为多个任务块。创建线程并分配任务块，计算中间结果。主线程处理最后一个任务块。等待所有线程完成。汇总中间结果，返回最终结果。4. 注意事项线程数的选择：线程数过多会导致上下文切换开销，性能反而下降。线程数过少可能无法充分利用多核 CPU 的性能。结果的准确性：如果 T 是浮点类型（如 float 或 double），并行化可能导致结果与串行版本不同（因为加法不满足结合律）。迭代器要求：必须是前向迭代器（如 std::vector 的迭代器）。5. 扩展在 C++17 中，可以直接使用 std::reduce 实现并行累加，它是 std::accumulate 的并行版本，避免了手动管理线程的复杂性。[![top] Goto Top](#table-of-contents)2.5 线程标识1. 什么是线程标识？线程标识是用来唯一标识一个线程的对象，其类型为std::thread::id。它是C++标准线程库中用于区分不同线程的工具。线程标识的类型：std::thread::id。默认值：如果线程标识没有关联到任何线程，则它会有一个默认构造值，表示“无线程”。2. 获取线程标识的两种方式通过std::thread对象获取：调用std::thread对象的成员函数get_id()可以获取该线程的标识。如果该std::thread对象没有关联到任何线程，get_id()将返回默认的“无线程”值。std::thread t([]{ /*线程任务*/ }); std::cout << t.get_id(); // 输出线程t的ID t.join();通过当前线程获取：调用std::this_thread::get_id()可以获取当前线程的标识。这个函数定义在<thread>头文件中。std::cout << std::this_thread::get_id(); // 输出当前线程的ID3. 线程标识的特性可复制和比较：std::thread::id对象可以自由地拷贝和比较。如果两个std::thread::id对象相等，则它们要么代表同一个线程，要么都表示“无线程”。如果不相等，则说明它们是不同的线程，或者一个有线程，一个没有线程。排序和哈希支持：std::thread::id支持比较操作（如<、>），这意味着可以作为排序容器的键值。标准库还提供了std::hash<std::thread::id>，因此可以将std::thread::id作为无序容器（如std::unordered_map）的键值。4. 使用场景4.1 检测线程是否需要执行某些操作在多线程程序中，可以通过线程标识来区分主线程和其他线程，从而决定是否执行特定操作。例如：std::thread::id master_thread; void some_core_part_of_algorithm() { if (std::this_thread::get_id() == master_thread) { do_master_thread_work(); // 主线程的特殊工作 } do_common_work(); // 所有线程的通用工作 }4.2 线程信息存储线程标识可以用作容器的键值，用来存储线程相关的信息。例如：用std::thread::id作为键值存储线程的状态。在多线程间传递信息时，用线程标识来区分不同的线程。4.3 输出线程标识可以通过std::cout输出线程标识的值，用于调试或日志记录：std::cout << std::this_thread::get_id();注意：线程标识的输出结果依赖于具体的实现，C++标准只要求相同的线程必须有相同的输出。5. 实际应用线程区分：在多线程程序中区分主线程和工作线程。线程数据管理：用线程标识作为键值，存储线程相关的数据或状态。调试和日志记录：通过线程标识帮助定位问题。总结std::thread::id是C++中用于标识线程的工具。可以通过std::thread::get_id()或std::this_thread::get_id()获取。具有可比较、可排序、支持哈希等特性。常用于线程区分、数据存储和调试。[![top] Goto Top](#table-of-contents)
《Effective STL》 Chapter 1: Containers（7-12）
50 Specific Ways to Improve Your Use of the Standard Template Library.《Effective STL》 Chapter 1: Containers（1-6）Item 7: 在使用存储new出来的指针的容器时，记得在容器销毁之前delete这些指针When using containers of newed pointers, remember to delete the pointers before the container is destroyed.STL中的容器非常智能。它们可以提供用于正向和反向遍历的迭代器（通过begin、end、rbegin等函数）；它们可以告诉你容器中存储了什么类型的对象（通过它们的value_type typedef）；在插入和删除元素时，它们会自动处理必要的内存管理；它们可以报告容器中当前有多少元素以及最多可以容纳多少元素（分别通过size和max_size）；当然，当容器本身被销毁时，它们会自动销毁它们所包含的每个对象。考虑到这些智能容器，许多程序员就不再为清理资源而担忧了。他们会想：“嘿，我的容器会替我处理这些问题。” 在很多情况下，他们是对的，但当容器中存储的是通过new动态分配的指针时，他们的想法就不够正确了。是的，指针容器会在它们（容器）被销毁时销毁它们所包含的每个元素，但指针的“析构函数”什么都不会做！它当然不会调用delete。因此，以下代码会导致资源泄漏：void doSomething() { vector<Widget*> vwp; for (int i = 0; i < SOME_MAGIC_NUMBER; ++i) vwp.push_back(new Widget); ... // 使用vwp } // 在这里，Widget对象会泄漏！当vwp超出作用域时，它的每个元素都会被销毁，但这并不能改变这样一个事实：new出来的对象从未被delete。这种删除操作是你的责任，而不是容器的责任。这其实是特意设计的功能。只有你知道这些指针是否需要被删除。通常情况下，你希望它们被删除。当确实需要删除时，实现这一点看起来似乎很简单：void doSomething() { vector<Widget*> vwp; ... // 如前所述 for (vector<Widget*>::iterator i = vwp.begin(); i != vwp.end(); ++i) delete *i; }这段代码是可以工作的，但前提是你对“工作”并没有太高的要求。这里有两个问题。一个问题是，这个新的for循环实际上与for_each的功能几乎相同，但它并不像直接使用for_each那么清晰（参见第43条）。另一个问题是，这段代码并不是异常安全的。如果在填充vwp的指针到删除它们之间抛出了异常，资源仍然会泄漏。幸运的是，这两个问题都可以解决。要将你的类似for_each的循环变成真正的for_each调用，你需要将delete转化为一个函数对象。这是小菜一碟，假设你有一个喜欢玩STL的孩子：template<typename T> struct DeleteObject : public unary_function<const T*, void> { void operator()(const T* ptr) const { delete ptr; } };现在你可以这样写：void doSomething() { ... // 如前所述 for_each(vwp.begin(), vwp.end(), DeleteObject<Widget>()); }不幸的是，这样做需要你显式地指定DeleteObject将删除的对象的类型（在这里是Widget）。这很烦人。vwp已经是一个vector<Widget*>，所以显然DeleteObject会删除Widget*指针！这显得很多余。这种冗余不仅令人厌烦，还可能导致难以追踪的错误。例如，假设有人不明智地决定从string继承：class SpecialString : public string { ... };从一开始这就是危险的，因为string（和所有标准STL容器一样）没有虚析构函数，而从没有虚析构函数的类继承是C++中的一个重大禁忌。（详细信息可以参考任何一本好的C++书。在《Effective C++》中，可以参考第14条。）尽管如此，有些人仍然会这样做，所以让我们看看以下代码会如何表现：void doSomething() { deque<SpecialString*> dssp; ... for_each(dssp.begin(), dssp.end(), DeleteObject<string>()); // 未定义行为！通过基类指针删除派生对象， // 而基类没有虚析构函数。 }注意，dssp被声明为存储SpecialString*指针，但for_each循环的作者告诉DeleteObject它将删除string*指针。可以理解的是，这样的错误很容易发生。SpecialString无疑与string非常相似，因此可以原谅它的用户偶尔忘记他们使用的是SpecialString而不是string。我们可以通过让编译器推导出传递给DeleteObject::operator()的指针类型来消除错误（以及减少DeleteObject的用户需要敲击的键数）。我们所需要做的就是将模板化从DeleteObject移到它的operator()中：struct DeleteObject { template<typename T> void operator()(const T* ptr) const { delete ptr; } };编译器知道传递给DeleteObject::operator()的指针的类型，因此我们让它们自动实例化一个接受该类型指针的operator()。这种类型推导的缺点是我们放弃了让DeleteObject适配器化的能力（参见第40条）。考虑到DeleteObject的设计用途，很难想象这会成为一个问题。使用这个新的DeleteObject版本，SpecialString用户的代码看起来像这样：void doSomething() { deque<SpecialString*> dssp; ... for_each(dssp.begin(), dssp.end(), DeleteObject()); // 啊！行为是定义的！ }简单明了，类型安全，就像我们希望的那样。但仍然不是异常安全的。如果在SpecialString对象被new出来后但在调用for_each之前抛出了异常，就会发生资源泄漏。这个问题可以通过多种方式解决，但最简单的可能是用智能指针替换指针容器，通常是引用计数指针。（如果你不了解智能指针的概念，你应该能在任何一本中高级C++书中找到相关描述。在《More Effective C++》中，相关内容在第28条。）STL本身不包含引用计数智能指针，编写一个好的智能指针（一个始终正确工作的智能指针）是很棘手的，你不想在没有必要的情况下自己去写一个。幸运的是，经过验证的实现并不难找到。一个这样的智能指针是Boost库中的shared_ptr（参见第50条）。使用Boost的shared_ptr，本条开头的例子可以重写成如下形式：void doSomething() { typedef boost::shared_ptr<Widget> SPW; // SPW = “shared_ptr to Widget” vector<SPW> vwp; for (int i = 0; i < SOME_MAGIC_NUMBER; ++i) vwp.push_back(SPW(new Widget)); // 创建一个SPW对象并将其push_back到vwp中。 ... // 使用vwp } // 在这里没有Widget泄漏，即使上面的代码抛出了异常。有一件事你绝对不能做，那就是认为可以通过创建auto_ptr的容器来自动删除指针。这是一个糟糕的想法，如此危险，以至于我专门用第8条来解释为什么要避免它。你真正需要记住的是，STL容器虽然智能，但它们还不足以知道是否应该删除它们包含的指针。为了避免资源泄漏，当你使用包含需要删除的指针的容器时，你必须要么用智能引用计数指针对象（如Boost的shared_ptr）替换这些指针，要么在容器销毁之前手动删除每个指针。最后，你可能会想到，如果像DeleteObject这样的结构可以让包含对象指针的容器更容易避免资源泄漏，那么类似的DeleteArray结构也可以让包含数组指针的容器更容易避免资源泄漏。当然这是可能的，但是否值得建议则是另一回事。第13条解释了为什么动态分配的数组几乎总是劣于vector和string对象，因此在你坐下来编写DeleteArray之前，请先阅读第13条。希望你能决定DeleteArray是一个永远不需要实现的结构。
《Effective STL》 Chapter 1: Containers（1-6）
50 Specific Ways to Improve Your Use of the Standard Template Library.本文作为读书的记录，后续会陆续更新。Item 1: 谨慎选择你的容器Choose your containers with care您知道 C++ 提供了多种容器供您使用，但您是否意识到这些容器种类繁多？为了确保您没有忽略任何选项，下面简要回顾一下。标准 STL 序列容器、vector、string、deque 和 list。标准 STL 关联容器，set、multiset、map 和 multimap。非标准序列容器 slist 和 rope。slist 是单链表，而 rope 本质上是重载string。（“rope” 是重载“string”。明白了吗？）您可以在 Item 50 中找到这些非标准（但常用）容器的简要概述。非标准关联容器 hash_set、hash_multiset、hash_map 和 hash_multimap。我在 Item 25 中研究了这些广泛使用的基于哈希表的标准关联容器变体。vector<char> 替代了 string。第 13 条描述了这种替代在哪些条件下是合理的。vector 可以替代标准的关联容器。正如 Item 23 所明确指出的，有时 vector 在时间和空间上的表现都优于标准的关联容器。几个标准的非 STL 容器，包括arrays、bitset、valarray、stack、queue,和priority_queue。由于这些都是非 STL 容器，因此本书中我很少谈论它们，不过第 16 条提到了数组优于 STL 容器的情况，第 18 条解释了为什么位集可能比 vector<bool> 更好。还值得一提的是，arrays可以与 STL 算法一起使用，因为指针可以用作array迭代器。关于容器的选项非常丰富，选择时需要考虑的因素也非常多。遗憾的是，大多数关于 STL 的讨论都对容器世界持相当狭隘的看法，忽略了许多与选择最合适的容器相关的问题。甚至STL也参与其中，为在 vector、deque 和 list 之间进行选择提供了以下指导：vector、list 和 deque 为程序员提供了不同的复杂性权衡，应相应地使用它们。vector 是默认应使用的序列类型。当在序列中间频繁插入和删除时，应使用 list。当大多数插入和删除发生在序列的开头或结尾时，deque 是首选的数据结构。如果您主要关心的是算法复杂性，我想这是一个合理的建议，但还有很多值得关注的事情。首先我需要介绍一种对 STL 容器进行分类的方法，这种方法讨论得并不多。这就是连续内存容器和基于节点的容器之间的区别。连续内存容器（也称为基于数组的容器）将其元素存储在一个或多个（动态分配的）内存块中，每个内存块包含多个容器元素。如果插入新元素或删除现有元素，则同一内存块中的其他元素必须上移或下移，以便为新元素腾出空间或填充以前被删除元素占用的空间。这种移动会影响性能（参见第 5 和 14 条）和异常安全性（我们很快就会看到）。标准的连续内存容器是 vector、string 和 deque。非标准 rope也是连续内存容器。基于节点的容器每块（动态分配的）内存仅存储一个元素。插入或删除容器元素只会影响指向节点的指针，而不会影响节点本身的内容，因此插入或删除某些内容时无需移动元素值。表示链接列表的容器（如 list和slist）是基于节点的，所有标准关联容器也是如此。（它们通常实现为平衡树。）非标准哈希容器使用不同的基于节点的实现，如您将在条目 25 中看到的那样。您是否需要能够在容器中的任意位置插入新元素？如果是，则需要一个序列容器；关联容器不行。您是否关心元素在容器中的排序方式？如果不关心，散列容器将是一个可行的选择。否则，您将希望避免使用散列容器。容器必须是标准 C++ 的一部分吗？如果是，那就排除了散列容器、slist 和rope。你需要哪一类迭代器？如果它们必须是随机访问迭代器，那么从技术上讲，你只能使用 vector、deque 和 string，但你也许还想考虑 rope。有关rope的信息，请参阅。）如果需要双向迭代器，你必须避免使用 slist（参见 Item 50）以及散列容器的一种常见实现（参见 Item 25）。在进行插入或删除时，避免移动现有的容器元素是否重要？如果重要，你需要远离连续内存容器（参见条款 5）。在插入或删除时避免移动现有容器元素是否重要？如果重要，则需要远离连续内存容器（参见条目 5）。容器中的数据是否需要与 C 兼容布局？如果重要，则只能使用vector（参见条目 16）。查找速度是否是关键考虑因素？如果重要，则需要查看散列容器（参见条目 25）、排序兼容布局？如果重要，则只能使用vector（参见条目（参见条目 23）和标准关联容器 — 可能按此顺序。您是否介意底层容器使用引用计数？如果重要，则需要避开 string，因为许多字符串实现都是引用计数的（参见条目 13）。还需要避免使用 rope，因为最终的 rope 实现基于引用计数（参见条目 50）。当然，您必须以某种方式表示字符串，因此需要考虑 vector<char>。插入和擦除是否需要事务语义？也就是说，您是否需要能够可靠地回滚插入和擦除？如果是，您将需要使用基于节点的容器。如果您需要多元素插入的事务语义（例如，范围形式 - 参见第 5 条），您将需要选择list,，因为list,是唯一提供多元素插入事务语义的标准容器。事务语义对于有兴趣编写异常安全代码的程序员来说尤其重要。（事务语义也可以通过连续内存容器实现，但会降低性能，并且代码不那么简单。要了解更多信息，请参阅 Sutter 的《Exceptional C++》第 17 条 [8]。）您是否需要最小化迭代器、指针和引用无效？如果是这样，您将需要使用基于节点的容器，因为在此类容器上插入和擦除永远不会使迭代器、指针或引用无效（除非它们指向您要擦除的元素）。通常，在连续内存容器上插入或擦除可能会使容器中的所有迭代器、指针和引用无效。您是否关心在容器上使用 swap是否会使迭代器、指针或引用无效？如果是这样，您需要避免使用 string，因为在 STL 中，string是唯一在交换期间使迭代器、指针和引用无效的。如果有一个带有随机访问迭代器的序列容器，其中只要不删除任何内容并且只在容器的末尾进行插入，指针和对数据的引用就不会失效，这会很有帮助吗？这是一种非常特殊的情况，但如果这是你的情况，deque就是你梦寐以求的容器。（有趣的是，当只在容器的末尾进行插入时，deque的迭代器可能会失效。deque是唯一一个迭代器可能会失效而不会使其指针和引用失效的标准 STL 容器。）这些问题远非问题的结束。例如，它们没有考虑不同容器类型所采用的不同内存分配策略。（第 10 和 14 条讨论了此类策略的某些方面。）尽管如此，它们应该足以说服您，除非您对元素排序、标准一致性、迭代器功能、与 C 的布局兼容性、查找速度、由于引用计数导致的行为异常、实现事务语义的难易程度或迭代器无效的条件不感兴趣，否则您要考虑的不仅仅是容器操作的算法复杂性。当然，这种复杂性很重要，但它远非全部。STL 在容器方面为您提供了许多选项。如果您超越 STL 的范围，还会有更多的选择。在选择容器之前，请务必考虑所有选项。“默认容器”？我不这么认为。Item 2: 警惕容器无关代码的幻觉Beware the illusion of container-independent codeSTL（标准模板库）的基础是泛化。数组被泛化为容器，并通过模板参数化为存储特定类型对象的容器。函数被泛化为算法，并通过模板参数化为使用特定类型迭代器的算法。指针被泛化为迭代器，并通过模板参数化为指向特定类型对象的迭代器。这仅仅是个开始。单个容器类型被泛化为序列容器和关联容器，而相似的容器则提供相似的功能。标准的连续内存容器（如vector、deque和string）提供随机访问迭代器，而标准的节点型容器（如list和关联容器）提供双向迭代器。序列容器支持push_front和/或push_back，但关联容器不支持。关联容器提供对数时间复杂度的lower_bound、upper_bound和equal_range成员函数，而序列容器则不提供这些函数。在如此多的泛化之下，想要加入这种趋势是很自然的。这种想法值得赞扬，当你编写自己的容器、迭代器和算法时，你当然应该追求这种泛化。然而，许多程序员试图以另一种方式追求泛化。他们试图避免在代码中绑定到特定类型的容器，而是试图泛化“容器”的概念，以便可以用vector，也可以用deque或list，而无需修改使用它们的代码。换句话说，他们试图编写“容器无关”的代码。这种泛化的愿望虽然出发点是好的，但几乎总是误入歧途。即使是最热衷于容器无关代码的支持者也很快会意识到，试图编写同时适用于序列容器和关联容器的代码是没有意义的。许多成员函数仅存在于某一类容器中，例如，只有序列容器支持push_front或push_back，而只有关联容器支持count和lower_bound等。甚至像insert和erase这样基本的操作，其签名和语义也因容器类别不同而有所不同。例如，将对象插入序列容器时，它会保留在插入的位置，而插入关联容器时，容器会根据排序规则将对象移动到适当的位置。再比如，针对迭代器的erase在序列容器中会返回一个新的迭代器，而在关联容器中则什么也不返回。假设你希望编写可同时用于最常见序列容器（vector、deque和list）的代码。显然，你必须限制自己仅使用它们的共同功能。这意味着不能使用reserve或capacity（因为deque和list不支持它们）。由于list的存在，你也不能使用operator[]，并且必须限制自己使用双向迭代器的功能。这进一步意味着你不能使用需要随机访问迭代器的算法，例如sort、stable_sort、partial_sort和nth_element。另一方面，由于vector的存在，你不能使用push_front和pop_front，而vector和deque的存在则使你无法使用splice和成员形式的sort。结合上述限制，这意味着你无法调用任何形式的sort来对“泛化的序列容器”进行排序。这些是显而易见的限制。如果你违反了这些限制，你的代码至少会在某些容器上无法通过编译。而能够通过编译的代码则更为棘手。主要的问题在于不同序列容器对迭代器、指针和引用的失效规则不同。为了编写能够正确运行在vector、deque和list上的代码，你必须假设任何会使这些容器中的迭代器、指针或引用失效的操作都会使你使用的容器中的它们失效。因此，你必须假设每次调用insert都会使所有内容失效，因为deque::insert会使所有迭代器失效，而由于无法调用capacity，vector::insert必须被假定为会使所有指针和引用失效。类似的推理表明，除非你正在删除容器中的最后一个元素，否则你也必须假设调用erase会使所有内容失效。更糟糕的是，你不能将容器中的数据传递给C接口，因为只有vector支持这一点（详见第16条）。你不能将bool作为存储类型实例化你的容器，因为正如第18条所解释的，vector<bool>并不总是像vector，并且它实际上并不存储bool。你也不能假设list的插入和删除操作是常数时间的，因为vector和deque在执行这些操作时需要线性时间。最终，你会发现自己创建了一个“泛化的序列容器”，它不能调用reserve、capacity、operator[]、push_front、pop_front、splice或任何需要随机访问迭代器的算法；一个每次调用insert和erase都需要线性时间并使所有内容失效的容器；一个与C接口不兼容的容器；一个不能存储bool的容器。这真的是你希望在应用程序中使用的容器类型吗？我猜不是。如果你放弃对list的支持，你仍然会失去reserve、capacity、push_front和pop_front；你仍然必须假设所有对insert和erase的调用需要线性时间并使所有内容失效；你仍然失去了与C的内存布局兼容性；你仍然不能存储bool。如果你转而尝试编写可以与不同关联容器一起工作的代码，情况也不会好多少。试图同时支持set和map几乎是不可能的，因为set存储单一对象，而map存储对象对。即使是同时支持set和multiset（或map和multimap）也很困难。以值为参数的insert成员函数在set/map和它们的多重版本中具有不同的返回类型，并且你必须非常小心地避免假设容器中存储的值的数量。面对现实吧：这不值得。不同的容器有显著不同的优缺点。它们并不是为了可以互换而设计的，你也几乎无法掩盖它们之间的差异。如果你尝试这样做，你只是在诱惑命运，而命运并不喜欢被诱惑。当然，总有一天你会发现自己选择的容器类型并不理想，需要更换为另一种类型。你现在知道，当你更换容器类型时，不仅需要修复编译器诊断出的问题，还需要检查所有使用容器的代码，以了解在新容器的性能特性和迭代器/指针/引用失效规则下需要进行哪些更改。如果你从vector切换到其他容器，你还需要确保不再依赖于vector的C兼容内存布局；如果你切换到vector，你需要确保没有使用它来存储bool。考虑到更换容器类型的不可避免性，你可以通过封装来简化这种更改。这是传统的解决方案：封装、封装、封装。最简单的方法之一是通过广泛使用容器类型的typedef进行封装。因此，与其写成这样：class Widget { ... }; vector<Widget> vw; Widget bestWidget; ... // 给bestWidget赋值 vector<Widget>::iterator i = find(vw.begin(), vw.end(), bestWidget);不如写成这样：class Widget { ... }; typedef vector<Widget> WidgetContainer; WidgetContainer cw; Widget bestWidget; ... WidgetContainer::iterator i = find(cw.begin(), cw.end(), bestWidget);这使得更改容器类型变得容易得多，尤其是在更改仅仅是添加自定义分配器时（这种更改不会影响迭代器/指针/引用失效规则）：class Widget { ... }; template<typename T> SpecialAllocator { ... }; // 第10条解释了为什么需要模板 typedef vector<Widget, SpecialAllocator<Widget>> WidgetContainer; WidgetContainer cw; Widget bestWidget; ... WidgetContainer::iterator i = find(cw.begin(), cw.end(), bestWidget);如果typedef的封装特性对你来说无关紧要，你仍然可能会欣赏它节省的工作量，尤其是对于迭代器类型。例如，如果你有一个类型为map<string, vector<Widget>::iterator, CIStringCompare>的对象，并希望使用const_iterator遍历该map，你真的愿意多次拼写出map<string, vector<Widget>::iterator, CIStringCompare>::const_iterator吗？一旦你使用STL一段时间，你就会意识到typedef是你的朋友。typedef只是某种类型的同义词，因此它提供的封装仅限于词法层面。typedef不会阻止客户端做任何他们本来就可以做的事情（或依赖的行为）。如果你想限制客户端对你选择的容器的依赖，你需要更强大的工具：类。为了限制在更换容器类型时可能需要修改的代码量，可以将容器隐藏在类中，并通过类接口限制暴露的容器特定信息。例如，如果你需要创建一个客户列表，不要直接使用list。相反，创建一个CustomerList类，并将list隐藏在其私有部分：class CustomerList { private: typedef list<Customer> CustomerContainer; typedef CustomerContainer::iterator CCIterator; CustomerContainer customers; public: ... // 限制通过接口暴露的list特定信息 };起初，这可能显得有些多余。毕竟，客户列表就是一个列表，对吧？也许吧。后来你可能会发现，你并不需要像预期的那样频繁地在列表中间插入或删除客户，但你确实需要快速识别出前20%的客户——这是一个非常适合使用nth_element算法的任务（见第31条）。但nth_element需要随机访问迭代器，它无法与list一起使用。在这种情况下，你的客户“列表”可能更适合用vector或deque实现。当你考虑这种更改时，你仍然必须检查CustomerList的每个成员函数和每个友元，以了解它们将如何受到影响（例如性能和迭代器/指针/引用失效规则等方面），但如果你在封装CustomerList的实现细节方面做得很好，对CustomerList客户端的影响应该很小。你无法编写容器无关的代码，但他们可能可以。Item 3: 确保容器中对象的拷贝操作既高效又正确Make copying cheap and correct for objects in containersSTL 容器存储对象，但并不是你直接放进去的那些对象。当你将一个对象添加到容器中（例如通过 insert 或 push_back 等操作），实际存储在容器中的，是你指定对象的拷贝。一旦对象进入容器，它可能会被进一步拷贝。例如，如果你向 vector、string 或 deque 中插入或移除元素，现有的容器元素通常会被移动（也就是被拷贝）。如果你使用排序算法（如 sort，参见第31小节）；或者使用 next_permutation、remove、unique 等算法（参见第32小节），对象也会被移动（拷贝）。是的，拷贝对象是 STL 的常见操作。了解这些拷贝操作的实现方式很重要。实际上，拷贝是通过调用对象的拷贝构造函数和拷贝赋值运算符来完成的。例如，对于一个用户自定义的类 Widget，这些函数通常声明如下：class Widget { public: Widget(const Widget&); // 拷贝构造函数 Widget& operator=(const Widget&); // 拷贝赋值运算符 };如果你没有显式声明这些函数，编译器会自动为你生成默认版本。此外，对于内置类型（如 int、指针等），拷贝操作只是简单地复制底层的比特位。由于 STL 容器频繁地进行拷贝操作，因此本节的重点很明确：如果容器中存储的对象拷贝开销较大，那么仅仅将对象放入容器中就可能成为性能瓶颈。容器中的对象被频繁移动时，这种性能问题会更加严重。此外，如果对象的“拷贝”操作具有非传统的定义（例如，拷贝操作改变了对象的某些状态），将这些对象存入容器中通常会导致问题。在继承的场景下，拷贝操作还会引发切片问题（slicing problem）。如果你创建一个存储基类对象的容器，然后尝试将派生类对象插入其中，派生类对象的特性会在拷贝过程中丢失。例如：vector<Widget> vw; class SpecialWidget : public Widget { ... }; // SpecialWidget 继承自 Widget SpecialWidget sw; vw.push_back(sw); // sw 被拷贝为基类对象，派生类的特性被丢弃切片问题表明，将派生类对象插入基类对象的容器中几乎总是错误的。如果你希望对象保留派生类的行为（例如调用派生类的虚函数等），那么这种操作肯定是错误的。一种避免切片问题，同时使拷贝操作高效和正确的简单方法是：创建指针容器，而不是直接存储对象的容器。也就是说，与其创建 vector<Widget>，不如创建 vector<Widget*>。拷贝指针的开销很低，它总是按预期工作（即复制指针的比特位），并且不会发生切片问题。然而，指针容器有其自身的 STL 相关问题（参见第7小节和第33小节）。为了避免这些问题，同时解决效率、正确性和切片问题，你可能会发现智能指针容器是一个有吸引力的选择。有关智能指针容器的更多信息，请参见第7小节。如果你觉得 STL 对拷贝操作过于频繁，那就再想想。是的，STL 确实会进行许多拷贝操作，但它通常会尽量避免不必要的拷贝操作。例如，与 C 和 C++ 的内置容器（数组）相比，STL 容器在避免不必要的对象创建方面做得更好：Widget w[maxNumWidgets]; // 创建一个包含 maxNumWidgets 个 Widget 的数组，默认构造每个对象上面的代码会构造 maxNumWidgets 个 Widget 对象，即使你可能只会使用其中的一部分，或者你打算立即用其他值覆盖这些默认构造的对象。而使用 STL 容器，我们可以这样做：vector<Widget> vw; // 创建一个初始大小为 0 的 vector，它会根据需要扩展或者，我们可以创建一个空的 vector，但预留足够的空间来存储最多 maxNumWidgets 个对象，而无需构造任何对象：vector<Widget> vw; vw.reserve(maxNumWidgets); // 参见第14小节了解 reserve 的细节与数组相比，STL 容器更加智能。它们只会在需要时创建对象，并且只会在你要求时使用默认构造函数。是的，STL 容器会进行拷贝操作，但它们仍然比数组更高效、更灵活。Item 4: 调用empty而不是检查size()是否为零Call empty instead of checking size() against zero对于任何容器 c，写下：if (c.size() == 0) ...基本上等同于写：if (c.empty()) ...既然如此，你可能会好奇，为什么要优先选择使用empty的方式，特别是考虑到empty通常实现为一个内联函数，仅仅返回size是否为0。你应该优先选择使用empty，原因很简单：对于所有标准容器来说，empty是一个常量时间（constant-time）操作，而对于某些list（链表）实现来说，size可能需要线性时间（linear-time）。但是什么原因导致list表现得如此“棘手”？为什么它不能像其他容器一样提供常量时间的size？答案与list的范围形式（range form）的unique和splice函数密切相关。考虑以下代码：list<int> list1; list<int> list2; ... list1.splice( list1.end(), list2, find(list2.begin(), list2.end(), 5), find(list2.rbegin(), list2.rend(), 10).base() );这段代码的作用是将list2中从第一个值为5的位置到最后一个值为10的位置的所有节点移动到list1的末尾（假设list2中确实存在这样的值）。但关键问题是：在splice操作完成后，list1中有多少个元素？显然，在splice操作后，list1的元素数量等于原有数量加上从list2中移动过来的元素数量。但是，如何知道从list2中移动了多少个元素呢？唯一的方法是遍历这个范围并逐一计数。假设你正在实现一个list类，你希望它的性能尽可能高效。你知道，许多用户会频繁调用size来获取列表的大小，因此你希望size是一个常量时间操作。然而，你也知道list是唯一一个支持高效splice操作的标准容器，用户选择list的一个重要原因就是它的splice操作具有常量时间的性能。这就带来了两难的局面。如果size是一个常量时间操作，那么每次splice都必须更新列表的大小。然而，对于范围形式的splice，更新大小需要遍历范围并计数，这将破坏splice的常量时间性能。如果你希望splice保持常量时间性能，那么size就不得不成为一个线性时间操作，因为它需要遍历整个列表来计算元素数量。不同的list实现对此冲突采取了不同的解决方案，这取决于实现者更重视size的效率还是splice的效率。如果你使用的list实现优先保证splice的常量时间性能，那么调用empty会比检查size() == 0更高效，因为empty始终是一个常量时间操作。即使你当前使用的实现中size是常量时间操作，也可能在未来切换到另一个实现时遇到问题。无论你使用哪种实现，调用empty而不是检查size()是否为零总是不会出错。因此，每当你需要判断容器是否为空时，请优先调用empty。Item 5: 优先使用范围成员函数而非单元素成员函数Prefer range member functions to their single-element counterparts快速回答！给定两个向量 v1 和 v2，如何让 v1 的内容与 v2 的后半部分相同？不要纠结当 v2 有奇数个元素时“后半部分”如何定义，只需做一些合理的事情。时间到！如果你的答案是：v1.assign(v2.begin() + v2.size() / 2, v2.end());或者类似的内容，那么你得到了满分并且可以获得一颗金星。如果你的答案涉及多个语句，但没有使用任何形式的循环，那么你几乎得到了满分，但没有金星。如果你的答案涉及一个循环，那么你还有进步的空间；如果涉及多个循环，那么……我们只能说你真的需要这本书了。顺便说一句，如果你对答案的反应是“哈？”，请认真看下去，因为你将学到一些非常有用的内容。这个小测试旨在达到两个目的。首先，它让我有机会提醒你 assign 成员函数的存在，这是一个非常方便的工具，但很多程序员都忽略了它。assign 可用于所有标准序列容器（如 vector、string、deque 和 list）。每当你需要完全替换容器的内容时，都应该考虑使用 assign。如果你只是将一个容器复制到另一个相同类型的容器中，operator= 是首选的赋值函数，但正如本例所示，当 operator= 无法满足需求时，assign 是一个很好的选择。这个测试的第二个目的是展示为什么范围成员函数优于它们的单元素替代方案。范围成员函数是一种成员函数，它像 STL 算法一样，使用两个迭代器参数来指定需要操作的一段范围。如果不使用范围成员函数来解决本条开头的问题，你不得不写一个显式的循环，可能类似这样：vector<Widget> v1, v2; // 假设 v1 和 v2 是 Widget 类型的向量 ... v1.clear(); for (vector<Widget>::const_iterator ci = v2.begin() + v2.size() / 2; ci != v2.end(); ++ci) { v1.push_back(*ci); }第43条详细探讨了为什么应该尽量避免编写显式循环，但你不需要阅读那条也能看出，写这段代码比直接调用 assign 需要更多的工作。稍后我们会看到，这段循环还带来了性能上的额外开销，但我们稍后再讨论。避免显式循环的一种方法是遵循第43条的建议，使用算法代替：v1.clear(); copy(v2.begin() + v2.size() / 2, v2.end(), back_inserter(v1));尽管这段代码没有显式循环，但它内部确实存在一个循环（在 copy 算法中实现）。因此，性能上的问题仍然存在。我们稍后再讨论性能问题，现在先简要指出：几乎所有使用 copy 的场景中，如果目标范围是通过插入迭代器（如 inserter、back_inserter 或 front_inserter）指定的，那么它们都可以（也应该）被范围成员函数替代。例如，这里可以用 insert 的范围版本替换 copy：v1.insert(v1.end(), v2.begin() + v2.size() / 2, v2.end());这段代码比调用 copy 需要稍少的输入，但它也更直接地表达了发生的事情：数据被插入到 v1 中。调用 copy 也表达了这一点，但不够直接。它把重点放在了复制操作上，而这并不是这里的重点。这里的重点是 v1 正在增加新数据。insert 成员函数清楚地表达了这一点，而 copy 则掩盖了这一点。在 STL 中，复制操作是如此基础，以至于本书的第3条就是关于它的！太多的 STL 程序员过度使用了 copy，所以我刚才的建议值得重复：几乎所有使用 copy 的场景中，如果目标范围是通过插入迭代器指定的，都应该被范围成员函数替代。回到我们的 assign 示例，我们已经找到了两个优先使用范围成员函数而非单元素替代方案的理由：编写代码时通常更省力。范围成员函数往往会导致代码更加清晰和直接。简而言之，范围成员函数生成的代码更容易编写，也更容易理解。还有什么理由不喜欢呢？然而，有些人会将这些理由视为编程风格问题，而开发者总是喜欢就风格问题争论不休，就像他们喜欢争论哪个编辑器是“唯一正确的编辑器”一样。（好像还有什么疑问似的，当然是 Emacs！）如果我们能找到一个更为广泛认同的标准来证明范围成员函数优于单元素成员函数，那就更好了。对于标准序列容器，我们确实有这样的标准：效率。当处理标准序列容器时，使用单元素成员函数通常会对内存分配器施加更多的需求，复制对象的频率更高，或者执行冗余操作，而这些在使用范围成员函数时可以避免。例如，假设你想将一个整型数组的内容复制到一个vector的开头。（数据可能最初存储在数组中，因为它来自某个遗留的 C API。关于将 STL 容器与 C API 混合使用时出现的问题，请参见第16条。）使用向量的范围 insert 函数，这样做非常简单：int data[numValues]; // 假设 numValues 已在其他地方定义 vector<int> v; ... v.insert(v.begin(), data, data + numValues); // 将 data 中的整型插入到 v 的开头如果使用显式循环调用单元素 insert，代码可能看起来像这样：vector<int>::iterator insertLoc(v.begin()); for (int i = 0; i < numValues; ++i) { insertLoc = v.insert(insertLoc, data[i]); ++insertLoc; }注意，我们必须小心保存 insert 的返回值以供下一次循环迭代使用，然后递增返回的迭代器。如果我们不在每次插入后更新 insertLoc，我们会遇到两个问题。首先，第一次插入后，所有后续的循环迭代都会导致未定义行为，因为每次调用 insert 都会使 insertLoc 失效。其次，即使 insertLoc 保持有效，我们也会始终在向量的开头插入（即 v.begin()），结果是复制到 v 的整数会以相反的顺序排列。如果我们遵循第43条的建议，用对 copy 的调用替换循环，我们会得到类似这样的代码：copy(data, data + numValues, inserter(v, v.begin()));当 copy 模板被实例化时，基于 copy 的代码和显式循环的代码几乎是相同的，因此为了分析效率，我们将专注于显式循环，同时记住，这种分析同样适用于使用 copy 的代码。关注显式循环只是为了更容易理解效率损失的来源。是的，这里的“损失”是复数形式，因为使用单元素版本的 insert 的代码会给你带来多达三种不同的性能“税”，而如果使用范围版本的 insert，这些开销都可以避免。第一个“税”是不必要的函数调用。将 numValues 个元素逐一插入到 v 中，自然需要调用 insert 函数 numValues 次。而使用范围形式的 insert，只需要进行一次函。形式的 insert，你绝对不会支付这笔“税”。内联优化无法帮你避开第二种“税”，即低效地将 v 中已有元素移动到插入后的最终位置的成本。每次调用 insert 向 v 中添加一个新值时，插入点之后的每个元素都必须向上移动一个位置，以为新元素腾出空间。因此，位置为 p 的元素必须移动到位置 p+1，以此类推。在我们的示例中，我们将 numValues 个元素插入到 v 的开头，这意味着在插入之前，v 中的每个元素都需要向上移动总计 numValues 个位置。然而，每次调用 insert 时，它们只会向上移动一个位置，因此每个元素总共会被移动 numValues 次。如果在插入之前 v 中有 n 个元素，那么总共会发生 n*numValues 次移动操作。在这个示例中，v 存储的是 int，因此每次移动可能会归结为调用一次 memmove。但是，如果 v 存储的是用户定义的类型（例如 Widget），每次移动都会导致调用该类型的赋值运算符或拷贝构造函数。（大多数情况下会调用赋值运算符，但每次移动 vector 的最后一个元素时，这次移动将通过调用该元素的拷贝构造函数完成。）因此，在一般情况下，将 numValues 个新对象逐一插入到一个包含 n 个元素的 vector<Widget> 的开头，会产生 n*numValues 次函数调用的开销：(n-1)*numValues 次调用 Widget 的赋值运算符，以及 numValues 次调用 Widget 的拷贝构造函数。即使这些调用被内联优化，你仍然需要执行 numValues 次移动 v 中元素的操作。相比之下，标准要求范围插入函数将已有的容器元素直接移动到它们的最终位置，也就是说，每个元素只需要移动一次。总的开销是 n 次移动操作，其中 numValues 次调用容器中对象类型的拷贝构造函数，其余的调用该类型的赋值运算符。与单元素插入策略相比，范围插入减少了 n*(numValues-1) 次移动操作。仔细想一想这点。如果 numValues 是 100，那么范围形式的 insert 将比反复调用单元素形式的 insert 少执行 99% 的移动操作！在继续讨论单元素成员函数相较于它们的范围版本所带来的第三种效率成本之前，我需要做一个小的补充说明。我在上一段中所写的内容是完全真实的，但并不完全全面。范围插入函数仅在能够在不失去当前位置的情况下确定两个迭代器之间的距离时，才能通过一次移动将元素放置到它的最终位置。这种情况几乎总是成立，因为所有前向迭代器（forward iterators）都提供这种功能，而前向迭代器几乎无处不在。标准容器的所有迭代器都提供前向迭代器功能，非标准的哈希容器（参见第25条）的迭代器也是如此。作为数组的迭代器的指针同样支持这种功能。实际上，唯一不支持前向迭代器功能的标准迭代器是输入迭代器（input iterators）和输出迭代器（output iterators）。因此，除非传递给范围形式 insert 的迭代器是输入迭代器（例如 istream_iterators，参见第6条），否则我上面所写的内容都是正确的。在这种情况下，范围插入函数才可能被允许逐个位置地将元素移动到它们的最终位置。如果它确实以这种方式实现，那么在元素移动次数方面的优势将不复存在。（对于输出迭代器，这个问题并不存在，因为输出迭代器不能用于指定 insert 的范围。）对于那些愚蠢地选择使用重复的单元素插入操作而不是单次范围插入操作的人来说，最终的性能“税”与内存分配有关，尽管它也带来了讨厌的拷贝成本。正如第14条所解释的，当你试图向一个内存已满的 vector 中插入一个元素时，vector 会分配具有更大容量的新内存，将旧内存中的元素复制到新内存中，销毁旧内存中的元素，并释放旧内存。然后，它会添加正在插入的元素。第14条还解释了，vector 的实现通常通过某种乘法因子增加其容量，因此插入 numValues 个新元素通常会导致按 numValues 的对数比例多次分配新内存。例如，如果以增长率为1.5的方式向一个 vector 中逐个插入1000个元素，则可能导致多达18次新内存分配 （包括每次分配时对元素的复制）。相比之下（现在这已经显而易见了），范围插入可以在开始插入之前（假设提供了前向迭代器）计算出所需的新内存量，因此它最多只需要重新分配一次底层内存。可以想象，这种方式节省的开销是相当可观的。我刚刚进行的分析是针对 vector 的，但同样的推理也适用于 string。对于 deque，推理类似，但由于 deque 的内存管理方式不同于 vector 和 string，关于重复内存分配的论点并不适用。然而，关于不必要地多次移动元素的论点通常仍然适用（尽管细节有所不同），以及关于函数调用次数的观察也是如此。在标准序列容器中，仅剩下 list，但即使在这里，使用范围形式的 insert 而不是单元素形式的 insert 也有性能优势。当然，关于重复函数调用的论点仍然成立，但由于链表的工作方式，拷贝和内存分配的问题并不存在。取而代之的是一个新问题：对链表中某些节点的 next 和 prev 指针进行重复的多余赋值。每次向链表中添加一个元素时，存储该元素的链表节点必须设置其 next 和 prev 指针。当然，新节点之前的节点（我们称之为 B，代表“before”）必须设置其 next 指针，而新节点之后的节点（我们称之为 A，代表“after”）必须设置其 prev 指针： 当通过调用 list 的单元素插入函数逐个添加一系列新节点时，除了最后一个新节点以外，所有其他新节点的 next 指针都会被设置两次：第一次指向 A，第二次指向插入在它之后的元素。而 A 的 prev 指针在每次有新节点插入到它前面时，都会被设置为指向该新节点。如果在 A 前插入了 numValues 个节点，那么插入的节点的 next 指针将会有 numValues-1 次多余的赋值操作，而 A 的 prev 指针也会有 numValues-1 次多余的赋值操作。总计下来，这就是 2*(numValues-1) 次不必要的指针赋值。当然，指针赋值的开销很低，但如果可以避免，为什么还要支付这些成本呢？到现在应该已经很清楚了，你完全可以避免这些成本，而关键就在于使用 list 的范围形式的 insert。由于该函数知道最终会插入多少个节点，它可以避免多余的指针赋值操作，仅通过对每个指针进行一次赋值来设置其正确的插入后值。因此，对于标准序列容器，在选择单元素插入和范围插入时，影响的不仅仅是编程风格。对于关联容器（associative containers），效率上的理由则更难以提出，尽管重复调用单元素插入所带来的额外函数调用开销问题仍然适用。此外，某些特殊类型的范围插入可能会为关联容器带来优化的可能性，但据我所知，这种优化目前仅存在于理论中。当然，当你读到这段内容时，理论可能已经变成了实践，因此在关联容器中使用范围插入可能确实比重复的单元素插入更高效。当然，它们绝不会比单元素插入更低效，所以选择范围插入并没有任何损失。即使抛开效率问题不谈，使用范围成员函数的事实仍然意味着在编写代码时需要更少的输入，同时还能生成更容易理解的代码，从而增强软件的长期可维护性。这两个特性本身就足以说服你优先选择范围成员函数，而效率上的优势只是一个额外的奖励。既然已经花了这么多篇幅讨论范围成员函数的优点，那么总结一下这些函数显然是合适的。了解哪些成员函数支持范围操作可以让你更容易识别使用它们的机会。在以下函数签名中，参数类型 iterator 字面上表示容器的迭代器类型，即 container::iterator。而参数类型 InputIterator 则表示任何输入迭代器都是可以接受的。范围构造（Range Construction）。所有标准容器都提供如下形式的构造函数：container::container(InputIterator begin, // beginning of range InputIterator end); // end of range当传递给此构造函数的迭代器是 istream_iterator 或 istreambuf_iterator（参见第29条）时，你可能会遇到 C++ 中最令人惊讶的解析行为之一——这种行为会导致编译器将该构造视为函数声明，而不是定义一个新的容器对象。第6条详细解释了关于这种解析行为的一切，包括如何避免它。范围插入（Range Insertion）。所有标准序列容器都提供如下形式的插入操作：void container::insert(iterator position, // where to insert the range InputIterator begin, // start of range to insert InputIterator end); // end of range to insert关联容器（Associative containers）使用其比较函数来确定元素的位置，因此它们提供了省略位置参数的函数签名：void container::insert(InputIterator begin, InputIterator end);在寻找将单元素插入替换为范围插入的方法时，不要忘记，有些单元素插入操作可能会通过采用不同的函数名称来伪装自己。例如，push_front 和 push_back 都是向容器中插入单个元素的操作，尽管它们并不叫 insert。如果你看到一个循环在调用 push_front 或 push_back，或者看到像 copy 这样的算法被传递了 front_inserter 或 back_inserter 作为参数，那么你就发现了一个可能可以用范围形式的 insert 来替代的地方，这通常是更优的策略。范围删除（Range Erasure）。每个标准容器都提供范围形式的 erase 操作，但序列容器和关联容器的返回类型有所不同。序列容器提供如下形式：iterator container::erase(iterator begin, iterator end);而关联容器则提供如下形式：void container::erase(iterator begin, iterator end);为什么会有这种差异？据称，让关联容器版本的 erase 返回一个迭代器（指向被删除元素之后的那个元素）会导致不可接受的性能损失。我和许多人一样认为这种说法站不住脚，但标准就是标准，标准规定了序列容器和关联容器的 erase 返回类型是不同的。本条关于 insert 的大部分效率分析同样适用于 erase。对于单元素 erase 的重复调用，其函数调用次数仍然比单次调用范围形式的 erase 多。使用单元素 erase 时，元素值仍需一个接一个地向它们的最终位置移动，而范围形式的 erase 则可以通过一次移动将它们直接移到最终位置。关于 vector 和 string 的 insert 的一个论点在 erase 上不适用，这涉及到重复分配的问题。（对于 erase，当然是涉及重复释放的问题。）这是因为 vector 和 string 的内存会自动扩展以容纳新元素，但当元素数量减少时，内存不会自动缩减。（第17条描述了如何减少 vector 或 string 持有的不必要内存。）范围形式的 erase 的一个特别重要的应用是“erase-remove 惯用法”。关于它的详细信息可以在第32条中找到。本条关于 insert 的大部分效率分析同样适用于 erase。对于单元素 erase 的重复调用，其函数调用次数仍然比单次调用范围形式的 erase 多。使用单元素 erase 时，元素值仍需一个接一个地向它们的最终位置移动，而范围形式的 erase 则可以通过一次移动将它们直接移到最终位置。关于 vector 和 string 的 insert 的一个论点在 erase 上不适用，这涉及到重复分配的问题。（对于 erase，当然是涉及重复释放的问题。）范围赋值（Range Assignment）。正如我在本条开头提到的，所有标准序列容器都提供范围形式的 assign 操作：void container::assign(InputIterator begin, InputIterator end);综上所述，有三个充分的理由支持优先选择范围成员函数而非它们的单元素版本。范围成员函数更易于编写，能够更清晰地表达你的意图，并且表现出更高的性能。这是一个难以超越的“三驾马车”。Item 6: 第6条：警惕C++中最令人困惑的解析Be alert for C++`s most vexing parse假设你有一个包含整数的文件，并且你希望将这些整数复制到一个list中。以下代码看起来像是一个合理的实现：ifstream dataFile("ints.dat"); list<int> data(istream_iterator<int>(dataFile), istream_iterator<int>());// 警告！这并不像你想象的那样这段代码的意图是通过list的范围构造函数（range constructor，参见第5条）传递一对istream_iterator，从而将文件中的整数复制到list中。这段代码可以编译，但是在运行时，它什么都不会做。它既不会从文件中读取任何数据，也不会创建一个list对象。这是因为第二条语句并没有声明一个list对象，也没有调用构造函数。它实际上在做的事情……好吧，它在做的事情如此奇怪，以至于我不敢直接告诉你，因为你可能不会相信我。相反，我需要一步步地解释清楚。你坐好了吗？如果没有，建议你找把椅子坐下……基础知识让我们从基础开始。这一行代码声明了一个函数f，它接受一个double类型的参数并返回一个int：int f(double d);下面这一行代码的作用是相同的，只不过给参数名称d加了一对括号。这些括号是多余的，会被忽略：int f(double (d));再来看这一行代码，它也声明了相同的函数，只是省略了参数名称：int f(double);这三种声明形式是等价的，尽管参数名称周围的括号可能是你不常见的。函数指针的声明接下来，我们来看另一组函数声明。第一行代码声明了一个函数g，它的参数是一个指向函数的指针，而这个函数本身接受无参数并返回一个double：int g(double (*pf)());这里还有另一种写法，它省略了指针符号*：int g(double pf());同样，我们可以省略参数名称，代码如下：int g(double ());注意到这里括号的不同用法：括号可以包裹参数名称（如d），但它们也可以单独存在。括号包裹参数名称时会被忽略，但如果括号单独存在，则表示参数是一个函数指针。回到原始代码现在让我们回到本条目开头的代码。以下是代码的完整内容：list<int> data(istream_iterator<int>(dataFile), istream_iterator<int>());请做好心理准备。这段代码实际上声明了一个函数data，其返回类型是list<int>。这个函数data有两个参数：第一个参数名为dataFile，其类型是istream_iterator<int>。括号包裹着dataFile，但这些括号是多余的，会被忽略。第二个参数没有名称，其类型是“指向函数的指针”，这个函数本身接受无参数并返回一个istream_iterator<int>。令人惊讶吧？但这符合C++的一个普遍规则：任何可以被解析为函数声明的代码，都会被解析为函数声明。如果你已经使用C++编程一段时间了，你几乎肯定遇到过另一种这种规则的表现形式。比如，以下代码是一个常见的错误：class Widget { ... }; // 假设Widget有一个默认构造函数 Widget w(); // 噢，糟糕……这段代码并没有声明一个Widget对象w，而是声明了一个名为w的函数。这个函数不接受任何参数，返回一个Widget对象。学习识别这种错误几乎是每个C++程序员的“成人礼”。如何修复了解了这种解析规则后，我们可以轻松地表达我们真正的意图。虽然在函数声明中，形式参数周围不允许添加括号，但在函数调用中，参数周围添加括号是合法的。因此，通过添加一对额外的括号，我们可以强制编译器按照我们的意图解析代码：list<int> data((istream_iterator<int>(dataFile)), istream_iterator<int>());这才是声明data的正确方式。考虑到istream_iterator和范围构造函数的实用性（参见第5条），掌握这一点非常重要。编译器兼容性问题不幸的是，并非所有编译器都支持这种正确的写法。在我测试的多个编译器中，有将近一半的编译器拒绝接受带有额外括号的声明！为了兼容这些编译器，你可以退而求其次，使用错误的声明方式，但这既不便携也不明智。毕竟，当前错误的解析方式在未来很可能会被修复。更好的解决方案是放弃使用匿名的istream_iterator对象，而是显式地为它们命名。以下代码应该在所有编译器上都能正常工作：ifstream dataFile("ints.dat"); istream_iterator<int> dataBegin(dataFile); istream_iterator<int> dataEnd; list<int> data(dataBegin, dataEnd);虽然这种方式写起来稍显冗长，但它在语法上是明确的，对编译器和阅读代码的人都更加友好。总结C++中最令人困惑的解析规则之一是：几乎所有看起来像函数声明的代码都会被解析为函数声明。通过添加额外的括号或使用显式命名的对象，你可以避免这种问题，从而让代码更加明确、可移植和易于维护。
还有一会2024年就要过去了，今年是很特别、故事很丰富的一年，走过很多的地方，看了很多次数的云，爬了很多次数的山，看过湖泊海洋，看过雪地美景，看过沙漠戈壁。还是写一些文字作为纪念。奔波“赶时间的人没有四季，只有一站和下一站……”，2024年是奔波感很强的一年，从上海到长沙，再从西北旺到石景山再到西直门。每次出发身上都有不同的职责，每次不同的经历也给这一年画了深深的刻度，回头看来在每个刻度都给自己留了很是难忘的犯蠢的场景，现在这些愚蠢的场景不仅没有随着时间而消淡，反而变得愈加有趣和值得回味，可能这就是老人常说的“读万卷书不如行万里路”。我还是会在工作中碰到考虑不周到问题。有很多时候会不由自主陷入一个怪圈，明明很需要同事帮助，但是想了想还是自己亲力亲为更让自己安心，以至于到了项目最终结题验收的节点，还会周而复始碰到消耗时间的小问题，在里面兜转不停。菜鸟今年有的项目里带了实习生一起参与。有一个小孩我是真的喜欢，像极了我第一次做这一行的时候。我还记得当时从南京打包了所有行李，把寄送的终点写在了我女朋友（现在的夫人）的学校，然后也打包了我那辆心爱的捷安特山地自行车，然后从南京坐高铁连夜奔赴北京，从朝阳门地铁口出来一股新鲜的味道。那年冬天我上下班就骑着那辆山地车，拿着每天160元的实习工资，在人民日报社家属院和建国门的写字楼两点一线奔波。和这个小孩一样。那些日子真是菜得不能再菜。当时带我的师傅一步一步"逼"我，愣是让我自己理解了如何写代码、重构代码、vtk渲染模型的基本原理。话说回来这次带实习生的项目做完后，我试着假设我是当年的菜鸟，让我站在现在的项目面前来做这个项目。说实话第一反应还是向外力求助，因为向外力求助本身也是能力的一部分。但是最后真正把问题解决，靠的是短时间把碎片信息收集起来，组织成为自己的思路，外加对整个问题的分解，进而形成解决方案。遇到信手拈来的事情占少数，或多或少都会碰到意想不到的困难。生活翻开相册，按照时间排序，会感觉今年经历了好多事情。经历了挚友的婚礼，经历了自己两次身体的受伤，在单位打乒乓球结交了很多挚友，甚至在家附近的乒乓球馆也认识了各行各业的朋友。总之时间是连续的，不是说过了12月31号这个节点后，时间会立马换一个主题，让你看到不一样的配色，今年经历的美好，明年依旧会遇到，今年经历的压力，明年也不会少。当然，现在也逐渐意识到，生活不是竞速赛，每个人有自己的节奏，按照自己的配速一路跑下去，关注路边的野芳佳木，风霜顽石才能感受生活。2024.12.31于 北京
git使用规范在软件开发过程中，规范的分支管理能够有效提高团队协作和项目稳定性。下面是一种常见的分支管理策略——Git Flow，适用于大多数项目的版本管理。具体步骤如下：Git Flow 分支管理模型1. 主分支（Main or Master）主要分支用于保存发布的正式版本。任何提交到主分支的代码都应该是稳定且准备发布的代码。2. 开发分支（Develop）开发分支是所有开发动作的基础分支。这个分支包含最新的开发代码，能够稳定运行但可能未经过全面测试。3. 功能分支（Feature Branches）这些分支用于开发新功能，每个新功能创建一个独立的功能分支。这些分支从开发分支中派生，完成后合并回开发分支。命名规范： feature/功能名称创建：从开发分支创建 git checkout -b feature/功能名称 develop合并回开发分支：功能完成后合并回开发分支 git checkout develop 然后 git merge --no-ff feature/功能名称删除：合并完成后删除 git branch -d feature/功能名称4. 发布分支（Release Branches）当准备发布一个新的版本时，从开发分支创建一个发布分支。这些分支用于修正Bug和准备发布，包括更新版本号、文档等。完成后合并到主分支和开发分支。命名规范： release/版本号创建：从开发分支创建 git checkout -b release/版本号 develop合并到主分支：发布完成并合并到主分支 git checkout main 然后 git merge --no-ff release/版本号合并回开发分支：然后将这些改动合并回开发分支 git checkout develop 然后 git merge --no-ff release/版本号删除：合并完成后删除 git branch -d release/版本号5. 热修复分支（Hotfix Branches）这些分支用于修复生产环境中的紧急问题。它们直接从主分支创建，修复完成后合并回主分支和开发分支。命名规范： hotfix/修复描述创建：从主分支创建 git checkout -b hotfix/修复描述 main合并到主分支：修复完成并合并到主分支 git checkout main 然后 git merge --no-ff hotfix/修复描述合并回开发分支：然后将这些改动合并回开发分支 git checkout develop 然后 git merge --no-ff hotfix/修复描述删除：合并完成后删除 git branch -d hotfix/修复描述示例工作流程创建开发分支：git checkout -b develop main创建功能分支进行开发：git checkout -b feature/add-new-feature develop [进行开发...] git checkout develop git merge --no-ff feature/add-new-feature git branch -d feature/add-new-feature准备发布：git checkout -b release/v1.0 develop [修正Bug，准备发布...] git checkout main git merge --no-ff release/v1.0 git checkout develop git merge --no-ff release/v1.0 git branch -d release/v1.0紧急修复：git checkout -b hotfix/fix-critical-bug main [进行修复...] git checkout main git merge --no-ff hotfix/fix-critical-bug git checkout develop git merge --no-ff hotfix/fix-critical-bug git branch -d hotfix/fix-critical-bug通过使用Git Flow模型，你可以清晰地管理不同类型的工作并确保代码质量，减少合并冲突，同时保证主分支的稳定。功能分支（Feature Branch）命名一般遵循一定的规范，从名称中可以清楚地知道该分支的目的或正在进行的功能。常见的命名惯例是在分支名称前加上「feature/」，后接简要描述功能的关键词或JIRA号、任务编号等。下面是一些具体示例：基于功能描述：feature/user-loginfeature/payment-integrationfeature/update-profile-pagefeature/shopping-cart基于任务编号或JIRA号：feature/JIRA-1234-user-loginfeature/issue-5678-payment-integration结合功能描述和任务编号：feature/JIRA-2345-update-profile-pagefeature/TASK-7890-shopping-cart示例说明feature/user-login：这个分支名称清晰地表明该分支用于开发“用户登录”功能。任何需要实现用户登录功能的代码更改都在该分支中进行。feature/payment-integration：该分支用于支付集成功能的开发，包括与第三方支付服务的对接、支付流程的实现等。feature/JIRA-1234-user-login：这里的分支名称结合了JIRA任务号和功能描述，便于追踪任务进度和了解分支的目标。feature/issue-5678-payment-integration：该分支创建用于处理编号为5678的任务，目标是实现支付集成功能。feature/JIRA-2345-update-profile-page：分支名称表明它与JIRA上编号为2345的任务相关，主要用于用户个人资料页面的更新功能开发。举例的实际操作假设项目开发中新添加用户登录功能，开发人员Alice收到任务并创建功能分支：# 从develop分支创建功能分支 git checkout -b feature/user-login develop # Alice在feature/user-login中进行开发 [开发用户登录功能...] # 将功能开发完成后合并回开发分支 git checkout develop git merge --no-ff feature/user-login # 删除功能分支 git branch -d feature/user-login通过遵循这样的分支命名和使用规范，团队成员可以轻松地识别每个分支的用途，方便协作和代码管理。合并分支时，不同的策略可以影响代码库的历史记录、可读性以及维护性。常用的合并策略包括创建合并提交（merge commit）、压缩合并（squash and merge）和变基（rebase）。下面分别介绍它们的特点及使用场景。创建合并提交（Merge Commit）特点保留所有历史记录：原分支的所有提交记录都保留，且生成一个新的合并提交。显示分支结构：可以直观查看分支合并情况、来源以及合并点。适用场景团队协作：需要保留详细的开发历史，以便追踪每次更改的详细过程。复杂项目：多分支并行开发，清楚显示合并结构有助于理解完整的开发流程。操作git checkout target-branch git merge source-branch压缩合并（Squash and Merge）特点简化历史记录：将源分支的所有提交压缩成一个提交，合并到目标分支。减少噪音：适用于源分支中有很多小而琐碎的提交，需要简化提交历史。适用场景单个功能：功能分支包含很多中间提交（例如小修复、调试输出等），希望在目标分支中只保留一个清晰的功能完成记录。代码审查后：代码审查过程中产生了多个小提交，最终合并希望保留简洁、易读的历史记录。操作通过GitHub或GitLab等平台的Web UI操作，通常有一个 Squash and Merge 选项，或者在命令行中：git checkout target-branch git merge --squash source-branch git commit -m "合并源分支并压缩：source-branch"变基（Rebase）特点重写历史：将源分支的提交重新应用在目标分支基础上，形成线性的提交历史。干净历史：避免了合并提交，使得提交历史更加线性和直观。适用场景个人项目：你是唯一的开发者或有极少数人参与，重写历史的风险低。代码整理：希望在合并前整理和清理提交历史，形成干净的线性提交史。注意事项禁止在公共分支上变基：非必要时，避免在已推送和多人协作的公共分支上使用变基，否则会导致其他人仓库的历史记录冲突。操作# 切到源分支进行变基 git checkout source-branch git rebase target-branch # 解决冲突后，继续变基 git rebase --continue # 切回目标分支并快速前推（保证目标分支是最新的） git checkout target-branch git merge source-branch总结创建合并提交：适用于需要保留完整详细的历史记录，适合团队协作、复杂项目。压缩合并：适用于简化历史记录，功能开发中产生了很多中间提交的情况。变基：适用于整理提交史，形成线性历史，但要谨慎使用，不要在公共分支上重写历史。选择哪种合并策略，需根据项目的具体需求和团队协作方式综合考虑。