Excel合并去重,除了数组公式还有这3种更简单的方法:数据透视表、Power Query和VBA宏
2026/6/10 11:49:55
做语音识别的同学都知道,把麦克风里的模拟信号变成文字,最怕的不是模型大,而是“卡”。
std::thread t(worker, pkt); t.detach();用完即焚。一句话:线程频繁生灭的代价,比 FFT 本身还贵。
先给三种线程管理方案跑分,再决定用谁。
| 方案 | 吞吐量(句/s) | 95% 延迟(ms) | CPU 利用率 | 代码侵入性 |
|---|---|---|---|---|
| 裸 std::thread | 42 | 285 | 65% | 低 |
| OpenMP sections | 55 | 220 | 70% | 中 |
| 线程池(8 固定线程) | 78 | 170 | 55% | 高 |
结论:线程池能把“线程成本”降到常数级,同时让 CPU 空出来做 MFCC。
整体链路:
[采集]→[双缓冲队列 A/B]→[线程池]→[FFT+MFCC]→[Decoder]→[结果]shared_ptr<Frame>传递,自动释放,防止拷贝。/** * @file thread_pool.h * @brief 固定大小线程池,支持任意可调用对象 */ #pragma once #include <vector> #include <queue> #include <thread> #include <mutex> #include <condition_variable> #include <functional> #include <future> class ThreadPool { public: explicit ThreadPool(size_t n = std::thread::hardware_concurrency()) : stop_( false ) { for(size_t i = 0; i < n; ++i) workers_.emplace_back([this] { worker(); }); } ~ThreadPool() { { std::unique_lock<std::mutex> lk(queue_m_); stop_ = true; } cv_.notify_all(); for(auto &w: workers_) if(w.joinable()) w.join(); } template<class F> auto enqueue(F&& f) -> std::future<decltype(f())> { using RetType = decltype(f()); auto task = std::make_shared<std::packaged_task<RetType()>>(std::forward<F>(f)); std::future<RetType> res = task->get_future(); { std::unique_lock<std::mutex> lk(queue_m_); if(stop_) throw std::runtime_error("enqueue on stopped pool"); tasks_.emplace([task](){ (*task)(); }); } cv_.notify_one(); return res; } private: void worker() { while(true) { std::function<void()> task; { std::unique_lock<std::mutex> lk(queue_m_); cv_.wait(lk, [this]{ return stop_ || !tasks_.empty(); }); if(stop_ && tasks_.empty()) return; task = std::move(tasks_.front()); tasks_.pop(); } task(); } } std::vector<std::thread> workers_; std::queue<std::function<void()>> tasks_; std::mutex queue_m_; std::condition_variable cv_; bool stop_; };template<typename T> class DoubleBuffer { public: explicit DoubleBuffer(size_t buf_samples) : buf_a_(buf_samples), buf_b_(buf_samples), write_buf_(&buf_a_), read_buf_(&buf_b_) {} // 采集线程调用,非阻塞 void push(const T* data, size_t n) { if(write_buf_->size() + n > write_buf_->capacity()) switch_buffer(); // 自动切换 write_buf_->insert(write_buf_->end(), data, data + n); } // 工作线程调用,swap 后返回只读指针 std::shared_ptr<const std::vector<T>> swap_and_get() { switch_buffer(); auto tmp = std::make_shared<std::vector<T>>(std::move(*read_buf_)); read_buf_->clear(); return tmp; } private: void switch_buffer() { std::lock_guard<std::mutex> lk(m_); std::swap(write_buf_, read_buf_); read_buf_->clear(); // 留给下次写 } std::vector<T> buf_a_, buf_b_; std::vector<T>* write_buf_; std::vector<T>* read_buf_; std::mutex m_; };struct Frame { std::vector<float> samples; size_t id; }; using FramePtr = std::shared_ptr<Frame>; // 采集线程 void capture_thread(DoubleBuffer<float>& db, ThreadPool& pool, std::atomic<bool>& run) { size_t id = 0; while(run) { float tmp[320]; // 20 ms @16 kHz mic_read(tmp, 320); db.push(tmp, 320); // 每 40 ms 提交一次任务 if(++id % 2 == 0) { pool.enqueue([&db,id]{ auto frame = db.swap_and_get(); asr_process(frame); // 里面做 FFT、MFCC、解码 }); } } }异常处理:
stop_再join,保证未完成任务执行完。run=false,防止空悬指针。hardware_concurrency()+2时吞吐量再提 8%。| 线程池大小 | 1 | 2 | 4 | 8 | 10 |
|---|---|---|---|---|---|
| CPU% | 18 | 30 | 45 | 55 | 57 |
| 95% 延迟 | 380 | 260 | 200 | 170 | 165 |
sox -n -t raw -r 16000 -e signed -b 16 -c 1 pipe循环灌数据,模拟 200 路并发,线程池依旧稳在 55% CPU。join导致~ThreadPool()抛异常,程序直接 abort。shared_ptr循环引用让 Frame 迟迟不释放,内存暴涨;用weak_ptr在回调里破除即可。线程池 + 双缓冲已经让延迟降 40%,但 FFT 还跑在 scalar 指令上。下一步:
kiss_fft的复数乘法,实测 256 点可再降 15% 耗时。std::transform_reduce已经支持 SIMD execution,要不要试试?开放问题:如果是你,会如何把 SIMD 指令和线程池调度融合,让 FFT 计算不跨 L2 缓存?欢迎留言讨论。
写完代码才发现,原来“让 AI 听懂人话”不只是调模型,先把线程池捋顺才是硬道理。
如果你想把这套思路直接搬到线上,推荐试试火山引擎的从0打造个人豆包实时通话AI动手实验,里面把 ASR→LLM→TTS 整条链路都封装好了,小白也能 30 分钟跑通;我亲自踩过坑,确实比自己搭脚手架省不少头发。