news 2026/7/12 4:22:11

C++异步编程四剑客:future、async、promise与packaged_task实战解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++异步编程四剑客:future、async、promise与packaged_task实战解析

1. 项目概述:为什么我们需要异步编程四剑客?

如果你写过C++的多线程程序,大概率经历过这样的场景:主线程启动一个后台任务去计算某个复杂结果,然后继续处理其他事情,等需要那个结果时,再回头去“取”。这个过程如果只用原始的std::thread和互斥锁std::mutex来协调,代码会迅速变得复杂且容易出错——你需要手动管理线程生命周期,用条件变量通知结果就绪,还要小心翼翼地处理数据竞争和死锁。

C++11引入的“异步编程四剑客”——std::future,std::async,std::promisestd::packaged_task——正是为了解决这类问题而生的。它们不是四个孤立的工具,而是一套相互关联、层层封装的异步任务处理框架。简单来说,它们提供了一种更高级、更安全的线程间通信和数据传递机制,让你能像调用普通函数一样发起异步任务,并优雅地获取其结果,而无需直接与底层线程和锁打交道。

这套机制的核心价值在于“解耦”和“抽象”。它将“任务的执行”与“结果的获取”分离开,将“数据的生产者”与“消费者”分离开。对于需要处理I/O密集型操作(如网络请求、文件读写)、计算密集型任务分解,或者构建响应式事件驱动系统的开发者来说,这四者组合使用,能极大简化并发代码的复杂度,提升可维护性。接下来,我们就逐一拆解这四位“剑客”的招式与内功。

2. 核心组件深度解析:四剑客的定位与关系

在深入每个组件的细节之前,我们必须先理清它们之间的层次关系和设计哲学。这四者并非平行选择,而是构成了一个从底层同步原语到高层便捷接口的完整工具链。

2.1 核心关系图与设计哲学

我们可以用一个自底向上的视角来理解它们:

  1. std::promise&std::future:这是最基础的一对,构成了异步结果的“传输通道”。promise是生产者端,负责设置值或异常;future是消费者端,负责获取(等待)这个值或异常。它们直接管理着一个共享状态(shared state)。
  2. std::packaged_task:在promise/future之上的一层封装。它将一个可调用对象(函数、Lambda、函数对象)包装起来,当其被调用时,自动将返回值或抛出的异常设置到与之关联的promise中。你可以把它看作promise+ 可调用对象”的打包器。
  3. std::async:这是最高层的抽象,可以看作是std::thread+std::packaged_task的便捷组合。它接受一个可调用对象和参数,然后(可能)在一个新线程中执行它,并直接返回一个future供你获取结果。它最大程度地隐藏了线程管理和任务包装的细节。

它们之间的关系可以概括为:async基于packaged_taskthread,而packaged_task内部使用了promise/future对。future是最终获取结果的统一句柄。

2.2std::future:异步结果的唯一句柄

std::future<T>是一个模板类,它代表了一个将在未来某个时刻可用的值(类型为T)。它是获取异步操作结果的唯一途径,核心职责是“等待”和“提取”。

核心接口与行为:

  • get():一次性操作。调用它会阻塞当前线程,直到共享状态就绪(即值或异常已被设置),然后返回该值或重新抛出存储的异常。调用后,future对象变为无效(valid() 返回 false)。
  • wait(): 仅阻塞等待,直到结果就绪,但不取出值。
  • wait_for()/wait_until(): 带超时或时间点的等待。
  • valid(): 检查此future对象是否关联着一个有效的共享状态。
  • share(): 将future转换为std::shared_future。调用后,原future失效。

关键特性与陷阱:

  • 移动语义future对象只能移动(move),不能复制(copy)。这确保了异步结果的唯一所有权,防止多个消费者意外地多次调用get()
  • 一次性消费get()只能调用一次。第二次调用会导致std::future_error异常。这是最容易踩的坑之一。
std::future<int> fut = std::async([](){ return 42; }); int a = fut.get(); // 正确,获取值 42 // int b = fut.get(); // 错误!future 已无效,抛出 std::future_error

2.3std::promise:结果的设定者

std::promise<T>future配对出现,它是结果的“写入端”。你可以在一个线程中通过promise设置值或异常,而在另一个线程中通过对应的future获取它。

核心接口与行为:

  • get_future(): 获取与此promise关联的future对象。每个promise只能调用一次此方法。
  • set_value(): 设置结果值,并令共享状态就绪。
  • set_exception(): 设置一个异常(通常通过std::current_exception()捕获),并令共享状态就绪。
  • set_value_at_thread_exit()/set_exception_at_thread_exit(): 设置值或异常,但承诺在线程退出时才令共享状态就绪。这有助于确保某些线程局部对象在结果就绪时依然有效。

一个典型的生产者-消费者示例:

#include <iostream> #include <thread> #include <future> #include <chrono> void producer(std::promise<int> prom) { std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时计算 prom.set_value(100); // 生产者设置结果 // 如果计算失败:prom.set_exception(std::make_exception_ptr(std::runtime_error("Calculation failed"))); } int main() { std::promise<int> prom; std::future<int> fut = prom.get_future(); // 消费者获取future std::thread prod_thread(producer, std::move(prom)); // 启动生产者线程,移交promise std::cout << "Main thread is doing other work...\n"; // 当需要结果时,等待并获取 int result = fut.get(); // 阻塞,直到 producer 调用了 set_value std::cout << "Result from producer: " << result << std::endl; prod_thread.join(); return 0; }

注意事项:

  • set_valueset_exception也只能调用一次,多次调用会抛出std::future_error
  • 如果promise在析构时仍未设置值或异常,其关联的futureget()时会抛出std::future_error异常(错误码为std::future_errc::broken_promise)。这提醒你,承诺了要给结果,就必须履行。

2.4std::packaged_task:可调用对象的任务包装器

std::packaged_task是一个类模板,它包装了一个可调用对象,使其能异步执行,并将其返回值(或抛出的异常)自动存储到内部的promise中,进而可以通过get_future()获取关联的future

它的本质是一个函数对象(functor)。当你调用packaged_task对象时(通过operator()),它实际上是在调用其内部包装的可调用对象,并将结果转发给内部的promise

基本用法:

#include <iostream> #include <thread> #include <future> int add(int a, int b) { return a + b; } int main() { // 1. 创建一个 packaged_task,包装一个可调用对象 std::packaged_task<int(int, int)> task(add); // 也可以包装lambda: [](int a, int b){ return a + b; } // 2. 获取与任务关联的 future std::future<int> fut = task.get_future(); // 3. 将任务移动到另一个线程中执行 // 注意:packaged_task 不可复制,必须使用 std::move std::thread t(std::move(task), 10, 20); // 4. 在主线程中通过 future 获取结果 int result = fut.get(); // 阻塞等待,获取 30 std::cout << "Result: " << result << std::endl; t.join(); return 0; }

为什么需要packaged_task想象一下,如果只有promise,你需要手动在任务函数中捕获promise对象,并在函数末尾调用set_valuepackaged_task将这个模式标准化和自动化了。它特别适合将现有的函数或函数对象“转换”成一个可以异步执行并获取未来结果的任务单元,是构建任务队列(thread pool)的理想基础组件。

2.5std::async:一键式异步执行

std::async是一个函数模板,它是最上层的便利接口。你给它一个函数和参数,它帮你处理线程创建(或从线程池获取)、任务包装(packaged_task)、并返回一个future

启动策略(Launch Policy):这是async最需要理解的部分,通过第一个参数指定:

  • std::launch::async异步执行。函数必定会在一个新线程(可能是系统线程或线程池中的线程)中执行。
  • std::launch::deferred延迟执行。函数不会立即执行,而是被“延迟”到其返回的future首次调用get()wait()时,并且在调用者的线程中同步执行
  • std::launch::async | std::launch::deferred(默认):由实现自行选择策略。编译器/标准库可以决定是立即异步执行还是延迟执行。这是不指定策略时的默认行为,也是不明确性的来源。

基本用法:

#include <iostream> #include <future> #include <chrono> int compute() { std::this_thread::sleep_for(std::chrono::seconds(1)); return 42; } int main() { // 明确指定异步执行 std::future<int> fut_async = std::async(std::launch::async, compute); std::cout << "Main thread continues immediately.\n"; std::cout << "Async result: " << fut_async.get() << std::endl; // 等待并获取 // 延迟执行 std::future<int> fut_deferred = std::async(std::launch::deferred, compute); std::cout << "Deferred task not started yet.\n"; // 直到调用 get(),任务才在当前线程(主线程)执行 std::cout << "Deferred result: " << fut_deferred.get() << std::endl; // 默认策略(不推荐在生产代码中依赖其具体行为) // auto fut_default = std::async(compute); // 行为由实现定义 return 0; }

std::async的陷阱与最佳实践:

  1. 默认策略的歧义性:永远不要依赖默认启动策略的行为。如果你需要真正的异步,务必显式指定std::launch::async。否则,延迟执行可能导致get()时发生意料之外的阻塞,或者使得wait_for/wait_until总是返回std::future_status::deferred,从而破坏超时逻辑。
  2. 返回值future的析构会隐式等待:这是async一个非常关键且容易忽略的特性。当返回的future对象析构时,如果其关联的异步任务是以async策略启动且尚未完成,析构函数会阻塞等待任务完成。这可能导致隐式的、难以察觉的同步点。
    void fire_and_forget_bad() { std::async(std::launch::async, []{ std::this_thread::sleep_for(std::chrono::seconds(5)); std::cout << "Task done.\n"; }); // 临时 future 在此析构,会阻塞等待5秒! std::cout << "Function returns after waiting.\n"; } void fire_and_forget_good() { auto fut = std::async(std::launch::async, []{ std::this_thread::sleep_for(std::chrono::seconds(5)); std::cout << "Task done.\n"; }); fut.detach(); // C++标准中没有 detach!这是错误的示例。正确做法是保存 future 或使用其他机制。 // 正确做法:将 future 存储到某个生命周期足够长的容器中,或者使用 std::thread 并 detach(如果需要真正的“发射后不管”)。 }
    因此,对于“发射后不管”的任务,std::async可能不是最佳选择,std::thread配合detach更直接(但需自行处理异常和资源)。

3. 组合应用与实战场景剖析

理解了单个组件后,我们来看看它们如何组合解决实际问题。这部分的重点是理解不同场景下,如何选择最合适的工具。

3.1 场景一:简单的异步计算与结果获取

这是std::async最典型的应用场景。你有一个纯函数,想让它后台运行,主线程稍后取结果。

#include <future> #include <vector> #include <numeric> #include <iostream> // 一个耗时的计算函数 double calculate_pi_approximation(long iterations) { double sum = 0.0; for (long i = 0; i < iterations; ++i) { double term = (i % 2 == 0) ? 1.0 : -1.0; sum += term / (2 * i + 1); } return 4.0 * sum; } int main() { // 使用 async 异步启动计算 std::future<double> pi_future = std::async(std::launch::async, calculate_pi_approximation, 100'000'000L); // 主线程可以同时做其他事情 std::cout << "Main thread is doing other work (e.g., UI updates)...\n"; // 当需要结果时,get() 会阻塞等待计算完成 try { double pi_approx = pi_future.get(); std::cout << "Approximation of Pi: " << pi_approx << std::endl; } catch (const std::exception& e) { std::cerr << "Calculation failed: " << e.what() << std::endl; } return 0; }

实操心得:对于这种一次性、无状态、结果明确的函数,std::async是首选。务必捕获get()可能抛出的异常,因为任务函数中的任何未捕获异常都会通过future传递到调用线程。

3.2 场景二:构建线程池或任务队列

线程池的核心是将任务(可调用对象)提交到一个队列,由一组工作线程取出执行。std::packaged_task因其可移动、可存储的特性,是任务队列元素的完美候选。

#include <iostream> #include <thread> #include <future> #include <queue> #include <mutex> #include <condition_variable> #include <functional> #include <vector> class SimpleThreadPool { public: SimpleThreadPool(size_t num_threads) : stop(false) { for(size_t i = 0; i < num_threads; ++i) { workers.emplace_back([this] { this->worker_loop(); }); } } template<typename F, typename... Args> auto enqueue(F&& f, Args&&... args) -> std::future<decltype(f(args...))> { // 推导返回类型 R using return_type = decltype(f(args...)); // 创建一个 packaged_task,包装任务函数和参数 // 使用 std::bind 或 lambda 进行参数绑定。注意 packaged_task 的模板参数是函数签名。 auto task = std::make_shared<std::packaged_task<return_type()>>( std::bind(std::forward<F>(f), std::forward<Args>(args)...) ); std::future<return_type> res = task->get_future(); { std::unique_lock<std::mutex> lock(queue_mutex); if(stop) { throw std::runtime_error("enqueue on stopped ThreadPool"); } // 将任务包装成一个 void() 类型的函数,放入队列 tasks.emplace([task](){ (*task)(); }); } condition.notify_one(); return res; } ~SimpleThreadPool() { { std::unique_lock<std::mutex> lock(queue_mutex); stop = true; } condition.notify_all(); for(std::thread &worker: workers) { worker.join(); } } private: std::vector<std::thread> workers; std::queue<std::function<void()>> tasks; std::mutex queue_mutex; std::condition_variable condition; bool stop; void worker_loop() { while(true) { std::function<void()> task; { std::unique_lock<std::mutex> lock(queue_mutex); condition.wait(lock, [this]{ return stop || !tasks.empty(); }); if(stop && tasks.empty()) return; task = std::move(tasks.front()); tasks.pop(); } task(); // 执行任务,packaged_task 内部会设置 promise } } }; // 使用示例 int main() { SimpleThreadPool pool(4); // 提交多个任务,获取 future auto fut1 = pool.enqueue([](int a, int b) { return a + b; }, 10, 20); auto fut2 = pool.enqueue([]() { std::this_thread::sleep_for(std::chrono::seconds(1)); return std::string("Hello from thread"); }); // 主线程继续工作... std::cout << "Main thread working...\n"; // 获取结果 std::cout << "Result 1: " << fut1.get() << std::endl; // 30 std::cout << "Result 2: " << fut2.get() << std::endl; // Hello from thread return 0; }

注意事项:这是线程池的极简示例,生产环境需要考虑更完善的异常处理、任务优先级、动态扩缩容等。关键在于理解packaged_task如何将任意返回类型的函数包装成一个void()任务,并通过future将结果传递回来。

3.3 场景三:多线程间的复杂协调与数据流

当多个线程需要协作完成一项工作,或者数据流需要经过多个处理阶段时,promise/future可以作为轻量级的线程间通信通道。结合std::shared_future,可以实现一对多的结果广播。

std::shared_future简介:它与future类似,但可以被复制,允许多个线程等待并获取同一个异步结果。通过futureshare()方法获得。

#include <iostream> #include <future> #include <vector> #include <thread> void data_producer(std::promise<int> prom) { // 模拟复杂的数据准备 std::this_thread::sleep_for(std::chrono::milliseconds(500)); prom.set_value(12345); // 生产数据 } void data_consumer(std::shared_future<int> shared_fut, int id) { // 多个消费者等待同一个结果 int data = shared_fut.get(); // get() 是线程安全的 std::cout << "Consumer " << id << " got data: " << data << std::endl; } void data_transformer(std::shared_future<int> input, std::promise<std::string> output) { // 一个处理阶段:将整数转换为字符串 int data = input.get(); output.set_value("Transformed: " + std::to_string(data * 2)); } int main() { // 第一阶段:生产原始数据 std::promise<int> data_promise; std::future<int> data_future = data_promise.get_future(); std::thread producer_thread(data_producer, std::move(data_promise)); // 将 future 转换为 shared_future,供多个下游消费者使用 std::shared_future<int> shared_data_future = data_future.share(); // 启动多个消费者线程 std::vector<std::thread> consumer_threads; for(int i = 0; i < 3; ++i) { consumer_threads.emplace_back(data_consumer, shared_data_future, i); } // 启动一个转换器线程,它也是消费者,同时是下一阶段的生产者 std::promise<std::string> transformed_promise; std::future<std::string> transformed_future = transformed_promise.get_future(); std::thread transformer_thread(data_transformer, shared_data_future, std::move(transformed_promise)); // 主线程等待最终转换结果 std::string final_result = transformed_future.get(); std::cout << "Main thread got final result: " << final_result << std::endl; // 等待所有线程结束 producer_thread.join(); transformer_thread.join(); for(auto& t : consumer_threads) t.join(); return 0; }

这个例子展示了一个简单的数据流管道:生产者 -> (多个消费者 + 一个转换器) -> 主线程。shared_future使得原始数据可以被安全地广播给多个线程。

4. 高级主题、陷阱与性能考量

掌握了基本用法后,我们还需要深入一些高级主题和实践中必然遇到的坑。

4.1 异常传递机制

异步编程中,异常处理至关重要。四剑客提供了完整的异常传递机制:在异步任务中抛出的任何未被捕获的异常,都会被捕获并存储到共享状态中。当消费者调用future.get()时,这个异常会在调用线程中被重新抛出。

std::future<void> fut = std::async(std::launch::async, [](){ throw std::runtime_error("Something bad happened in async task!"); }); try { fut.get(); // 这里会抛出 std::runtime_error } catch (const std::runtime_error& e) { std::cerr << "Caught exception from async task: " << e.what() << std::endl; }

对于promise,你也可以使用set_exception主动设置异常。这在你需要将非异常错误(如错误码)转换为异常传递时非常有用。

4.2future的状态与有效性管理

一个future对象有三种状态:

  1. 无效(no state):默认构造或已被移动。valid() == false
  2. 延迟(deferred):仅当asyncdeferred策略启动时。任务尚未执行。
  3. 就绪(ready)未就绪(not ready):共享状态已有值/异常(就绪),或仍在等待(未就绪)。

关键规则:

  • 只有valid()truefuture才能调用get(),wait(),share()
  • get()调用后,future变为无效。
  • share()调用后,原future变为无效。
  • 始终检查valid()或妥善管理future的生命周期,避免访问无效状态。

4.3 性能与实现细节

  1. std::async的线程来源:标准并未规定std::launch::async必须创建新线程。实现可能使用一个线程池。这意味着连续调用async可能复用线程,但也意味着线程局部存储(TLS)需要小心处理,因为下次任务可能跑在不同的线程上。
  2. 共享状态的开销future/promise背后的共享状态通常涉及动态内存分配和同步原语(如条件变量)。对于极高频、极轻量的任务,这种开销可能成为瓶颈。此时,可能需要更轻量级的无锁队列或其他并发结构。
  3. 等待与轮询futurewait_forwait_until可以用来实现非阻塞等待或超时。避免在循环中频繁调用短超时的wait_for进行轮询,这会浪费CPU。通常结合条件变量或使用futurewait()在数据就绪时被通知是更好的选择。
  4. std::condition_variable的对比promise/future是单次、单值的通信机制。condition_variable则更通用,可以用于多次通知和复杂的条件等待。对于简单的“结果已就绪”通知,future更简洁安全。

4.4 常见问题排查与调试技巧

  1. std::future_error: No statefuture already retrieved

    • 原因:调用了无效futureget(),或多次调用get()
    • 排查:检查future的来源。它是否来自promise.get_future()packaged_task.get_future()get()是否只调用了一次?使用future.valid()进行调试检查。
  2. std::future_error: Broken promise

    • 原因:与future关联的promise在析构前未设置值或异常。
    • 排查:确保每个promise在离开作用域前,都调用了set_valueset_exception或其_at_thread_exit变体。检查所有可能提前返回或抛出的代码路径。
  3. 程序在future.get()处永久挂起

    • 原因:生产者端(promisepackaged_task)从未设置结果。
    • 排查
      • 生产者的线程是否正常启动并运行到了set_value
      • 生产者线程是否因异常而提前终止?异常是否被捕获并传递给了promise
      • 对于packaged_task,你是否真的调用了它(或将其传递到线程执行)?
      • 对于async,检查启动策略。如果是deferredget()会同步执行函数,检查函数内部是否有死锁。
  4. 使用std::async后性能反而下降或产生过多线程

    • 原因:不加选择地使用默认策略的async,可能导致实现创建大量线程,或任务过轻,线程管理开销占比过高。
    • 解决
      • 对于大量的小任务,使用线程池(如上面基于packaged_task的示例)而非直接async
      • 显式指定std::launch::async以确保异步行为符合预期。
      • 使用性能分析工具监控线程创建数量。
  5. std::shared_futureget()是 const 的:与std::future不同,shared_future::get()是 const 成员函数,且可以多次调用。每次调用都返回结果的副本(对于值类型)或引用(对于引用类型)。这意味着对于大型对象,要考虑复制成本,可以考虑用std::shared_ptr包装结果。

5. 现代C++的演进与替代方案

C++11之后,标准库在并发方面仍在持续增强。了解这些进展有助于你在更复杂的场景下做出选择。

  • std::future的延续(C++11 无,第三方库或 C++20/23):标准的future不支持回调(即“当结果就绪时,请执行这个函数”)。这导致了链式异步调用需要手动嵌套get(),容易陷入“回调地狱”。C++20 提出了std::future::then但未正式引入。你可以使用第三方库(如 Facebook 的 Folly、Boost.Asio)中的future实现,它们提供了丰富的延续操作。C++23 引入了std::future<T>::then的类似物std::future<T>::and_thenor_else等,但支持度有待考察。

  • std::jthread(C++20):可协作中断的线程。它比std::thread更安全,在析构时会自动join,并提供了请求停止的机制。可以与std::stop_tokenstd::stop_source配合,优雅地终止长时间运行的异步任务。如果你的异步任务需要支持外部取消,考虑结合jthreadpromise(在任务中定期检查停止请求)。

  • 协程(C++20):这是异步编程的范式转变。协程允许你以近乎同步的写法编写异步代码,彻底避免回调嵌套。std::future可以作为协程的返回类型之一(std::future<T>本身不是可等待体,但可以适配)。新的std::task<T>(或类似提案)是专为协程设计的。如果你在使用 C++20 或更高版本,并且代码库较新,探索协程是处理复杂异步流的未来方向。

  • std::latchstd::barrier(C++20):用于多线程同步的轻量级工具,适用于“分阶段任务同步”的场景,与future的单次信号不同,它们可以复用。

如何选择?

  • 简单的一次性异步计算:首选std::async(std::launch::async, ...)。明确、简洁。
  • 需要将任务放入队列(线程池):使用std::packaged_task包装任务,配合自定义的队列和线程组。
  • 需要精细控制结果设置时机或处理复杂线程间通信:直接使用std::promisestd::future
  • 需要将结果广播给多个等待者:使用std::shared_future
  • 需要组合多个异步操作、避免回调地狱:在支持的情况下,探索支持延续的future库(如 Folly)或直接使用 C++20 协程。
  • 需要支持任务取消:结合std::jthreadstd::stop_token,在任务函数中检查停止请求,并通过promise.set_exception提前结束。

C++的异步编程四剑客为你提供了从简单到复杂、从底层到高层的完整工具箱。理解它们各自的设计意图、相互关系以及背后的陷阱,是编写正确、高效且易于维护的并发C++代码的关键一步。从async的便捷开始,在需要更多控制时深入packaged_taskpromise/future,最终根据项目需求考量更现代的替代方案,这条路径能帮助你平滑地应对各种并发挑战。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/12 4:20:09

NISP二级与CISP认证对比:3大核心差异与无缝置换路径详解

NISP二级与CISP认证深度解析&#xff1a;职业发展路径与核心差异在信息安全行业快速发展的今天&#xff0c;专业认证已成为衡量从业人员能力的重要标尺。对于初入行业的新人而言&#xff0c;如何在众多认证中选择最适合自身发展路径的资质&#xff0c;往往成为职业规划中的关键…

作者头像 李华
网站建设 2026/7/12 4:20:05

Stage 进程模型:主进程、独立进程、组件运行关系怎么理解

Stage 进程模型&#xff1a;主进程、独立进程、组件运行关系怎么理解 很多状态问题看起来是变量没更新&#xff0c;本质是组件运行在不同进程或不同生命周期里。进程模型不清楚&#xff0c;就容易把内存单例当成全局事实。 这类问题通常不是某个 API 写错&#xff0c;而是工程边…

作者头像 李华
网站建设 2026/7/12 4:19:08

Qt C++图形界面开发实战:从零构建文档编辑器

1. 项目概述最近在整理自己的技术栈&#xff0c;发现很多朋友对用C做图形界面开发有点发怵&#xff0c;总觉得门槛高、生态不如Python或Web前端。其实&#xff0c;用Qt&#xff08;C&#xff09;来快速搭建一个功能完备的图形界面应用&#xff0c;体验非常顺畅。今天我就以开发…

作者头像 李华
网站建设 2026/7/12 4:18:56

Windows统计“所有软件今天的使用时长”,

统计“所有软件今天的使用时长”&#xff0c;但 Windows 默认不会完整保存每个桌面软件当天的前台使用时长&#xff0c;所以无法准确补算今天已经过去的时间。蓝奏:https://wwbrx.lanzout.com/iwitm3vtm3cb免费!!!我可以从现在开始帮你记录&#xff0c;统计内容包括&#xff1a…

作者头像 李华
网站建设 2026/7/12 4:15:19

【一线大厂Java面试题合集】第36篇-SQL优化与Explain实战

第36篇:SQL 优化与 Explain 实战 模块:MySQL深度解析 | 难度:进阶 | 面试频率:★★★★★ 1. Explain 的各字段含义?type / key / Extra 怎么看? Explain 基本使用 EXPLAIN SELECT * FROM t_user WHERE id = 1;-- 建表和测试数据

作者头像 李华