C++ 与 推理流水线:基于 C++ 协程实现预处理、模型计算与后处理的高并发异步编排架构

news2026/4/8 23:22:01
尊敬的各位技术同行大家好。今天我们聚焦一个在现代人工智能应用中至关重要的议题如何构建高性能、高并发的推理流水线。随着深度学习模型在各行各业的广泛部署将这些模型高效地集成到生产系统中实现低延迟、高吞吐量的推理服务成为了我们面临的核心挑战。特别是对于C开发者而言如何利用语言的强大能力来驾驭复杂的异步并发编程优化资源利用率是需要深入探讨的。本次讲座我们将深入探讨“C 与 推理流水线基于 C 协程实现预处理、模型计算与后处理的高并发异步编排架构”。我们将剖析推理流水线的本质审视传统并发方法的局限并最终揭示C协程如何为我们提供一种优雅而强大的解决方案以构建高效、可维护且高度并发的异步编排系统。推理流水线的本质与挑战一个典型的深度学习推理任务并非一个单一的、原子性的操作它通常由多个连续的阶段组成形成一个“推理流水线”。这些阶段各司其职且具有不同的计算特性预处理 (Preprocessing)功能: 负责接收原始输入数据如图像文件、视频流、传感器数据、文本进行解码、裁剪、缩放、归一化、格式转换等操作使其符合模型输入的特定要求。计算特性: 通常是CPU密集型任务可能涉及文件I/O、图像处理库如OpenCV、字符串操作等。模型计算 (Model Computation)功能: 将预处理后的数据送入深度学习模型如TensorFlow Lite、ONNX Runtime、TensorRT进行实际的推理计算生成模型的原始输出。计算特性: 通常是GPU密集型任务如果模型部署在GPU上也可能是CPU密集型如果部署在CPU上。涉及大量矩阵乘法和卷积操作。后处理 (Postprocessing)功能: 对模型输出进行解析、解码、阈值处理、非极大值抑制NMS、结果格式化等将其转换为业务逻辑所需的最终可理解的输出如检测框坐标、分类标签、语义分割掩码。计算特性: 通常是CPU密集型任务可能涉及复杂的逻辑判断、数据结构操作、结果序列化等。推理流水线面临的核心挑战端到端延迟 (End-to-End Latency): 从接收原始输入到返回最终结果的总时间。对于实时应用如自动驾驶、在线推荐低延迟至关重要。吞吐量 (Throughput): 单位时间内可以处理的请求数量。对于高并发服务高吞吐量是衡量性能的关键指标。资源利用率 (Resource Utilization): 确保CPU、GPU、内存等硬件资源得到充分利用避免资源空闲或瓶颈。异构计算协调: 协调CPU和GPU之间的数据传输和任务切换最小化同步开销。I/O瓶颈: 数据加载、网络传输等I/O操作可能成为整个流水线的瓶颈。复杂性管理: 随着流水线阶段增多同步、错误处理、取消等机制变得异常复杂。理想情况下我们希望这些阶段能够并行执行例如当一个请求的模型计算正在GPU上进行时另一个请求的预处理可以在CPU上同时进行从而实现计算和I/O的重叠最大化硬件利用率。传统并发编程方法的局限性在C中实现并发和异步编程有多种传统方法但它们在面对推理流水线的特定挑战时往往暴露出一些局限性。同步/顺序执行 (Synchronous/Sequential Execution):模式: 每个请求从预处理到模型计算再到后处理完全按顺序执行。优点: 简单易懂易于实现和调试。缺点: 极低的吞吐量和高延迟。CPU和GPU在大部分时间处于空闲状态无法重叠计算。例如当GPU忙于推理时CPU空闲当CPU忙于预处理时GPU空闲。线程池 (Thread Pools):模式: 使用一个固定大小的线程池来处理请求。每个请求可能由一个或多个任务提交到线程池执行。优点: 比单线程效率高减少了线程创建/销毁的开销。缺点:上下文切换开销: 如果为每个请求分配一个线程来处理整个流水线当并发请求数量非常大时线程数量可能导致频繁的上下文切换降低性能。资源浪费: 线程在等待I/O或GPU完成时会阻塞但仍然持有操作系统资源无法被其他任务有效利用。异构任务调度复杂: 难以优雅地将CPU密集型任务调度到CPU线程GPU密集型任务调度到GPU上下文并在它们之间高效切换。同步复杂性: 跨线程数据传递和状态同步需要显式的锁、条件变量等机制容易引入死锁、竞态条件。异步I/O与回调 (Asynchronous I/O with Callbacks):模式: 任务提交后立即返回当操作完成时通过回调函数通知。优点: 不阻塞线程理论上可以实现高并发。缺点:回调地狱 (Callback Hell): 嵌套的回调函数使得代码难以阅读、理解和维护尤其是在复杂的流水线中。错误处理复杂: 异常在回调链中传递困难。逻辑分散: 业务逻辑被拆分到多个不连续的回调函数中难以追踪整个请求的处理流程。下表简要对比这些方法特性/方法同步/顺序执行线程池异步I/O与回调并发度低中高高吞吐量低中高高延迟高中低理论上资源利用率低中高代码复杂度低中同步机制高回调地狱错误处理可维护性高中低适用场景简单、低负载适度并发、同构任务高并发、I/O密集、复杂逻辑难以驾驭很明显我们需要一种既能实现高并发和高资源利用率又能保持代码清晰、易于维护的编程范式。这就是C协程的用武之地。C协程异步编程的范式革新C20引入的协程Coroutines是异步编程领域的一项重大革新。它允许函数在执行过程中被暂停suspend和恢复resume从而使得异步代码能够以接近同步代码的顺序和直观性来编写。什么是协程协程是一种特殊的函数它可以在执行过程中将控制权交还给调用者或调度器并在稍后从上次暂停的地方继续执行。与线程不同协程的切换是由程序员在用户空间显式控制的没有操作系统的介入因此上下文切换的开销极低。协程是“栈less”stackless的意味着它的状态局部变量、指令指针等被存储在堆上而不是在函数调用栈上这使得成千上万个协程可以以极低的内存开销同时存在。C协程的关键概念co_await: 用于暂停当前协程的执行并等待一个“可等待对象”Awaitable完成。当可等待对象完成时协程会从co_await点继续执行。co_return: 用于从协程中返回一个值并结束协程的执行。co_yield: 在此推理流水线场景中较少使用但作为协程的一部分它用于生成一系列值类似于生成器。可等待对象 (Awaitable): 任何实现了特定接口await_ready()、await_suspend()、await_resume()的对象。当协程co_await一个可等待对象时调度器会调用这些接口来决定是否暂停协程以及如何暂停和恢复。Promise类型 (Promise Type): 这是协程的核心机制之一。每个协程都有一个关联的Promise类型它定义了协程的生命周期管理、返回值类型、异常处理以及与外部世界的交互方式。协程的返回值类型通常是一个TaskT或FutureT之类的自定义类型是通过Promise类型构建的。协程句柄 (Coroutine Handle):std::coroutine_handle是一个轻量级对象用于引用和操作一个协程的实例例如恢复它。协程对于推理流水线的优势高并发低开销: 大量协程可以并发运行而不会像线程那样产生高昂的上下文切换和内存开销。顺序化异步代码:co_await关键字使得异步操作看起来像同步调用极大地简化了代码逻辑避免了“回调地狱”。高效的资源利用: 当协程等待I/O或GPU任务完成时它会暂停并交出控制权其所在的线程可以去执行其他协程从而充分利用CPU核心。自然地处理异构任务: 通过在co_await点切换到不同的执行上下文例如CPU线程池到GPU调度器可以无缝地协调CPU和GPU任务。易于错误处理:try-catch块可以像处理同步代码一样处理协程中的异常无需复杂的异常传递机制。基于C协程的异步编排架构设计核心思想是将推理流水线的每个阶段预处理、模型计算、后处理封装成一个异步操作即一个可以co_await的可等待对象或一个返回协程任务的函数。一个更高层次的协程负责编排这些阶段按照业务逻辑顺序驱动整个流水线。架构组件任务类型 (Task Type): 用于封装协程的返回值和状态。类似于std::future但专为协程设计。执行器 (Executor): 负责将协程的恢复操作调度到特定的线程或执行上下文。我们将需要CPU执行器: 用于CPU密集型任务预处理、后处理。通常基于一个线程池实现。GPU执行器: 用于GPU密集型任务模型计算。可能是一个管理GPU设备上下文和流的专用线程或者是一个将任务提交到GPU异步队列的接口。I/O执行器: 用于I/O密集型任务数据加载。可以与CPU执行器合并或独立。上下文切换器 (Context Switcher Awaitable): 一个特殊的awaitable用于在协程中显式地将执行权从一个执行器切换到另一个执行器。数据流与内存管理数据在流水线阶段之间传递时应尽量减少复制。可以使用std::unique_ptr或std::shared_ptr进行所有权转移或共享。对于大型数据如图像张量可以考虑零拷贝技术例如如果GPU支持直接访问CPU内存或使用统一内存。错误处理协程内部的异常可以像普通函数一样被try-catch捕获。如果异常未被捕获它将通过协程的promise_type::unhandled_exception()方法传递给协程的调用者最终可以通过Task类型获取并重新抛出。C协程实现细节与代码示例为了演示上述架构我们需要一些基础的协程支持代码。1. 核心任务类型TaskTTaskT是协程的返回类型它允许我们co_await协程的完成并获取结果。这是一个简化的实现省略了大部分错误处理和高级特性以保持简洁。#include coroutine #include iostream #include thread #include queue #include vector #include future // For std::packaged_task and std::future in executor // --- 1. TaskT 定义 --- // TaskT 作为协程的返回类型类似 std::futureT templatetypename T struct Task { struct promise_type; using handle_type std::coroutine_handlepromise_type; handle_type handle; Task(handle_type h) : handle(h) {} Task(Task other) noexcept : handle(std::exchange(other.handle, nullptr)) {} Task operator(Task other) noexcept { if (this ! other) { if (handle) handle.destroy(); handle std::exchange(other.handle, nullptr); } return *this; } ~Task() { if (handle) handle.destroy(); } // Awaiter interface for co_await bool await_ready() const noexcept { // If the result is already available, no need to suspend. // For simplicity, we always suspend here to demonstrate scheduling. return false; } void await_suspend(std::coroutine_handle awaiting_coroutine) noexcept { // Store the awaiting coroutine handle in the promise handle.promise().set_continuation(awaiting_coroutine); // At this point, the current coroutine (Task) is running // We let it run. When it finishes, it will resume awaiting_coroutine. } T await_resume() { if (handle.promise().exception_ptr) { std::rethrow_exception(handle.promise().exception_ptr); } return std::move(handle.promise().value); } // Promise type definition struct promise_type { T value; std::exception_ptr exception_ptr; std::coroutine_handle continuation; // To store the awaiting coroutine // Called when the coroutine is first created Task get_return_object() { return Task{handle_type::from_promise(*this)}; } // Called before the coroutine body starts std::suspend_always initial_suspend() { return {}; } // Called after the coroutine body finishes (or co_return) std::suspend_always final_suspend() noexcept { // When the current coroutine finishes, resume the awaiting coroutine if (continuation) { continuation.resume(); } return {}; } // Called on co_return T void return_value(T v) { value std::move(v); } // Called if an unhandled exception propagates out of the coroutine body void unhandled_exception() { exception_ptr std::current_exception(); } void set_continuation(std::coroutine_handle h) { continuation h; } }; }; // Specialization for Taskvoid template struct Taskvoid { struct promise_type; using handle_type std::coroutine_handlepromise_type; handle_type handle; Task(handle_type h) : handle(h) {} Task(Task other) noexcept : handle(std::exchange(other.handle, nullptr)) {} Task operator(Task other) no_except { if (this ! other) { if (handle) handle.destroy(); handle std::exchange(other.handle, nullptr); } return *this; } ~Task() { if (handle) handle.destroy(); } bool await_ready() const noexcept { return false; } void await_suspend(std::coroutine_handle awaiting_coroutine) noexcept { handle.promise().set_continuation(awaiting_coroutine); } void await_resume() { if (handle.promise().exception_ptr) { std::rethrow_exception(handle.promise().exception_ptr); } } struct promise_type { std::exception_ptr exception_ptr; std::coroutine_handle continuation; Task get_return_object() { return Task{handle_type::from_promise(*this)}; } std::suspend_always initial_suspend() { return {}; } std::suspend_always final_suspend() noexcept { if (continuation) { continuation.resume(); } return {}; } void return_void() {} void unhandled_exception() { exception_ptr std::current_exception(); } void set_continuation(std::coroutine_handle h) { continuation h; } }; };2. 执行器 (Executor) 和上下文切换器我们将实现一个简单的线程池作为CPU执行器并定义一个PostToExecutor可等待对象用于将当前协程的恢复操作提交给指定的执行器。// --- 2. Executor 和 PostToExecutor --- // 抽象执行器接口 class Executor { public: virtual void execute(std::functionvoid() task) 0; virtual ~Executor() default; }; // 线程池执行器 (CPU Executor) class ThreadPoolExecutor : public Executor { std::vectorstd::thread workers; std::queuestd::functionvoid() tasks; std::mutex queue_mutex; std::condition_variable condition; bool stop; public: ThreadPoolExecutor(size_t threads) : stop(false) { for (size_t i 0; i threads; i) { workers.emplace_back([this] { while (true) { std::functionvoid() task; { std::unique_lockstd::mutex lock(this-queue_mutex); this-condition.wait(lock, [this] { return this-stop || !this-tasks.empty(); }); if (this-stop this-tasks.empty()) return; task std::move(this-tasks.front()); this-tasks.pop(); } task(); } }); } } ~ThreadPoolExecutor() { { std::unique_lockstd::mutex lock(queue_mutex); stop true; } condition.notify_all(); for (std::thread worker : workers) { worker.join(); } } void execute(std::functionvoid() task) override { { std::unique_lockstd::mutex lock(queue_mutex); if (stop) throw std::runtime_error(enqueue on stopped ThreadPoolExecutor); tasks.emplace(std::move(task)); } condition.notify_one(); } }; // 上下文切换器将协程的恢复操作调度到指定执行器 struct PostToExecutor { Executor exec; bool await_ready() const noexcept { return false; } // Always suspend and post void await_suspend(std::coroutine_handle h) { exec.execute([h]() { h.resume(); // Schedule the coroutine to resume on the executors thread }); } void await_resume() const noexcept {} };3. 模拟数据结构和全局执行器// --- 3. 模拟数据结构和全局执行器 --- // 模拟推理流水线的数据类型 struct InferenceInput { int id; std::string data; }; struct PreprocessedData { int id; std::string processed_data; }; struct ModelOutput { int id; std::vectorfloat features; }; struct InferenceResult { int id; std::string final_result; }; // 全局执行器实例 ThreadPoolExecutor cpu_executor(4); // 4个CPU线程用于预处理和后处理 ThreadPoolExecutor gpu_executor(1); // 1个“GPU”线程用于模拟模型计算 (实际中会是GPU管理线程) // 模拟一个简单的事件循环来驱动协程 class EventLoop { std::queueTaskInferenceResult active_tasks; public: void submit(TaskInferenceResult task) { active_tasks.push(std::move(task)); } void run_until_complete() { // In a real scenario, this would be more sophisticated, polling for completion // or using an I/O event loop. For this example, we just wait for submitted tasks. while (!active_tasks.empty()) { // For simplicity, we just get the result, which will block until the Task completes. // In a real non-blocking event loop, youd use a mechanism to check if handle.done() is true // and then call await_resume() (or get_result()) without blocking. // Our Tasks final_suspend will resume the awaiting coroutine, but here we run the orchestrator directly. // So we need a way to poll. For demo, we just block here. std::cout EventLoop: Waiting for a task to complete... std::endl; try { active_tasks.front().await_resume(); // This will block until the task is done (via final_suspend resuming) } catch (const std::exception e) { std::cerr EventLoop: Task failed with exception: e.what() std::endl; } active_tasks.pop(); } std::cout EventLoop: All tasks completed. std::endl; } }; EventLoop main_event_loop; // Main event loop注意上述EventLoop::run_until_complete()中的await_resume()调用是阻塞的。在生产环境中一个真正的非阻塞事件循环会维护一个待处理协程的列表并定期检查它们是否准备好恢复而不是阻塞等待单个任务。但为了演示核心的协程调度和上下文切换这种简化是可接受的。4. 推理流水线阶段协程现在我们可以定义推理流水线的各个阶段为异步协程函数。// --- 4. 推理流水线阶段协程 --- // 预处理阶段 TaskPreprocessedData preprocess_async(InferenceInput input) { co_await PostToExecutor{cpu_executor}; // 切换到CPU线程池执行 std::cout [ input.id ] Preprocessing on CPU thread std::this_thread::get_id() std::endl; // 模拟耗时CPU工作 std::this_thread::sleep_for(std::chrono::milliseconds(50)); PreprocessedData output {input.id, processed_ input.data}; co_return output; } // 模型计算阶段 TaskModelOutput infer_async(PreprocessedData preprocessed_data) { co_await PostToExecutor{gpu_executor}; // 切换到GPU执行器线程 (模拟) std::cout [ preprocessed_data.id ] Inferring model on GPU thread std::this_thread::get_id() std::endl; // 模拟耗时GPU工作 std::this_thread::sleep_for(std::chrono::milliseconds(100)); ModelOutput output {preprocessed_data.id, {0.1f, 0.2f, 0.7f}}; // 模拟模型输出 co_return output; } // 后处理阶段 TaskInferenceResult postprocess_async(ModelOutput model_output) { co_await PostToExecutor{cpu_executor}; // 切换回CPU线程池执行 std::cout [ model_output.id ] Postprocessing on CPU thread std::this_thread::get_id() std::endl; // 模拟耗时CPU工作 std::this_thread::sleep_for(std::chrono::milliseconds(30)); InferenceResult output {model_output.id, final_result_for_ std::to_string(model_output.id)}; co_return output; }5. 流水线编排协程最后我们定义一个顶层协程来编排整个推理流水线。// --- 5. 流水线编排协程 --- TaskInferenceResult run_inference_pipeline(InferenceInput input) { std::cout [ input.id ] Pipeline started on thread std::this_thread::get_id() std::endl; // 阶段1: 预处理 PreprocessedData preprocessed co_await preprocess_async(std::move(input)); // 阶段2: 模型计算 ModelOutput output co_await infer_async(std::move(preprocessed)); // 阶段3: 后处理 InferenceResult result co_await postprocess_async(std::move(output)); std::cout [ result.id ] Pipeline finished on thread std::this_thread::get_id() std::endl; co_return result; }6. 演示主函数在main函数中我们创建多个推理请求并提交给事件循环。// --- 6. 主函数演示 --- int main() { std::cout Main thread ID: std::this_thread::get_id() std::endl; // 提交多个推理请求 for (int i 0; i 5; i) { main_event_loop.submit(run_inference_pipeline({i, raw_data_ std::to_string(i)})); } // 运行事件循环直到所有任务完成 main_event_loop.run_until_complete(); std::cout All inference pipelines have completed. std::endl; // 确保执行器在main函数结束前销毁让所有线程退出 // cpu_executor和gpu_executor是全局变量会在main结束后自动销毁 return 0; }运行输出示例 (部分):Main thread ID: 139750030063424 [0] Pipeline started on thread 139750030063424 [1] Pipeline started on thread 139750030063424 [2] Pipeline started on thread 139750030063424 [3] Pipeline started on thread 139750030063424 [4] Pipeline started on thread 139750030063424 EventLoop: Waiting for a task to complete... [0] Preprocessing on CPU thread 139750021678848 [1] Preprocessing on CPU thread 139750013284096 [2] Preprocessing on CPU thread 139749996500480 [3] Preprocessing on CPU thread 139750021678848 [4] Preprocessing on CPU thread 139750013284096 [0] Inferring model on GPU thread 139750004895232 [1] Inferring model on GPU thread 139750004895232 [2] Inferring model on GPU thread 139750004895232 [3] Inferring model on GPU thread 139750004895232 [4] Inferring model on GPU thread 139750004895232 [0] Postprocessing on CPU thread 139750021678848 [1] Postprocessing on CPU thread 139750013284096 [2] Postprocessing on CPU thread 139749996500480 [3] Postprocessing on CPU thread 139750021678848 [4] Postprocessing on CPU thread 139750013284096 [0] Pipeline finished on thread 139750021678848 [1] Pipeline finished on thread 139750013284096 [2] Pipeline finished on thread 139749996500480 [3] Pipeline finished on thread 139750021678848 [4] Pipeline finished on thread 139750013284096 EventLoop: All tasks completed. All inference pipelines have completed.从输出可以看出多个请求的预处理任务在不同的CPU线程上并发执行随后模型计算任务在模拟的GPU线程上并发执行尽管只有一个GPU线程但协程能高效地切换最后后处理任务再次回到CPU线程池并发执行。整个过程不同阶段的计算被重叠提升了效率。高级考量与最佳实践批量推理 (Batching)对于GPU批量推理可以显著提高吞吐量。协程可以优雅地实现动态批处理当有足够的请求或达到时间阈值时将它们组合成一个批次提交给模型计算协程。例如一个专门的协程可以收集传入的PreprocessedData直到批次大小满足或等待超时然后co_await一个infer_batch_async(std::vectorPreprocessedData batch)。资源管理与池化:模型实例池: 多个模型计算协程可能需要访问同一个模型实例或者需要多个模型实例以提高并发。可以使用对象池来管理模型实例。GPU内存管理: 对于TensorRT等高性能推理引擎需要手动管理GPU内存。协程可以与自定义的内存分配器集成。数据缓冲区: 预处理和后处理阶段可能需要大的数据缓冲区。池化这些缓冲区可以减少内存分配/释放的开销。错误处理与取消:协程中的异常可以被try-catch捕获。未捕获的异常会存储在Task的promise_type中并在await_resume()时重新抛出。取消机制Cancellation是异步编程的复杂部分。C协程本身不提供内置的取消支持需要手动实现例如通过传递一个std::stop_token并在co_await点检查它。与现有异步库集成:许多现有的C异步库如Boost.Asio、libuv提供了自己的事件循环和异步操作。可以通过创建自定义的awaitable来桥接这些库的异步操作使其能够在协程中使用co_await。性能监控与追踪:在每个co_await点前后记录时间戳和线程ID可以构建一个详细的性能追踪系统识别瓶颈。使用专门的性能分析工具如Intel VTune, NVIDIA Nsight来深入分析CPU和GPU的利用率。零拷贝数据传输:在CPU和GPU之间传输数据通常是性能瓶颈。利用cudaHostRegister进行固定内存pinned memory可以加速CPU-GPU传输。如果硬件和驱动支持甚至可以尝试统一内存Unified Memory或GPU Direct RDMA等技术实现零拷贝。C协程架构的显著优势采用C协程构建推理流水线架构带来了多方面的显著优势极致的并发效率: 协程的轻量级特性和用户空间调度使得系统能够以极低的开销支持成千上万个并发请求远超传统线程模型。高效的资源利用: 通过在I/O等待或GPU计算期间暂停协程并切换到其他任务协程确保CPU和GPU等异构资源能够持续保持忙碌状态避免空闲。代码清晰与可维护性:co_await使得异步、并发的代码能够以近乎同步的顺序逻辑编写极大地提升了代码的可读性、可理解性和可维护性告别了“回调地狱”。灵活的调度策略: 通过自定义执行器和PostToExecutor可以根据任务类型CPU密集、GPU密集、I/O密集将协程的恢复操作精确调度到最合适的执行上下文。简化错误处理: 协程内部的异常处理与同步代码类似简化了异步流程中的错误传播和处理。易于扩展: 当需要增加新的流水线阶段或调整现有阶段时只需修改相应的协程函数而不会影响整个架构的稳定性。面临的挑战与应对尽管C协程带来了诸多益处但在实际应用中也存在一些挑战学习曲线: C协程是C20的新特性其底层机制promise type, awaitable相对复杂需要一定的学习投入。调试复杂性: 异步和并发问题本身就难以调试协程的暂停和恢复机制可能使问题追踪变得更具挑战性。但现代调试器如GDB的Python扩展正在逐步增强对协程的可见性。生态系统成熟度: 相较于Go或Rust等语言C协程的生态系统仍在发展中可能需要开发者自行实现或整合更多基础组件如更完善的调度器、取消机制。兼容性: 确保编译环境支持C20标准和协程特性。应对这些挑战的最佳实践包括深入理解协程的底层原理从简单用例开始逐步迭代利用日志和追踪系统辅助调试以及积极关注C标准库和第三方库的发展以利用更成熟的工具和框架。C协程为我们构建高性能、高并发的推理流水线提供了前所未有的强大工具。通过精心设计的异步编排架构我们能够充分利用现代硬件的异构计算能力显著提升AI推理服务的性能、效率与可维护性。这不仅是技术上的飞跃更是将AI模型从实验室推向实际生产环境的关键一步。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2493941.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…

网络编程(Modbus进阶)

思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析

1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具&#xff0c;该工具基于TUN接口实现其功能&#xff0c;利用反向TCP/TLS连接建立一条隐蔽的通信信道&#xff0c;支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式&#xff0c;适应复杂网…

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?

编辑&#xff1a;陈萍萍的公主一点人工一点智能 未来机器人的大脑&#xff1a;如何用神经网络模拟器实现更智能的决策&#xff1f;RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战&#xff0c;在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…

Linux应用开发之网络套接字编程(实例篇)

服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…