当前位置: 首页 > news >正文

C++ `std::future` 与 `std::promise` 超全解析笔记

<摘要>

本笔记是一份为希望征服 C++ 异步编程的开发者准备的“活泼版”核心机制详解手册。它将彻底揭开 std::futurestd::promise 这对“梦幻组合”的神秘面纱,让你不仅明白“是什么”,更透彻理解“为什么”和“怎么用”。

笔记将带领你从 异步编程的“为什么” 出发,深入核心概念 std::future(未来期望)std::promise(未来承诺) 的协同工作机制。我们将通过一个贯穿始终的生动比喻——“快递模型”promise 是发货方,future 是收货方,std::async 是快递公司)——来化解所有抽象概念。

内容涵盖 核心API深度剖析线程间值传递与同步的魔法异常传递的优雅处理共享未来(std::shared_future)的妙用 以及 实战应用场景(从并行计算到状态机)。我们辅以大量代码示例、Mermaid 流程图和时序图,确保原理清晰可见。

无论你是想摆脱回调地狱,还是希望掌握现代 C++ 高效并发编程的利器,亦或是被线程间通信所困扰,这份笔记都将成为你的终极指南。让我们开始这场充满奇思妙想的 C++ 异步之旅吧!


<解析>

第一章:为啥要异步?——从“同步苦等”到“异步逍遥”

1.1 同步的烦恼:世界的“阻塞”

想象一下你的程序是一个热情的餐厅服务员(主线程)。

同步模式(Synchronous) 下,你的工作流程是这样的:

  1. 顾客A点了一份需要慢炖2小时的招牌菜。
  2. 你(主线程)就站在厨房门口,死死地盯着锅,什么都不做,干等2小时。
  3. 2小时后,菜好了,你端给顾客A。
  4. 然后你才开始服务下一桌顾客B。
// 伪代码:同步的、阻塞的世界
void synchronous_waiter() {Dish dish_for_A = kitchen.cook_slow_dish("A's Order"); // 阻塞2小时!serve_to_table(A, dish_for_A); // 2小时后...Dish dish_for_B = kitchen.cook_fast_dish("B's Order"); // 现在才做B的serve_to_table(B, dish_for_B);
}

这样做的缺点太明显了:

  • 极低的效率: 资源(你这个服务员)在等待期间被完全浪费。
  • 糟糕的响应性: 顾客B心里在骂街:“我就点杯咖啡,为啥要等2小时?”
  • 吞吐量瓶颈: 餐厅的服务能力取决于最慢的那道菜。
1.2 异步的曙光:世界的“非阻塞”

现在,让我们用 异步模式(Asynchronous) 来改造一下:

  1. 顾客A点了一份慢炖菜。
  2. 你(主线程)对厨房说:“做好了叫我!”,然后立刻转身就去服务顾客B。
  3. 你给顾客B做完咖啡并端上去。
  4. 在空闲时,你时不时问一下厨房:“A的菜好了吗?”(轮询),或者更高级一点,厨房会主动摇铃通知你(回调事件驱动)。
  5. 厨房摇铃了,你去把菜端给顾客A。
// 伪代码:异步的、非阻塞的世界
void asynchronous_waiter() {Future<Dish> future_dish_for_A = kitchen.async_cook("A's Order"); // 立刻返回一个“未来凭证”Dish dish_for_B = kitchen.cook_fast_dish("B's Order"); // 立刻做B的serve_to_table(B, dish_for_B);// ... 可能还可以服务顾客C、D...// 现在来看看A的菜好了没if (future_dish_for_A.is_ready()) { // 轮询一下Dish dish_for_A = future_dish_for_A.get(); // 获取结果serve_to_table(A, dish_for_A);}
}

异步的优势:

  • 极高的资源利用率: 服务员(主线程)在任务(慢炖菜)执行期间不会被阻塞,可以去做其他事情。
  • 出色的响应性: 即时任务(顾客B的咖啡)能够立刻得到处理。
  • 更高的吞吐量: 能够同时处理多个长时间任务,系统整体效率提升。

C++ 的实现方式: std::async, std::thread + std::future/std::promise 就是 C++11 为我们提供的“厨房摇铃系统”和“未来凭证”。

1.3 核心角色登场:Future 与 Promise

让我们用一個最貼近生活的快遞模型來理解它們:

  • std::promise(承诺): 好比是發貨方。他向社会(另一个线程)承诺:“我未来会发出一个包裹(数据或异常)”。他负责生产包裹,并把包裹交给快递系统。

    • 核心操作:set_value() / set_exception() -> 发货
  • std::future(未来): 好比是收貨方手中的物流單號取件碼。他持有这个凭证,就可以:

    1. 查询包裹状态(wait_for(), wait_until())。
    2. 等待包裹送达,如果没到就阻塞自己(get(), wait())。
    3. 最终提取包裹(get())。
    • 核心操作:get() -> 收货
  • std::async: 好比是快遞公司。你告诉它一个任务(比如“帮我去买包烟”),它立刻给你一个future(物流单号),然后自己安排一个快递员(线程)去执行这个任务。任务完成后,结果会自动通过 promise/future 通道送回来。

    • 它是 thread + promise 的便捷包装。

它们之间的关系:
一个 promise 和一个 future一对一的契约关系,通过一个共享状态(Shared State) 连接。promise 往共享状态里“写”,future 从共享状态里“读”。

set_value/set_exception
发货
get/wait
查询/提货
std::promise
发货方
共享状态
中转仓库
std::future
收货方/物流单

这个模型将贯穿我们整个讲解,让一切变得清晰易懂。


第二章:深度解剖“物流系统”——API详解与协同工作流

2.1 发货方:std::promise

std::promise 是一个模板类,你承诺要发送什么类型的货物,就用什么类型来实例化它。

核心操作:

  1. 做出承诺(构造函数):

    #include <future>
    std::promise<int> int_promise; // 我承诺,将来会发出一个int包裹
    std::promise<std::string> string_promise; // 我承诺,将来会发出一个string包裹
    
  2. 发货 set_value():
    这是履行承诺的最主要方式。

    void producer(std::promise<int>& prom) {std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时生产int result = 42; // 生产出的珍贵货物prom.set_value(result); // 打包并发货!(写入共享状态)std::cout << "Producer: Value 42 set in promise.\n";
    }
    

    一旦 set_value 被调用,共享状态就变为 就绪(ready)。这会解除所有在 future 端等待的线程的阻塞。

  3. 发出异常通知 set_exception():
    如果“生产过程中”出了意外(比如生产线故障),promise 也可以发送一个异常,而不是一个值。

    void faulty_producer(std::promise<int>& prom) {try {std::this_thread::sleep_for(std::chrono::seconds(1));throw std::runtime_error("Factory exploded!"); // 生产过程中发生意外// prom.set_value(...) // 永远不会执行} catch (...) {// 捕获所有异常,并将异常“打包”进promiseprom.set_exception(std::current_exception());// 也可以直接设置一个异常对象:// prom.set_exception(std::make_exception_ptr(std::runtime_error("Exploded")));}std::cout << "Producer: Exception set in promise.\n";
    }
    

    这对异步错误处理至关重要!异常可以安全地跨线程传递。

  4. 获取物流单 get_future():
    promisefuture 是一对。发货方(promise)需要生成一张对应的物流单(future)交给收货方。

    std::promise<int> prom;
    std::future<int> fut = prom.get_future(); // 从promise获取与之关联的future
    // 现在,fut 就是收货方的取件凭证
    

    重要: 一个 promiseget_future() 只能调用一次。多次调用会抛出 std::future_error 异常。就像一份合同只有一张原件。

2.2 收货方:std::future

std::future 也是一个模板类,必须与对应的 promise 类型匹配。

核心操作:

  1. 等待并提取货物 get():
    这是最核心的函数。它做三件事:

    • 阻塞等待: 如果货物(共享状态)还未就绪,调用 get() 的线程会被阻塞
    • 取货: 一旦就绪,它就获取值(或异常)。
    • 销毁凭证get() 只能调用一次。调用后,共享状态被销毁,future 变为无效(valid() == false)。
    void consumer(std::future<int>& fut) {std::cout << "Consumer: Waiting for value...\n";// 阻塞等待,直到生产者set_value或set_exceptionint result = fut.get(); // 如果生产者set_exception,这里会重新抛出那个异常!std::cout << "Consumer: Got value: " << result << "\n";// fut.valid() == false 了!不能再调用get或wait。
    }
    
  2. 等待 wait():
    只负责等待,不提取值。get() 内部其实就先调用了 wait()。如果你只是想同步,而不关心具体值,可以用这个。

    fut.wait(); // 只是阻塞等待,直到就绪
    // 之后可以再调用 get(),或者用 wait_for/wait_until 检查
    
  3. 限时等待 wait_for() / wait_until():
    这是“非阻塞”等待的核心。它们不会让线程无限期阻塞,而是等一段时间或等到某个时间点就返回,并告诉你当前的状态。

    std::future_status status = fut.wait_for(std::chrono::milliseconds(500));
    switch (status) {case std::future_status::ready:std::cout << "Value is ready!\n";break;case std::future_status::timeout:std::cout << "Still waiting...\n";break;case std::future_status::deferred:// 和std::async的启动策略有关,稍后讲break;
    }
    

    这允许你在等待的同时做一些其他工作,实现更灵活的协作。

  4. 检查有效性 valid():
    检查这个 future 是否关联着一个有效的共享状态。在调用 get() 之后,valid() 会变为 false

2.3 完整的快递流程:第一个例子

让我们把发货方和收货方用线程连接起来,完成一次完整的“快递”过程。

#include <iostream>
#include <future>
#include <thread>
#include <chrono>int main() {// 1. 创建“承诺”(发货方)std::promise<int> data_promise;// 2. 获取“未来凭证”(物流单)std::future<int> data_future = data_promise.get_future();// 3. 启动一个生产者线程(发货),把promise移动给它//    注意:promise不可拷贝,但可以移动,所有权转移给新线程std::thread producer_thread([promise = std::move(data_promise)]() mutable {std::cout << "Producer: Started working...\n";std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟工作std::cout << "Producer: Work done. Sending result...\n";promise.set_value(100); // 发货!});// 4. 在主线程(收货方)等待结果std::cout << "Main: Started waiting for the result...\n";// data_future.get() 会阻塞,直到生产者set_valueint result = data_future.get();std::cout << "Main: Got the result: " << result << std::endl;// 5. 等待线程结束producer_thread.join();return 0;
}

输出:

Main: Started waiting for the result...
Producer: Started working...
Producer: Work done. Sending result...
Main: Got the result: 100

使用 Mermaid 绘制的时序图
这张图清晰地展示了两个线程如何通过 promise/future 进行协同。

Main ThreadProducer Threaddata_futureShared Statedata_promise初始化std::promise<int> data_promiseget_future()创建关联std::future<int> data_future启动生产者线程std::thread(..., std::move(data_promise))data_future.get()(阻塞等待)模拟工作 2秒set_value(100)写入值 100状态标记为ready通知就绪解除阻塞从get()返回结果 100join()Main ThreadProducer Threaddata_futureShared Statedata_promise

这个过程完美诠释了线程间数据传递和同步的优雅方式。


第三章:快递公司的服务——std::async

手动管理 std::threadstd::promise 有点繁琐。std::async 是这个“快递模型”中的快递公司,它提供了一个更高级、更便捷的接口来启动异步任务。

3.1 两种派送策略(启动策略)

当你叫快递时,你可以选择:

  • 立即派送: 马上找个快递员出发。
  • 延迟派送: 先登记,等你有空了自己去取(或者等快递员有空再来拿)。

std::async 也接受类似的策略,通过 std::launch 参数指定:

  1. std::launch::async (异步派送):

    • 行为: 强制立即创建一个新的线程来执行任务。
    • 比喻: “马上派个快递员来取件!”
    • 注意: 即使你没有调用 future.get(),线程也会在后台运行。
  2. std::launch::deferred (延迟派送):

    • 行为: 延迟执行任务。只有在调用 future.get()future.wait() 时,任务才会在调用线程上同步执行
    • 比喻: “先把件放你这,等我需要时我自己过来取。”
    • 用途: 惰性求值,避免不必要的线程开销。
  3. 默认策略 (std::launch::async | std::launch::deferred):

    • 如果不指定策略,编译器可以自由选择两种方式中的一种。这意味着你的任务可能不会并发执行! 所以,如果明确需要并发,最好显式指定 std::launch::async
3.2 使用 std::async

它的基本用法非常简单:给它一个可调用对象(函数、lambda、函数对象等),它返回一个 std::future

#include <iostream>
#include <future>
#include <chrono>int calculate_meaning_of_life() {std::this_thread::sleep_for(std::chrono::seconds(2));return 42;
}int main() {// 方式1:使用默认策略(让编译器决定)std::future<int> result_future = std::async(calculate_meaning_of_life);// ... 主线程可以同时做其他事情 ...// 方式2:显式指定异步策略(推荐)auto result_future_async = std::async(std::launch::async, calculate_meaning_of_life);// 方式3:延迟执行auto result_future_deferred = std::async(std::launch::deferred, calculate_meaning_of_life);// 此时任务还未执行std::cout << "Main: Doing other work...\n";std::this_thread::sleep_for(std::chrono::seconds(3));// 现在需要结果了,才同步执行calculate_meaning_of_life函数std::cout << "The answer is: " << result_future_deferred.get() << std::endl;// 获取异步任务的结果std::cout << "The answer (async) is: " << result_future_async.get() << std::endl;std::cout << "The answer (default) is: " << result_future.get() << std::endl;return 0;
}

背后的魔法std::async 内部帮你创建了一个 std::promise,启动了一个线程(如果是 async 策略),线程执行完任务后会把结果 set_value 到那个 promise 中,而你拿到的 future 正是与之关联的那一个。它封装了所有手动操作。

3.3 传递参数

std::async 传递参数就像给 std::thread 传递参数一样简单,直接跟在函数名后面即可。参数会被拷贝移动到新线程中。

std::string concatenate(const std::string& a, const std::string& b) {return a + b;
}int main() {std::string s1 = "Hello, ";std::string s2 = "async world!";// 参数会被拷贝到新线程中auto fut = std::async(std::launch::async, concatenate, s1, s2);std::cout << fut.get() << std::endl; // 输出 "Hello, async world!"// 使用std::ref来传递引用(要非常小心生命周期!)// auto fut_ref = std::async(std::launch::async, concatenate, std::ref(s1), std::ref(s2));return 0;
}

第四章:异常处理与高级话题

4.1 跨线程的异常传递

这是 promise/future 机制最优雅的特性之一。它让异步错误处理变得和同步代码一样自然——使用 try-catch

#include <iostream>
#include <future>
#include <stdexcept>void might_throw(std::promise<int>& prom) {try {std::cout << "Worker: I might throw an exception!\n";throw std::runtime_error("Oops from worker thread!");prom.set_value(42); // 不会执行} catch (...) {// 捕获所有异常,并传递给promiseprom.set_exception(std::current_exception());}
}int main() {std::promise<int> prom;std::future<int> fut = prom.get_future();std::thread worker(might_throw, std::ref(prom));try {// get() 会重新抛出worker线程中设置的异常int value = fut.get();std::cout << "Main: Got value: " << value << std::endl;} catch (const std::exception& e) {// 在主线程捕获并处理来自工作线程的异常!std::cerr << "Main: Caught an exception from the worker: " << e.what() << std::endl;}worker.join();return 0;
}

输出:

Worker: I might throw an exception!
Main: Caught an exception from the worker: Oops from worker thread!

这种方式极大地简化了异步编程中的错误处理逻辑。

4.2 共享的Future:std::shared_future

std::future 有一个重要限制:get() 只能调用一次,它是移动only的。这意味着只能有一个消费者。

但有时,你可能会有多个消费者等待同一个结果。比如,一个计算结果出来后,多个线程都需要用它进行下一步处理。这时就需要 std::shared_future

  • 它可以被拷贝。多个 shared_future 对象可以关联到同一个共享状态。
  • 每个 shared_future 都可以调用 get(),并且可以多次调用。

创建方式:

  1. std::future 转换(移动)而来。这是最常见的方式。

    std::promise<int> p;
    std::future<int> f = p.get_future();
    // 将 future 移动转换为 shared_future
    std::shared_future<int> sf = f.share(); // 注意:此时 f 变为无效!
    // 或者直接初始化:
    // std::shared_future<int> sf(p.get_future());
    
  2. 通过 std::async 直接返回(C++14 起,std::async 的返回类型可以自动推导为 std::shared_future,如果启动策略是 deferred 的话?不,主要还是要自己转换)。更通用的方法是使用 share()

使用示例:

void consumer(const std::shared_future<int>& sf, int id) {// 每个消费者都可以get()int result = sf.get(); // get() 是 const 的,可以多次调用std::cout << "Consumer " << id << " got: " << result << std::endl;
}int main() {std::promise<int> p;// 先获取普通的future,然后转换为shared_futureauto sf = p.get_future().share(); // sf 的类型是 std::shared_future<int>// 启动多个消费者线程,传递sf的拷贝std::thread c1(consumer, sf, 1);std::thread c2(consumer, sf, 2);std::thread c3(consumer, sf, 3);// 生产者设置值std::this_thread::sleep_for(std::chrono::seconds(1));p.set_value(100);c1.join();c2.join();c3.join();return 0;
}
// 可能的输出:
// Consumer 1 got: 100
// Consumer 2 got: 100
// Consumer 3 got: 100

第五章:实战应用场景

5.1 场景一:并行计算(Map)

将一个大任务分解成多个独立的小任务,并行计算,最后汇总结果。

#include <iostream>
#include <vector>
#include <future>
#include <numeric>
#include <chrono>// 计算一个子向量部分和
int parallel_sum(const std::vector<int>& v, int start, int end) {int sum = 0;for (int i = start; i < end; ++i) {sum += v[i];}return sum;
}int main() {std::vector<int> numbers(100000000, 1); // 1亿个1,总和应该是1亿// 获取硬件支持的并发线程数unsigned int num_threads = std::thread::hardware_concurrency();std::cout << "Using " << num_threads << " threads.\n";std::vector<std::future<int>> futures;int chunk_size = numbers.size() / num_threads;auto start_time = std::chrono::high_resolution_clock::now();// 启动异步任务计算每一部分的和for (int i = 0; i < num_threads; ++i) {int start = i * chunk_size;int end = (i == num_threads - 1) ? numbers.size() : start + chunk_size;// 启动异步任务,并将返回的future存入vectorfutures.push_back(std::async(std::launch::async, parallel_sum, std::ref(numbers), start, end));}// 等待所有任务完成,并收集结果int total_sum = 0;for (auto& fut : futures) {total_sum += fut.get(); // 这里会等待每个任务完成}auto end_time = std::chrono::high_resolution_clock::now();auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);std::cout << "Parallel sum: " << total_sum << std::endl;std::cout << "Time taken: " << duration.count() << " ms" << std::endl;// 对比单线程版本start_time = std::chrono::high_resolution_clock::now();int single_sum = std::accumulate(numbers.begin(), numbers.end(), 0);end_time = std::chrono::high_resolution_clock::now();duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);std::cout << "Single-threaded sum: " << single_sum << std::endl;std::cout << "Time taken: " << duration.count() << " ms" << std::endl;return 0;
}
5.2 场景二:异步I/O操作

模拟一个需要等待的网络请求或文件读取。

std::string fetch_data_from_server(const std::string& query) {// 模拟网络延迟std::this_thread::sleep_for(std::chrono::seconds(3));// 模拟返回结果return "Response to '" + query + "'";
}int main() {std::cout << "Main: Initiating async data fetch...\n";auto data_future = std::async(std::launch::async, fetch_data_from_server, "SELECT * FROM users");std::cout << "Main: Waiting for data. I can do other things now...\n";for (int i = 0; i < 5; ++i) {std::cout << "Main: Doing other work (" << i << ")...\n";std::this_thread::sleep_for(std::chrono::milliseconds(500));}// 现在真的需要数据了std::cout << "Main: Now I need the data.\n";try {std::string result = data_future.get();std::cout << "Main: Received: " << result << std::endl;} catch (...) {std::cout << "Main: Failed to get data from server.\n";}return 0;
}
5.3 场景三:实现一个简单的状态轮询

使用 wait_for 实现非阻塞的轮询。

int main() {auto slow_task = []() {std::this_thread::sleep_for(std::chrono::seconds(10));return true;};auto fut = std::async(std::launch::async, slow_task);// 主循环,可以处理其他事件while (true) {// 等待100毫秒看看任务完成没auto status = fut.wait_for(std::chrono::milliseconds(100));if (status == std::future_status::ready) {std::cout << "\nTask is done! Result: " << std::boolalpha << fut.get() << std::endl;break;} else {// 任务还没完成,干点别的std::cout << "." << std::flush; // 打印一个点表示还在等待// 这里可以处理UI事件、网络请求等}}return 0;
}

最终总结

std::futurestd::promise 是 C++11 为异步编程带来的革命性工具,它们提供了一种类型安全异常安全优雅的线程间通信和同步机制。

  • 核心模型Promise(发货方)Future(收货方) 通过共享状态一对一配对。
  • 核心操作promise.set_value() / set_exception()future.get()
  • 便捷工具std::async(快递公司) 封装了 thread + promise 的常见模式,让启动异步任务变得极其简单。
  • 高级特性
    • 异常传递: 通过 set_exceptionget() 重新抛出,完美处理跨线程错误。
    • 多消费者: 使用 std::shared_future
    • 非阻塞等待: 使用 wait_for() / wait_until() 实现轮询和超时控制。

最佳实践:

  1. 默认使用 std::async: 除非有特殊需求(如需要精细控制线程),否则优先使用它。
  2. 显式指定启动策略: 使用 std::launch::async 确保真正的并发。
  3. 警惕 future.get() 的阻塞: 在UI线程或关键线程中调用它要小心,以免卡住界面。
  4. 善用 std::shared_future: 用于多消费者场景。
  5. 拥抱异常传递: 不要在线程内部吞掉异常,让调用方有机会处理。

掌握 future/promise 模型,意味着你掌握了现代 C++ 异步编程的“道”,而不仅仅是“术”。它将让你编写的并发代码更健壮、更清晰、更易于维护。


文章转载自:

http://f6DeAoOi.tbbhj.cn
http://gVZgkqDx.tbbhj.cn
http://40HoZXCs.tbbhj.cn
http://ZKSzmdHz.tbbhj.cn
http://Wf5bvpiI.tbbhj.cn
http://5HRdaV9b.tbbhj.cn
http://Evu7NfKt.tbbhj.cn
http://66LbQbYA.tbbhj.cn
http://hNgULW9Z.tbbhj.cn
http://izLRtrmm.tbbhj.cn
http://PKYJ9Zuk.tbbhj.cn
http://ae4EOmn0.tbbhj.cn
http://spAop4NJ.tbbhj.cn
http://h8TTwv6n.tbbhj.cn
http://KN94Hhxf.tbbhj.cn
http://0t365Re7.tbbhj.cn
http://6mw95HH9.tbbhj.cn
http://40QsZrw3.tbbhj.cn
http://COqp3nvt.tbbhj.cn
http://CPLf09VO.tbbhj.cn
http://p38hnq7R.tbbhj.cn
http://zxqLemTD.tbbhj.cn
http://VRcVeovW.tbbhj.cn
http://PADIrX9C.tbbhj.cn
http://s3KoMSvL.tbbhj.cn
http://JnuWypg7.tbbhj.cn
http://AOl8pn96.tbbhj.cn
http://ulCNMiYD.tbbhj.cn
http://yjj7XLXf.tbbhj.cn
http://jKxYhRHc.tbbhj.cn
http://www.dtcms.com/a/385902.html

相关文章:

  • VScode插件Remote-SSH
  • 挣脱网络桎梏:CapsWriter-Offline+cpolar,让高效输入不受网络牵绊
  • Qt地图软件开发/GIS软件开发组件/修改天地图支持21级别/离线瓦片地图
  • Kafka 跨集群地理复制(Geo-Replication)
  • ​​[硬件电路-235]:双极型三极管、MOS场效应管、IGBT管异同比较
  • Spark专题-第二部分:Spark SQL 入门(1)-Spark SQL 简介
  • Spark源码学习分享之submit提交流程(1)
  • 5、二叉树-小堆
  • 技术奇点爆发周:2025 年 9 月科技突破全景扫描
  • 从Dubbo到SpringCloud Alibaba:大型项目迁移的实战手册(含成本分析与踩坑全记录)(一)
  • 【算法】C语言多组输入输出模板
  • 测试 Docker 的实时恢复功能
  • 系统中间件与云虚拟化-serverless-基于阿里云函数计算的云工作流CloudFlow设计与体验
  • springboot netty 客户端网络编程入门与实战
  • TCP/IP模型
  • 智慧用电安全管理系统的核心优势
  • flutter结合NestedScrollView+TabBar实现嵌套滚动
  • 基于定制开发开源AI智能名片S2B2C商城小程序的社群团购线上平台搭建研究
  • DEDECMS 小程序插件简介 2.0全新上线
  • 详解 Spring Boot 单元测试:@SpringBootTest 与 JUnit 依赖配置及环境注入
  • JMeter元件简介与JMeter测试计划
  • 陪诊小程序:让医疗关怀触手可及
  • n*n矩阵方程组Ax=b,使用Eigen矩阵库常用解法介绍
  • IvorySQL 4.6:DocumentDB+FerretDB 实现 MongoDB 兼容部署指南
  • UART,IIC,SPI总线(通信协议)
  • 记录一次小程序请求报错:600001
  • 光谱相机的新兴领域应用
  • GO学习记录十——发包
  • OpenLayers数据源集成 -- 章节十六:XML图层详解:OpenStreetMap数据的动态加载与智能样式渲染方案
  • vector 模拟实现 4 大痛点解析:从 memcpy 到模板嵌套的实战方案