【cpp tool】GDB coredump vscode GUI 和多线程常用笔记

outline

  • gdb常用命令
  • gdb coredump 回放
  • 多线程 常用知识
  • vscode 挂载 ros 断点调式
  • 如何配置多个目录下的文件断点

来源于过去工作中copilot问答 在此总结为笔记 防止忘记

文章目录


gdb常用命令

coredump

coerdump 查看前提

为 了 让 List 和 行 号 定 位 更 准 确 , 建 议 用 -00 -g -fno-omit-frame-pointer 编 译 :

-g: 包 含 调 试 符 号 ;

-00: 关 闭 优 化 , 避 免 行 号 和 代 码 折 叠 影 响 定 位 ;

-fno-omit-frame-pointer: 让 回 溯 更 稳 定 。


为什么

在 编 译 程 序 时 ,-g 表 示 生 成 调 试 符 号 信 息 (Debug Symbols), 这 些 信 息 会 嵌 入 到 可 执 行 文 件 中 , 使 得 调 试 工 具 ( 如 gdb) 能 够 :

  • 映 射 机 器 码 到 源 代 码 ( 如 变 量 名 、 函 数 名 、 行 号 等 ) 。
  • 提 供 完 整 的 堆 栈 跟 踪 (backtrace), 精 确 定 位 崩 溃 位 置 。
  • 支 持 单 步 调 试 、 查 看 变 量 值 等 高 级 调 试 操 作 。
    为 什 么 调 试 core dump 需 要 -g 编 译 的 可 执 行 文 件 ?
  1. 符 号 匹 配
    ● core 文 件 是 程 序 崩 溃 时 的 内 存 快 照 , 但 本 身 不 包 含 源 代 码 信 息 。
    ● 必 须 用 生 成 core 的 同 一 份 二 进 制 文 件 ( 最 好 是 -g 编 译 的 未 剥 离 版 本 ) 加 载 到 gdb, 才 能 正 确 解 析 core 文 件 中 的 地 址 。
  2. 未 剥 离 (Unstripped) 的 重 要 性
    ● 默 认 编 译 的 可 执 行 文 件 可 能 被 strip 命 令 移 除 调 试 符 号 ( 节 省 空 间 ), 导 致 gdb 无 法 解 析 。
    ● 调 试 时 应 保 留 未 剥 离 的 二 进 制 文 件 ( 即 不 执 行 strip) 。

如果要在当前文件夹子下 导出coredump文件 同时设置大小防护 需要进行一些设置

复制代码
命令:
sudo sysctl -w kernel.core_pattern=/home/q670270/H10Research/coredump/core_%e_%p_%t 

命令解读:


sysctl -w      :动态修改内核参数(`-w` 表示 write,即写入新值)                       |

kernel.core_pattern 控制 core dump 文件命名和存储路径的内核参数                           | 

/home/q670270/...自定义的 core 文件存储路径和命名格式                                  | 

  
存储路径:    core_%e_%p_%t 会被保存到目录:   

    /home/q670270/H10Research/coredump/ 


   需确保该目录存在且可写(否则 core 文件生成失败): 

     mkdir -p /home/q670270/H10Research/coredump 

     chmod 777 /home/q670270/H10Research/coredump  # 按需调整权限 

文件名格式:
	%e:executable

	%p:进程 PID  1234 

  	%t: 崩溃时间戳(UNIX timestamp)   1651234567

生成的 core 文件示例:   

   /home/q670270/H10Research/coredump/core_nginx_1234_1651234567 

作用总结

  1. 集中管理 core 文件

    将所有程序的 core dump 统一存储到指定目录,避免散落在不同路径。

  2. 避免文件名冲突

    通过 %e(程序名)、%p(PID)、%t(时间戳)确保每次崩溃生成唯一的 core 文件。

  3. 方便调试

    文件名直接体现崩溃的程序、进程和时间,快速定位问题。


注意事项

  1. 临时生效

    通过 sysctl -w 修改的参数 重启后会失效 。若需永久生效,需将以下内容写入 /etc/sysctl.conf

    bash 复制代码
    kernel.core_pattern=/home/q670270/H10Research/coredump/core_%e_%p_%t 

    然后执行:

    bash 复制代码
    sudo sysctl -p  # 重新加载配置 
  2. 依赖其他配置

    • 需确保系统允许生成 core 文件(检查 ulimit -c,建议设为 unlimited):

      bash 复制代码
      ulimit -c unlimited          # 当前会话生效 
      
      echo "ulimit -c unlimited" >> ~/.bashrc  # 永久生效 
    • 某些系统(如 Ubuntu)默认禁用 core dump,需关闭 apport 服务:

      bash 复制代码
      sudo systemctl stop apport 
      
      sudo systemctl disable apport 
  3. 权限与磁盘空间

    • 确保目标目录可写(尤其是崩溃进程的运行用户需有权限)。

    • core 文件可能很大(尤其是内存占用高的程序),需监控磁盘空间

GDB 调试常见命令




coredump 常见类型

gdb 针对coredump 常用命令 bt回溯

vscode GUI

vscode GUI 断点几点笔记

用的配置在项目的 launch.json 里(如果没有这个文件,VS Code 会让你创建/自动生成)。

调试启动配置

左侧 Run and Debug(运行和调试)面板

顶部下拉框选择一个配置

点齿轮图标(⚙)或 "create a launch.json"

→ 它会打开或创建 launch.json

位置:launch.json

里面每一个对象就是一个可选的调试配置,例如(示意):

"program":要调试的可执行文件路径(比如${workspaceFolder}/build/thread_pool_basic)

"cwd":运行时工作目录(常设为 ${workspaceFolder}/build)

"args":命令行参数

"environment":环境变量

"stopAtEntry":是否停在 main

"MIMode": "gdb":Linux 下用 gdb

"preLaunchTask":调试前先自动编译(配合 tasks.json)

调试前自动编译的任务配置(可选但常用)

位置:tasks.json

如果 launch.json 里写了:

那 VS Code 就会去 tasks.json 里找这个 label 的任务来执行(一般是 cmake --build ... 或 make)

Launch 和attach 区别
  • launch(启动调试)
    你让 VS Code 自己启动一个新进程,并从一开始就调试它。

典型场景:

调试 thread_pool_basic

需要从 main() 开始单步

想用 preLaunchTask 先编译再启动

关键点:

VS Code 会调用调试器(Linux 常见是 gdb),由调试器 run 启动程序

你在 launch.json 里会写 "program", "args", "cwd"

  • attach(附加到已运行的进程)
    你的程序已经在运行了(可能是你手动启动,或是后台服务),VS Code 只是连上去调试。

典型场景:

程序启动很复杂(必须先跑脚本/环境)

程序是 daemon/服务

你要调试"运行了一段时间后才出现的问题"

多进程/容器里已经有进程了

关键点:

不会启动程序

你需要提供 "processId"(选进程)或其它方式定位目标进程

如果进程没带调试符号、或被优化裁剪,体验会差

  • pipeTransport(通过"管道/远程通道"去跑 gdb)
    pipeTransport 不是一种"模式",它是一个机制:
    让 VS Code 不在本机直接运行 gdb,而是通过 ssh/docker 等方式,在另一台机器/容器里运行 gdb,再把调试协议转回来。

典型场景:

程序跑在远端 Linux

程序跑在 Docker 容器

本机是 Windows,但要调试 Linux 里的程序(WSL/ssh/container)

你会看到类似配置项:

pipeTransport.pipeProgram: "ssh" / "docker" / "bash" 等

pipeTransport.pipeArgs: ssh 参数、docker exec 参数

pipeTransport.debuggerPath: 远端的 gdb 路径(比如 gdb)

注意:pipeTransport 可以配合 launch 也可以配合 attach:

远端启动调试:launch + pipeTransport

远端附加调试:attach + pipeTransport

所以你说的"pipe attach"大概率就是:attach 到远端进程。

  • pipeLaunch(旧/特定扩展里的"通过管道启动")
    pipeLaunch 这个名字在不同扩展/版本里出现过,一般可以理解为:

"启动方式是 launch,但启动过程不是本机直接 exec,而是通过一条管道命令在另一端启动"

在 C++ 扩展(cpptools / cppdbg)里,现在更主流/标准的是用 pipeTransport 来做这件事。

你如果在某些旧配置里看到 pipeLaunch,它和 pipeTransport 的目标很像:解决"远端启动/远端调试器"的问题。


一句话对照表

launch:VS Code 启动程序并调试

attach:程序已在跑,VS Code 连上去调试

pipeTransport:调试器在远端/容器里跑(可配合 launch 或 attach)

pipeLaunch:类似"通过管道在远端启动"的旧式/特定配置(现在多用 pipeTransport)


插件 挂载 ros 断点调式

https://blog.csdn.net/weixin_46479223/article/details/132814522

加载core文件

复制代码
{ 

    "version": "0.2.0", 

    "configurations": [ 

        { 

            "name": "Debug core dump", 

            "type": "cppdbg", 

            "request": "launch", 

            "program": "/home/q670270/H10Research/coredump/coredumpinstance", 

            "coreDumpPath": "/home/q670270/H10Research/coredump/core_coredumpinstance_XXXX_YYYYYY",  // 替换为实际文件名 

            "MIMode": "gdb", 

            "stopAtEntry": false, 

            "externalConsole": false, 

            "cwd": "/home/q670270/H10Research/coredump", 

            "environment": [] 

        } 

    ] 

} 

多线程

几个示例

  • 线程的基本创建与管理
  • 死锁的产生与解决
  • 线程池的实现与使用
  • 竞争条件与并发安全

基础

cpp 复制代码
#include <iostream>
#include <thread>

void hello() {
    std::cout << "Hello from thread!" << std::endl;
}

void nohello() {
    std::cout << "noHello from thread!" << std::endl;
}

int a {0U};

int main() {
int b {1U};
    std::cout << "hardware_concurrency: " << std::thread::hardware_concurrency() << std::endl;

    std::thread t0(nohello);
    std::thread t1(hello);
    std::thread t2([](){ std::cout << "Hello from lambda thread!" << std::endl; std::cout<<a<<std::endl; });
    std::thread t3;
    t3 = std::thread([b](){ std::cout << "Hello from assigned thread!" << std::endl; std::cout<<b<<std::endl;});
    int c {0U};
    std:: cout << "Main thread id: " << std::this_thread::get_id() << std::endl;
    std:: cout << "t0 id: " << t0.get_id() << std::endl;
    std::cout << "t1 id: " << t1.get_id() << std::endl;
    std::cout << "t2 id: " << t2.get_id() << std::endl;
    std::cout << "t3 id: " << t3.get_id() << std::endl;

    std::cout<< "t0 joinable: " << t0.joinable() << std::endl;
    std::cout << "t1 joinable: " << t1.joinable() << std::endl;
    std::cout << "t2 joinable: " << t2.joinable() << std::endl;
    std::cout << "t3 joinable: " << t3.joinable() << std::endl;


    auto handle = t1.native_handle();
    std::cout << "t1 native_handle: " << handle << std::endl;

   
    std::thread t4([](){ std::cout << "Hello from swapped thread!" << std::endl; });
    t4.swap(t3); // t3现在是swapped thread, t4是assigned thread

   
    t1.join();
    std::cout << "t1 11111: " << std::endl;
    t2.detach();
  
    t3.join();
    std::cout << "t3 11111: " << std::endl;
    t4.join();

    std::cout << "t4 11111: " << std::endl;

    std::cout << "Main thread finished." << std::endl;

    t0.detach();

    std::cout << "t0 joinable: " << t0.joinable() << std::endl;
    std::cout << "t1 joinable: " << t1.joinable() << std::endl;
    std::cout << "t2 joinable: " << t2.joinable() << std::endl;
    std::cout << "t3 joinable: " << t3.joinable() << std::endl;
    return 0;
}

dead lock

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

// 演示"潜在死锁"与"正确避免死锁"的两种写法
std::mutex m1, m2;

// 可能产生死锁:两个线程获取锁顺序相反
void bad_task1() {
    std::lock_guard<std::mutex> lk1(m1);
    std::this_thread::sleep_for(std::chrono::milliseconds(50));
    std::lock_guard<std::mutex> lk2(m2);
    std::cout << "bad_task1 acquired both locks\n";
}
void bad_task2() {
    std::lock_guard<std::mutex> lk2(m2);
    std::this_thread::sleep_for(std::chrono::milliseconds(50));
    std::lock_guard<std::mutex> lk1(m1);
    std::cout << "bad_task2 acquired both locks\n";
}

// 正确方式:保持统一获取顺序 或 使用std::lock + std::lock_guard(adopt_lock)
void good_task1() {
    std::lock(m1, m2); // 一次性锁定,避免竞争顺序
    std::lock_guard<std::mutex> lk1(m1, std::adopt_lock);
    std::lock_guard<std::mutex> lk2(m2, std::adopt_lock);
    std::cout << "good_task1 acquired both locks safely\n";
}
void good_task2() {
    std::lock(m1, m2);
    std::lock_guard<std::mutex> lk1(m1, std::adopt_lock);
    std::lock_guard<std::mutex> lk2(m2, std::adopt_lock);
    std::cout << "good_task2 acquired both locks safely\n";
}

int main() {
    std::cout << "--- Potential deadlock demo (may hang depending on timing) ---" << std::endl;
    std::thread t1(bad_task1);
    std::thread t2(bad_task2);
    t1.join();
    t2.join();

    std::cout << "--- Safe locking demo ---" << std::endl;
    std::thread t3(good_task1);
    std::thread t4(good_task2);
    t3.join();
    t4.join();

    std::cout << "Deadlock demo finished." << std::endl;
    return 0;
}

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

std::mutex mtx;
std::timed_mutex tmtx;

void example_lock_guard(int id) {
    std::lock_guard<std::mutex> lk(mtx);
    std::cout << "lock_guard: thread " << id << " inside\n";
}

void example_unique_lock(int id) {
    std::unique_lock<std::mutex> lk(mtx);
    std::cout << "unique_lock: thread " << id << " inside\n";
}

void example_defer_lock(int id) {
    std::unique_lock<std::mutex> lk(mtx, std::defer_lock);
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    lk.lock();
    std::cout << "defer_lock + lock: thread " << id << " inside\n";
}

void example_try_lock(int id) {
    if (mtx.try_lock()) {
        std::cout << "try_lock: thread " << id << " got lock\n";
        mtx.unlock();
    } else {
        std::cout << "try_lock: thread " << id << " failed to get lock\n";
    }
}

void example_scoped_lock(int id) {
    std::scoped_lock lk(mtx);
    std::cout << "scoped_lock: thread " << id << " inside\n";
}

void example_timed_mutex(int id) {
    using namespace std::chrono_literals;
    if (tmtx.try_lock_for(50ms)) {
        std::cout << "timed_mutex: thread " << id << " got lock\n";
        tmtx.unlock();
    } else {
        std::cout << "timed_mutex: thread " << id << " timeout\n";
    }
}

int main() {
    std::cout << "Mutex tutorial start" << std::endl;

    std::thread a(example_lock_guard, 1);
    std::thread b(example_unique_lock, 2);
    std::thread c(example_defer_lock, 3);

    a.join(); b.join(); c.join();

    // try_lock demo
    std::thread t1(example_try_lock, 1);
    {
      t1.join();
      std::lock_guard<std::mutex> lk(mtx);
      std::this_thread::sleep_for(std::chrono::milliseconds(20));
    }

    // scoped_lock demo (C++17)
    std::thread s1(example_scoped_lock, 1);
    s1.join();

    // timed_mutex demo
    std::thread tm1(example_timed_mutex, 1);
    {
        std::lock_guard<std::timed_mutex> lk(tmtx);
        std::this_thread::sleep_for(std::chrono::milliseconds(60));
    }
    tm1.join();

    std::cout << "Mutex tutorial finished" << std::endl;
    return 0;
}

race

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
std::mutex mtx;
int counter = 0;
std::atomic<int> at_counter {0};

void increment() {
    for (int i = 0; i < 100000; ++i) {
        ++counter;
        ++at_counter;
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    std::cout << "Counter: " << counter << std::endl;
    std::cout << "Atomic Counter: " << at_counter.load() << std::endl;  
    return 0;
}

线程池

cpp 复制代码
#include <condition_variable>
#include <functional>
#include <iostream>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>

// 一个"最基础、最容易读"的线程池:
// - 只支持 void() 任务(没有 future / 返回值)
// - 一个任务队列 + 一个条件变量
// - 析构时 stop=true + notify_all + join
class BasicThreadPool {
public:
  explicit BasicThreadPool(std::size_t n) {
    workers.reserve(n);
    for (std::size_t i = 0; i < n; ++i) {
      workers.emplace_back([this, i] {
        while (true) {
          std::function<void()> task;

          // 1) 取任务:持锁访问队列
          {
            std::unique_lock<std::mutex> lock(mtx);

            // 没任务就睡:wait 会"释放 mtx 并阻塞",有人 notify 才醒
            cv.wait(lock, [this] { return stop || !tasks.empty(); });

            // stop=true 且队列空:退出线程
            if (stop && tasks.empty()) {
              return;
            }

            // 从队列取一个任务
            task = std::move(tasks.front());
            tasks.pop();
          }

          // 2) 执行任务:不要持锁执行(否则 enqueue 会被卡住)
          task();
        }
      });
    }
  }

  ~BasicThreadPool() {
    {
      std::lock_guard<std::mutex> lock(mtx);
      stop = true;
    }
    cv.notify_all();
    for (auto &t : workers) {
      t.join();
    }
  }

  // 提交一个任务(无返回值)
  void enqueue(std::function<void()> f) {
    {
      std::lock_guard<std::mutex> lock(mtx);
      tasks.push(std::move(f));
    }
    cv.notify_one();
  }

private:
  std::vector<std::thread> workers;
  std::queue<std::function<void()>> tasks;

  std::mutex mtx;
  std::condition_variable cv;
  bool stop = false;
};

int main() {
  BasicThreadPool pool(4);

  // 提交 8 个打印任务。
  // 注意:cout 多线程打印可能交错,这是正常现象(这里为了简单不加输出锁)。
  for (int i = 0; i < 8; ++i) {
    pool.enqueue([i] {
      std::cout << "[task " << i << "] run on thread "
                << std::this_thread::get_id() << "\n";
    });
  }

  // main 直接结束也没关系:pool 析构会等待所有 worker 退出。
  // 但析构只保证线程退出,不保证队列里任务都执行完吗?
  // 在这个实现里:stop=true 后 worker 会"先把队列清空再退出",因此任务会跑完。
  std::cout << "submitted tasks, leaving main...\n";
  return 0;
}

改进线程池

cpp 复制代码
#include <condition_variable>
#include <functional>
#include <future>
#include <iostream>
#include <mutex>
#include <queue>
#include <thread>
#include <tuple>
#include <type_traits>
#include <utility>
#include <vector>

namespace {
std::mutex g_cout_mutex;

template <class... Args>
void print_line(Args &&...args) {
  std::lock_guard<std::mutex> lock(g_cout_mutex);
  (std::cout << ... << std::forward<Args>(args)) << std::endl;
}
}  // namespace

class ThreadPool {
public:
    ThreadPool(size_t n) : stop(false) {
        for (size_t i = 0; i < n; ++i) {
            workers.emplace_back([this] {
                while (true) {
                    std::function<void()> task;
                    {
                        std::unique_lock<std::mutex> lock(this->queue_mutex);
                        this->condition.wait(lock, [this] {
                          return this->stop || !this->tasks.empty();
                        });  // 如果队列空并且 stop
                             // 也没触发,就释放锁、睡眠等待;直到"来任务"或"需要停机"才醒来,并且醒来会重新拿锁确认条件成立。
                        if (this->stop && this->tasks.empty()) return;
                        task = std::move(this->tasks.front());  // 移交所有权
                        this->tasks.pop();  // 拿出来之后删除
                    }
                    task();  // 运行这个对象

                    // 任务完成计数:用于 wait_for_all()。
                    {
                      std::lock_guard<std::mutex> lock(this->wait_mutex);
                      --this->unfinished;
                    }
                    this->wait_cv.notify_all();
                }
            });
        }
    }
    ~ThreadPool() {
        {
            std::unique_lock<std::mutex> lock(queue_mutex);
            stop = true;
        }
        condition.notify_all();
        for (std::thread &worker : workers) worker.join();
    }

    // 等待所有已提交的任务执行完成(不要求 stop 发生)。
    void wait_for_all() {
      std::unique_lock<std::mutex> lock(wait_mutex);
      wait_cv.wait(lock, [this] { return unfinished == 0; });
    }

    // 兼容原始用法:提交 void() 任务。
    void enqueue(std::function<void()> f) {
      {
        std::lock_guard<std::mutex> lock(wait_mutex);
        ++unfinished;
      }

        {
            std::unique_lock<std::mutex> lock(queue_mutex);
            if (stop) {
              // 线程池正在析构/停止,拒绝继续入队(避免悬空访问)。
              {
                std::lock_guard<std::mutex> lk(wait_mutex);
                --unfinished;
              }
              wait_cv.notify_all();
              throw std::runtime_error("enqueue on stopped ThreadPool");
            }
            tasks.push(std::move(f));
        }
        condition.notify_one();
    }

    // 升级点 1:支持返回值的 enqueue,返回 future。
    template <class F, class... Args>
    auto enqueue(F &&f, Args &&...args)
        -> std::future<std::invoke_result_t<F, Args...>> {
      using R = std::invoke_result_t<F, Args...>;

      // 用 lambda + tuple 捕获参数,避免 std::bind 在模板推导里带来递归/爆炸。
      auto bound =
          [func = std::forward<F>(f),
           tup = std::make_tuple(std::forward<Args>(args)...)]() mutable -> R {
        return std::apply(std::move(func), std::move(tup));
      };

      auto task_ptr =
          std::make_shared<std::packaged_task<R()>>(std::move(bound));
      std::future<R> fut = task_ptr->get_future();

      // 直接入队(不要再调用 enqueue(...),否则当 R=void 时会递归选中模板版)。
      {
        std::lock_guard<std::mutex> lock(wait_mutex);
        ++unfinished;
      }

      {
        std::unique_lock<std::mutex> lock(queue_mutex);
        if (stop) {
          {
            std::lock_guard<std::mutex> lk(wait_mutex);
            --unfinished;
          }
          wait_cv.notify_all();
          throw std::runtime_error("enqueue on stopped ThreadPool");
        }
        tasks.push([task_ptr] { (*task_ptr)(); });
      }
      condition.notify_one();

      return fut;
    }

private:
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;
    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop;

    // 升级点 2:用于等待"所有任务完成"。
    std::mutex wait_mutex;
    std::condition_variable wait_cv;
    std::size_t unfinished = 0;
};

int main() {
    ThreadPool pool(4);

    // 既可以提交 void() 任务,也可以提交带返回值任务并拿到 future。
    std::vector<std::future<int>> results;
    for (int i = 0; i < 8; ++i) {
      // 打印任务:保持原来的演示效果
      pool.enqueue([i] {
        print_line("Task ", i, " is running in thread ",
                   std::this_thread::get_id());
      });

      // 计算任务:演示返回值 future
      results.push_back(pool.enqueue([i] { return i * i; }));
    }

    pool.wait_for_all();

    int sum = 0;
    for (auto &f : results) sum += f.get();
    print_line("Sum of squares = ", sum);
    print_line("Thread pool demo finished.");
    return 0;
}

关于 mutex_tutuorial.cpp

为什么会"抢锁失败"?

  1. std::thread t1(...) 一创建就开始跑
    t1 线程的 example_try_lock() 可能马上就执行到 mtx.try_lock()。

而主线程在下一行 lock_guard,并不代表它已经先执行到了那一行。操作系统可能会这样调度:

主线程创建 t1

OS 立刻把 CPU 切给 t1

t1 执行 mtx.try_lock()

这时主线程可能还没来得及进入 { lock_guard... } 这个代码块

此时就有两种情况:

**情况 A(t1 先跑):**t1 抢到锁(try_lock 成功),输出 "got lock",然后 unlock

**情况 B(主线程先跑):**主线程先进入 lock_guard,把 mtx 锁住,并 sleep 20ms

t1 在这 20ms 内执行 try_lock -> 失败(因为锁已经被主线程持有)

关于 deadlock.cpp

第一段:bad_task1/bad_task2(潜在死锁)

std::thread t1(bad_task1);std::thread t2(bad_task2);t1.join();t2.join();

运行时可能出现的行为

有时程序直接卡住不动(最典型)

bad_task1 先锁 m1,再去锁 m2

bad_task2 先锁 m2,再去锁 m1

如果发生下面的调度顺序:

t1 拿到 m1

t2 拿到 m2

t1 等 m2(等 t2 释放)

t2 等 m1(等 t1 释放)

这就形成"环路等待",两边都等不到,主线程在 t1.join() / t2.join() 永远等不到返回,于是卡死。

有时能正常打印并结束

如果调度刚好让某个线程连续拿到两把锁(比如 t1 很快拿到 m1 后又拿到 m2),那就不会死锁。

所以它叫 "may hang depending on timing"。

结论:第一段不确定,会"有时成功、有时卡死"。

第二段:good_task1/good_task2(安全加锁)

std::thread t3(good_task1);std::thread t4(good_task2);t3.join();t4.join();

运行时的行为

good_task1/good_task2 使用了 std::lock(m1, m2):它会用一种避免死锁的策略一次性把两把锁都锁住(内部会处理顺序和重试)。

然后用 std::lock_guardstd::mutex lk(m1, std::adopt_lock) 接管这两把已经锁好的互斥量,保证函数结束后自动释放。

因此:

不会出现互相等待,只会出现"其中一个线程先拿到两把锁执行,另一个线程等它释放后再继续"。

所以 join() 一定会返回,程序一定能走到 "Deadlock demo finished"。

如何记忆线程是否安全

最短的判断公式

同一份数据,如果可能被多个线程同时访问,并且至少有一个线程会写 → 不加同步就是不线程安全(data race / UB)。

反过来:

只有读(并且期间没人写)→ 通常安全

每个线程只用自己的私有数据(局部变量、线程私有对象)→ 安全

共享数据 + 锁/原子/消息队列/条件变量保护 → 安全(前提是用对)

代码里怎么快速判断:3 步法

Step 1:找"共享"的东西

这些东西通常是共享的:

全局变量/静态变量

堆上对象(多个线程都拿着指针/引用)

类成员变量(比如线程池里的 tasks, stop)

std::cout 这种全局资源

只要是共享的,就要警惕。

Step 2:看有没有"写"

写包括:

赋值:stop = true

容器修改:push/pop/front(front 也可能读到内部结构)

自增:x++

修改对象内部状态:调用非 const 方法(大概率会写)

只要共享 + 写,就默认不安全。

Step 3:找同步点(有没有"保护它")

常见保护方式:

std::mutex(lock_guard/unique_lock)

std::atomic

把共享数据放在线程安全队列里(消息传递)

更高级的读写锁、信号量等

✅ 通常是线程安全的(在正确使用前提下)

  • 线程自己的局部变量
    比如 worker 函数里的 std::function<void()> task;(每个线程一份)
  • 不可变数据(const 且不被修改)
    -std::atomic 的原子读写(比如 atomic 的 fetch_add)
  • 每次访问都在同一把 mutex 保护下的共享数据
    ❌ 通常不是线程安全的(默认都当不安全)
  • 大部分 STL 容器:std::vector/std::queue/std::map/...
  • 多线程同时 push/pop 一定不安全
  • 多线程读 + 有线程写,也不安全
  • std::cout / std::cin(输出会交错,甚至内部状态互相影响)
  • 任意"共享对象"调用非 const 方法(除非文档明确说线程安全)

看到一行代码就问自己:

这行访问的数据是共享的吗?(谁还能访问它?)

这行是读还是写?(push/pop/++ 都算写)

这行是否在同一把锁或原子保护下?

如果没有保护,会不会两个线程同时跑到这里?

只要 1 + 2 的答案是 "是",3 是 "否",那就基本确定:不线程安全。

相关推荐
MounRiver_Studio2 小时前
RISC-V IDE MRS2使用笔记(十二):快捷配置页面
ide·mcu·risc-v·嵌入式开发
夏幻灵2 小时前
如何理解编译?
经验分享·笔记
研來如此2 小时前
VSCode连接远程服务器
服务器·ide·vscode
祁思妙想2 小时前
修改python后端项目名称:pycharm中fastapi框架的项目
ide·pycharm·fastapi
iconball2 小时前
个人用云计算学习笔记 --23(Shell 编程-2)
linux·运维·笔记·学习·云计算
iconball2 小时前
个人用云计算学习笔记 --26 OpenStack 核心服务
运维·笔记·学习·云计算·openstack
polarislove02142 小时前
9.1[ 定时器 ] 时基单元-嵌入式铁头山羊STM32笔记
笔记·stm32·嵌入式硬件
三品吉他手会点灯2 小时前
STM32F103 学习笔记-21-串口通信(第1节)-串口通信协议简介
笔记·stm32·单片机·嵌入式硬件·学习
风之子npu2 小时前
AXI 原子访问
arm开发·笔记·学习