C++ 并发性能优化实战:提升多线程应用的效率与稳定性

在这里插入图片描述
🧑 博主简介:CSDN博客专家、CSDN平台优质创作者,获得2024年博客之星荣誉证书,高级开发工程师,数学专业,拥有高级工程师证书;擅长C/C++、C#等开发语言,熟悉Java常用开发技术,能熟练应用常用数据库SQL server,Oracle,mysql,postgresql等进行开发应用,熟悉DICOM医学影像及DICOM协议,业余时间自学JavaScript,Vue,qt,python等,具备多种混合语言开发能力。撰写博客分享知识,致力于帮助编程爱好者共同进步。欢迎关注、交流及合作,提供技术支持与解决方案。
技术合作请加本人wx(注明来自csdn):xt20160813

在这里插入图片描述

C++ 并发性能优化实战:提升多线程应用的效率与稳定性

在现代软件开发中,多核处理器的普及使得并发编程成为提升应用性能的关键手段。C++ 作为一门高性能语言,提供了丰富的并发支持,但不当的使用同样可能导致性能瓶颈甚至程序错误。本文将深入探讨 C++ 并发性能优化的策略和实践,通过详细的示例,帮助开发者在项目中有效识别并解决并发带来的性能问题。

目录

  1. 并发编程基础
    • 什么是并发与并行
    • C++ 中的并发支持
  2. 识别并发性能瓶颈
    • 常见的并发性能问题
    • 性能分析工具
  3. 优化策略
    • 1. 减少锁的粒度与使用
    • 2. 使用无锁编程
    • 3. 线程池的应用
    • 4. 数据局部性优化
    • 5. 避免竞态条件与死锁
    • 6. 任务划分与负载均衡
    • 7. 内存管理与缓存优化
  4. 实战案例:高性能并行图像处理
    • 初始实现
    • 优化步骤
    • 优化后的实现
  5. 最佳实践与总结
  6. 参考资料

并发编程基础

什么是并发与并行

并发(Concurrency)指的是在同一时间段内,多个任务交替执行,以提高系统的吞吐量和资源利用率。而并行(Parallelism)则是指在同一时刻,多个任务同时执行,以缩短任务完成时间。虽然两者密切相关,但并发更强调任务的管理与调度,并行则强调同时执行。

C++中的并发支持

自 C++11 起,C++ 标准库引入了一系列并发支持,包括线程(std::thread)、互斥锁(std::mutex)、条件变量(std::condition_variable)等。此外,C++17 引入了并行算法,C++20 更进一步增强了协程(Coroutines)等特性。这些工具为开发者提供了构建高性能并发应用的基础。


识别并发性能瓶颈

在优化并发程序之前,首先需要识别性能瓶颈。以下是常见的并发性能问题和识别方法。

常见的并发性能问题

  1. 过度锁竞争:多个线程频繁争用同一把锁,导致线程阻塞和上下文切换,降低系统吞吐量。
  2. 任务划分不合理:任务粒度过细或过粗,导致线程管理开销增加或资源利用率降低。
  3. 线程过多或过少:线程数量不匹配硬件资源,导致 CPU 核心空闲或频繁上下文切换。
  4. 缓存不友好:数据结构和访问模式导致缓存未命中率高,增加内存访问延迟。
  5. 死锁与竞态条件:不当的同步机制导致线程间相互等待或数据不一致。

性能分析工具

使用性能分析工具可以有效发现并发程序中的性能瓶颈。以下是几种常用的工具:

  • Perf:Linux 系统下的强大性能分析工具,适用于 CPU 性能监控和分析。
  • Valgrind:特别是 Callgrind 模块,可以进行详细的代码性能分析。
  • Intel VTune Profiler:提供全面的性能分析,支持多种硬件架构。
  • Visual Studio Profiler:集成在 Visual Studio 中,适用于 Windows 平台的性能分析。
  • Google PerfTools:包括 CPU Profiler,可用于分析程序的 CPU 使用情况。

示例:使用 Perf 进行分析

  1. 编译程序时开启调试信息和优化选项

    g++ -O2 -g -o my_app my_app.cpp -pthread
    
  2. 运行 Perf 进行性能分析

    perf record -g ./my_app
    
  3. 生成报告

    perf report
    

通过分析报告,可以识别出程序中消耗 CPU 时间较多的函数和代码段,进而定位性能瓶颈。


优化策略

针对上述常见的并发性能问题,以下是几种有效的优化策略。

1. 减少锁的粒度与使用

锁粒度指的是锁定的资源范围。锁粒度越细,允许的并发度越高,但管理锁的开销也可能增加。

优化方法:

  • 细化锁粒度:将一个大锁拆分为多个小锁,锁定更具体的资源。
  • 使用读写锁:对于读多写少的场景,使用共享锁(读锁)和独占锁(写锁)来提高并发度。
  • 避免锁嵌套:尽量减少多个锁的嵌套使用,避免死锁风险。

示例:细化锁粒度

#include <vector>
#include <mutex>
#include <thread>class ThreadSafeVector {
public:void push_back(int value) {std::lock_guard<std::mutex> lock(mutex_);data_.push_back(value);}int get(size_t index) const {std::lock_guard<std::mutex> lock(mutex_);return data_.at(index);}private:std::vector<int> data_;mutable std::mutex mutex_;
};

优化:

将整个容器的锁拆分为多个段锁,每个段锁保护容器的一部分。

#include <vector>
#include <mutex>
#include <thread>
#include <shared_mutex>class SegmentedThreadSafeVector {
public:void push_back(int value) {std::unique_lock<std::mutex> lock(mutex_);data_.push_back(value);}int get(size_t index) const {std::unique_lock<std::mutex> lock(mutex_);return data_.at(index);}private:std::vector<int> data_;mutable std::mutex mutex_;
};

尽管在这个简单示例中锁粒度优化效果有限,但在复杂数据结构中,细化锁粒度可以显著提升并发性能。

2. 使用无锁编程

无锁编程通过原子操作和无锁数据结构,避免使用互斥锁,从而减少锁竞争和上下文切换的开销。

优化方法:

  • 原子操作:使用 std::atomic 提供的原子操作,确保线程安全的同时避免锁的开销。
  • 无锁数据结构:采用无锁队列、无锁栈等数据结构,提高并发性能。

示例:使用原子变量

#include <atomic>
#include <thread>
#include <vector>
#include <iostream>std::atomic<int> counter(0);void increment(int num_iterations) {for(int i = 0; i < num_iterations; ++i) {counter.fetch_add(1, std::memory_order_relaxed);}
}int main() {const int num_threads = 4;const int iterations = 1000000;std::vector<std::thread> threads;for(int i = 0; i < num_threads; ++i) {threads.emplace_back(increment, iterations);}for(auto& t : threads) {t.join();}std::cout << "Final counter value: " << counter.load() << std::endl;return 0;
}

说明:

通过使用 std::atomic<int>,多个线程可以安全地对 counter 进行递增操作,无需互斥锁,显著提升性能。

3. 线程池的应用

频繁创建和销毁线程会带来较大的开销。使用线程池可以重用线程资源,减少线程管理的开销,提高任务处理效率。

优化方法:

  • 固定大小线程池:预先创建一定数量的线程,处理任务队列中的任务。
  • 动态调整线程池:根据任务负载动态调整线程池的大小,优化资源利用。

示例:简单线程池实现

#include <vector>
#include <thread>
#include <queue>
#include <functional>
#include <mutex>
#include <condition_variable>
#include <future>
#include <iostream>class ThreadPool {
public:ThreadPool(size_t num_threads);~ThreadPool();// 提交任务template<class F, class... Args>auto enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type>;private:// 工作者线程std::vector<std::thread> workers_;// 任务队列std::queue<std::function<void()>> tasks_;// 同步std::mutex queue_mutex_;std::condition_variable condition_;bool stop_;
};// 构造函数
ThreadPool::ThreadPool(size_t num_threads) : stop_(false) {for(size_t i = 0; i < num_threads; ++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(); });if(this->stop_ && this->tasks_.empty())return;task = std::move(this->tasks_.front());this->tasks_.pop();}// 执行任务task();}});}
}// 析构函数
ThreadPool::~ThreadPool() {{ std::unique_lock<std::mutex> lock(queue_mutex_);stop_ = true;}condition_.notify_all();for(std::thread &worker: workers_)worker.join();
}// 提交任务
template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type> {using return_type = typename std::result_of<F(Args...)>::type;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");tasks_.emplace([task]() { (*task)(); });}condition_.notify_one();return res;
}// 使用示例
int main() {ThreadPool pool(4);std::vector<std::future<int>> results;// 提交任务for(int i = 0; i < 8; ++i) {results.emplace_back(pool.enqueue([i]() -> int {std::this_thread::sleep_for(std::chrono::milliseconds(100));return i*i;}));}// 获取结果for(auto && result: results)std::cout << result.get() << ' ';std::cout << std::endl;return 0;
}

说明:

通过线程池,多个任务可以复用固定数量的线程执行,避免了频繁创建和销毁线程的开销,提升了并发性能。

4. 数据局部性优化

数据局部性指的是数据在内存中的分布对缓存性能的影响。在并发程序中,优化数据的缓存局部性,可以减少缓存未命中率,提高内存访问速度。

优化方法:

  • 结构化数据存储:使用结构体数组(SoA)而非数组结构体(AoS),提高数据的连续性。
  • 避免伪共享:不同线程访问的数据不应位于同一个缓存行,避免伪共享导致的性能下降。

示例:避免伪共享

#include <atomic>
#include <thread>
#include <vector>
#include <iostream>// 伪共享示例
struct SharedData {std::atomic<int> counter1;std::atomic<int> counter2;
};int main() {SharedData data;data.counter1 = 0;data.counter2 = 0;auto increment1 = [&data]() {for(int i = 0; i < 1000000; ++i) {data.counter1.fetch_add(1, std::memory_order_relaxed);}};auto increment2 = [&data]() {for(int i = 0; i < 1000000; ++i) {data.counter2.fetch_add(1, std::memory_order_relaxed);}};std::thread t1(increment1);std::thread t2(increment2);t1.join();t2.join();std::cout << "Counter1: " << data.counter1 << "\nCounter2: " << data.counter2 << std::endl;return 0;
}

优化:

通过填充无用数据避免 counter1counter2 位于同一缓存行。

#include <atomic>
#include <thread>
#include <vector>
#include <iostream>// 避免伪共享的结构
struct SharedData {alignas(64) std::atomic<int> counter1;alignas(64) std::atomic<int> counter2;
};int main() {SharedData data;data.counter1 = 0;data.counter2 = 0;auto increment1 = [&data]() {for(int i = 0; i < 1000000; ++i) {data.counter1.fetch_add(1, std::memory_order_relaxed);}};auto increment2 = [&data]() {for(int i = 0; i < 1000000; ++i) {data.counter2.fetch_add(1, std::memory_order_relaxed);}};std::thread t1(increment1);std::thread t2(increment2);t1.join();t2.join();std::cout << "Counter1: " << data.counter1 << "\nCounter2: " << data.counter2 << std::endl;return 0;
}

说明:

通过使用 alignas(64),确保每个计数器位于不同的缓存行,避免多个线程同时访问相邻数据导致的伪共享问题。

5. 避免竞态条件与死锁

竞态条件和死锁不仅会导致程序错误,还会显著影响性能。良好的同步机制设计可以避免这些问题。

优化方法:

  • 锁的获取顺序一致:确保多个线程获取多个锁的顺序一致,避免循环等待导致的死锁。
  • 使用更高层次的同步机制:如使用条件变量、读写锁等,减少锁的争用。

示例:避免死锁的锁获取顺序

#include <mutex>
#include <thread>
#include <iostream>std::mutex mutex1;
std::mutex mutex2;void thread_a() {std::lock_guard<std::mutex> lock1(mutex1);std::lock_guard<std::mutex> lock2(mutex2);std::cout << "Thread A acquired both locks\n";
}void thread_b() {std::lock_guard<std::mutex> lock1(mutex1);std::lock_guard<std::mutex> lock2(mutex2);std::cout << "Thread B acquired both locks\n";
}int main() {std::thread t1(thread_a);std::thread t2(thread_b);t1.join();t2.join();return 0;
}

说明:

通过确保所有线程以相同的顺序获取锁,可以避免死锁的发生。

6. 任务划分与负载均衡

合理的任务划分和负载均衡可以确保所有线程都能充分利用 CPU 资源,避免某些线程空闲而其他线程过载。

优化方法:

  • 动态任务调度:使用工作窃取(Work Stealing)等策略,动态调整各线程的任务负载。
  • 合理划分任务粒度:任务粒度应适中,过细增加调度开销,过粗导致负载不均。

示例:使用线程池进行动态任务调度

在前述线程池示例中,任务被动态分配到空闲线程上,实现了负载均衡。

7. 内存管理与缓存优化

高效的内存管理和缓存优化可以显著减少内存访问延迟,提升并发程序的整体性能。

优化方法:

  • 内存对齐:确保数据结构按照缓存行对齐,减少缓存未命中率。
  • 预分配内存:提前分配必要的内存,避免在高并发时进行频繁内存分配。
  • 使用缓存友好的数据结构:如数组和连续内存布局的数据结构,提升缓存局部性。

示例:使用内存池进行内存管理

#include <memory>
#include <vector>
#include <iostream>template<typename T>
class MemoryPool {
public:MemoryPool(size_t size = 1024) {allocate_block(size);}~MemoryPool() {for(auto block : blocks_)::operator delete[](block);}T* allocate() {if(free_list_.empty()) {allocate_block(block_size_);}T* obj = free_list_.back();free_list_.pop_back();return obj;}void deallocate(T* obj) {free_list_.push_back(obj);}private:void allocate_block(size_t size) {T* new_block = static_cast<T*>(::operator new[](size * sizeof(T)));blocks_.push_back(new_block);for(size_t i = 0; i < size; ++i)free_list_.push_back(new_block + i);}std::vector<T*> blocks_;std::vector<T*> free_list_;size_t block_size_ = 1024;
};// 使用示例
struct MyObject {int data;// ...
};int main() {MemoryPool<MyObject> pool;// 分配对象MyObject* obj1 = pool.allocate();obj1->data = 42;// 使用对象std::cout << "Object data: " << obj1->data << std::endl;// 释放对象pool.deallocate(obj1);return 0;
}

说明:

通过内存池管理对象的分配和释放,减少了频繁的堆分配操作,提高了内存管理效率,特别适用于高并发环境下的大量对象创建与销毁。


实战案例:高性能并行图像处理

为了更直观地展示上述优化策略的应用,以下将通过一个高性能并行图像处理的案例,详细说明优化过程。

初始实现

假设有一个简单的图像处理程序,需要对一幅大图像的每个像素进行亮度调整。

#include <vector>
#include <thread>
#include <mutex>
#include <iostream>struct Pixel {unsigned char r, g, b;
};class Image {
public:Image(size_t width, size_t height) : width_(width), height_(height), pixels_(width * height) {}Pixel& at(size_t x, size_t y) { return pixels_[y * width_ + x]; }size_t width() const { return width_; }size_t height() const { return height_; }private:size_t width_;size_t height_;std::vector<Pixel> pixels_;
};void adjust_brightness(Image& img, size_t start_y, size_t end_y, int brightness) {for(size_t y = start_y; y < end_y; ++y) {for(size_t x = 0; x < img.width(); ++x) {Pixel& p = img.at(x, y);p.r = std::min(static_cast<int>(p.r) + brightness, 255);p.g = std::min(static_cast<int>(p.g) + brightness, 255);p.b = std::min(static_cast<int>(p.b) + brightness, 255);}}
}int main() {size_t width = 4000;size_t height = 3000;Image img(width, height);// 初始化图像数据(简化)for(auto& p : img.pixels_) {p.r = p.g = p.b = 100;}int brightness = 50;size_t num_threads = std::thread::hardware_concurrency();std::vector<std::thread> threads;size_t rows_per_thread = height / num_threads;for(size_t i = 0; i < num_threads; ++i) {size_t start_y = i * rows_per_thread;size_t end_y = (i == num_threads - 1) ? height : (i + 1) * rows_per_thread;threads.emplace_back(adjust_brightness, std::ref(img), start_y, end_y, brightness);}for(auto& t : threads) {t.join();}std::cout << "Brightness adjustment completed.\n";return 0;
}

潜在问题:

  1. 锁的使用:在当前实现中,没有显式的锁,但如果在调整亮度时需要修改共享数据结构,可能引入锁。
  2. 数据局部性:访问图像像素的顺序若不连续,可能影响缓存命中率。
  3. 线程管理开销:频繁创建和销毁线程可能带来额外开销。

优化步骤

针对上述问题,可以进行以下优化:

  1. 使用线程池:避免频繁创建和销毁线程,通过线程池管理线程资源。
  2. 提高数据局部性:确保线程访问的内存范围连续,提高缓存命中率。
  3. 减少内存访问冲突:确保每个线程操作独立的图像区域,避免数据竞争。

优化后的实现

#include <vector>
#include <thread>
#include <mutex>
#include <iostream>
#include <future>
#include <algorithm>// 保持 Pixel 和 Image 结构不变
struct Pixel {unsigned char r, g, b;
};class Image {
public:Image(size_t width, size_t height) : width_(width), height_(height), pixels_(width * height) {}Pixel& at(size_t x, size_t y) { return pixels_[y * width_ + x]; }size_t width() const { return width_; }size_t height() const { return height_; }std::vector<Pixel>& get_pixels() { return pixels_; }private:size_t width_;size_t height_;std::vector<Pixel> pixels_;
};// 线程池类(简化)
class ThreadPool {
public:ThreadPool(size_t num_threads);~ThreadPool();template<class F>auto enqueue(F&& f) -> std::future<void>;private:std::vector<std::thread> workers_;std::queue<std::function<void()>> tasks_;std::mutex queue_mutex_;std::condition_variable condition_;bool stop_;
};// 线程池实现
ThreadPool::ThreadPool(size_t num_threads) : stop_(false) {for(size_t i = 0; i < num_threads; ++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(); });if(this->stop_ && this->tasks_.empty())return;task = std::move(this->tasks_.front());this->tasks_.pop();}task();}});}
}ThreadPool::~ThreadPool() {{ std::unique_lock<std::mutex> lock(queue_mutex_);stop_ = true;}condition_.notify_all();for(std::thread &worker: workers_)worker.join();
}template<class F>
auto ThreadPool::enqueue(F&& f) -> std::future<void> {auto task = std::make_shared< std::packaged_task<void()> >(std::forward<F>(f));std::future<void> res = task->get_future();{ std::unique_lock<std::mutex> lock(queue_mutex_);if(stop_)throw std::runtime_error("enqueue on stopped ThreadPool");tasks_.emplace([task]() { (*task)(); });}condition_.notify_one();return res;
}// 调整亮度函数
void adjust_brightness(Image& img, size_t start_y, size_t end_y, int brightness) {for(size_t y = start_y; y < end_y; ++y) {for(size_t x = 0; x < img.width(); ++x) {Pixel& p = img.at(x, y);p.r = std::min(static_cast<int>(p.r) + brightness, 255);p.g = std::min(static_cast<int>(p.g) + brightness, 255);p.b = std::min(static_cast<int>(p.b) + brightness, 255);}}
}int main() {size_t width = 4000;size_t height = 3000;Image img(width, height);// 初始化图像数据(简化)std::fill(img.get_pixels().begin(), img.get_pixels().end(), Pixel{100, 100, 100});int brightness = 50;size_t num_threads = std::thread::hardware_concurrency();ThreadPool pool(num_threads);std::vector< std::future<void> > futures;size_t rows_per_task = height / (num_threads * 4); // 分成更多任务for(size_t y = 0; y < height; y += rows_per_task) {size_t end_y = std::min(y + rows_per_task, height);futures.emplace_back(pool.enqueue([&img, y, end_y, brightness]() {adjust_brightness(img, y, end_y, brightness);}));}// 等待所有任务完成for(auto &fut : futures)fut.get();std::cout << "Brightness adjustment completed.\n";return 0;
}

优化效果分析:

  1. 使用线程池:通过线程池复用线程资源,减少了频繁创建和销毁线程的开销,提高了任务调度效率。
  2. 任务划分更细:将图像划分为更多的任务,使得线程可以更均匀地分配工作,避免某些线程过载而其他线程空闲。
  3. 数据局部性提升:每个任务处理连续的图像行,提升了内存访问的连续性和缓存命中率。
  4. 避免数据竞争:每个任务处理独立的图像区域,无需额外的同步机制,减少锁的使用和争用。

最佳实践与总结

通过上述优化策略和实战案例,我们可以总结出以下 C++ 并发性能优化的最佳实践:

  1. 合理使用锁:尽量减少锁的使用,细化锁的粒度,采用读写锁等高级同步机制,避免锁竞争和死锁。
  2. 采用无锁编程:利用原子操作和无锁数据结构,提升并发性能,减少上下文切换的开销。
  3. 使用线程池:避免频繁创建和销毁线程,通过线程池管理线程资源,提高任务处理效率。
  4. 优化数据局部性:确保数据访问的连续性和缓存友好性,减少缓存未命中率,提升内存访问速度。
  5. 任务划分合理:合理划分任务粒度,确保所有线程负载均衡,避免资源闲置和过载。
  6. 避免数据竞争:设计线程间数据访问的独立性,减少共享数据和同步需求,避免竞态条件。
  7. 进行性能分析:使用性能分析工具定位并发程序中的性能瓶颈,针对性地进行优化。
  8. 遵循并发模型:采用成熟的并发编程模型和设计模式,如生产者-消费者模式、任务并行等,提高代码的可维护性和性能。

总结:

C++ 并发编程在提升应用性能方面具有巨大潜力,但同时也带来了复杂性和挑战。通过理解并发基础、识别性能瓶颈、应用有效的优化策略,开发者可以构建高效、稳定的并发应用。最重要的是,持续进行性能分析和优化,确保应用在不同负载和环境下都能表现出色。


参考资料

  • C++ 并发编程官方文档
  • C++ Concurrency in Action - Anthony Williams
  • Effective Modern C++ - Scott Meyers
  • Intel VTune Profiler 文档
  • Google PerfTools

标签

C++、并发编程、性能优化、多线程、线程池、无锁编程

版权声明

本文版权归作者所有,未经允许,请勿转载。

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

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

相关文章

Python----计算机视觉处理(Opencv:道路检测之车道线拟合)

完整版&#xff1a; Python----计算机视觉处理&#xff08;Opencv:道路检测完整版&#xff1a;透视变换&#xff0c;提取车道线&#xff0c;车道线拟合&#xff0c;车道线显示&#xff09; 一、获取左右车道线的原始位置 导入模块 import cv2 import numpy as np from matplot…

优选算法的妙思之流:分治——归并专题

专栏&#xff1a;算法的魔法世界 个人主页&#xff1a;手握风云 目录 一、归并排序 二、例题讲解 2.1. 排序数组 2.2. 交易逆序对的总数 2.3. 计算右侧小于当前元素的个数 2.4. 翻转对 一、归并排序 归并排序也是采用了分治的思想&#xff0c;将数组划分为多个长度为1的子…

C语言查漏补缺:基础篇

1.原理 C语言是一门编译型计算机语言&#xff0c;要编写C代码&#xff0c;C源代码文本文件本身无法直接执行&#xff0c;必须通过编译器翻译和链接器的链接&#xff0c;生成二进制的可执行文件&#xff0c;然后才能执行。这里的二进制的可执行文件就是我们最终要形成的可执行程…

TPS入门DAY02 服务器篇

1.创建空白插件 2.导入在线子系统以及在线steam子系统库 MultiplayerSessions.uplugin MultiplayerSessions.Build.cs 3.创建游戏实例以及初始化会话创建流程 创建会话需要的函数&#xff0c;委托&#xff0c;委托绑定的回调&#xff0c;在线子系统接口绑定某一个委托的控制其…

产品经理课程

原型工具 一、土耳其机器人 这个说法来源于 1770 年出现的一个骗局&#xff0c;一个叫沃尔夫冈冯肯佩伦&#xff08;Wolfgang von Kempelen&#xff09;的人为了取悦奥地利女皇玛丽娅特蕾莎&#xff08;Maria Theresia&#xff09;&#xff0c;“制造”了一个会下国际象棋的机…

nginx中的limit_req 和 limit_conn

在 Nginx 中&#xff0c;limit_req 和 limit_conn 是两个用于限制客户端请求的指令&#xff0c;它们分别用于限制请求速率和并发连接数。 limit_req limit_req 用于限制请求速率&#xff0c;防止客户端发送过多请求影响服务器性能。它通过 limit_req_zone 指令定义一个共享内存…

基于winform的串口调试助手

目录 一、串口助手界面设计 1.1 串口配置 1.2 接收配置 1.3 发送配置 1.4 接收窗口和发送窗口 1.5 状态显示窗口 1.6 串口通讯控件 二、程序编写 2.1 端口号自动识别并显示在端口号下拉框 功能说明&#xff1a; 2.2 波特率下拉框显示 2.3 数据位下拉框显示 2.4 校…

Docker基础2

如需转载&#xff0c;标记出处 本次我们将下载一个 Docker 镜像&#xff0c;从镜像中启动容器 上一章&#xff0c;安装 Docker 时&#xff0c;获得两个主要组件&#xff1a; Docker 客户端 Docker 守护进程&#xff08;有时称为“服务器”或“引擎”&#xff09; 守护进程实…

Rocketmq2

一、生产者端防丢失 1. 发送方式选择 同步发送&#xff1a;使用 send() 方法&#xff0c;等待 Broker 确认响应&#xff08;SendResult&#xff09;&#xff0c;确保消息已成功发送。异步发送&#xff1a;使用 sendAsync() 方法并设置回调函数&#xff0c;处理发送成功 / 失败…

RabbitMQ详解,RabbitMQ是什么?架构是怎样的?

目录 一,RabbitMQ是什么? 二,RabbitMQ架构 2.1 首先我们来看下RabbitMQ里面的心概念Queue是什么? 2.2 交换器Exchange 2.3 RabbitMQ是什么? 2.4 重点看下优先级队列是什么? 三,RabbitMQ集群 3.1 普通集群模式 3.2 镜像队列集群 一,RabbitMQ是什么? 假设我们程序…

【一步步开发AI运动APP】六、运动计时计数能调用

之前我们为您分享了【一步步开发AI运动小程序】开发系列博文&#xff0c;通过该系列博文&#xff0c;很多开发者开发出了很多精美的AI健身、线上运动赛事、AI学生体测、美体、康复锻炼等应用场景的AI运动小程序&#xff1b;为了帮助开发者继续深耕AI运动领域市场&#xff0c;今…

MySQL——DQL的多表查询

一、交叉连接 标准语法&#xff1a;select * from 表1 cross join 表2 where 表1.公共列 表2.公共列; 简单语法&#xff1a;select * from 表1 , 表2 where 表1.公共列 表2.公共列; 公共列&#xff1a;两张表具有相同含义的列&#xff0c;不是列名一样。 …

【Linux内核】如何更加优雅阅读Linux内核源码(vscode)

1. 前言 因为已经习惯在Ubuntu下进行嵌入式工作开发&#xff0c;但Linux源码在Source Insight下进行阅读&#xff0c;一直很苦恼Linux/Windows来回切换的开发方式&#xff0c;当前发现可以通过 vscode clangd(扩展组件) 方式进行更好的内核源码阅读。 2. 环境 操作系统&…

21.OpenCV获取图像轮廓信息

OpenCV获取图像轮廓信息 在计算机视觉领域&#xff0c;识别和分析图像中的对象形状是一项基本任务。OpenCV 库提供了一个强大的工具——轮廓检测&#xff08;Contour Detection&#xff09;&#xff0c;它能够帮助我们精确地定位对象的边界。这篇博文将带你入门 OpenCV 的轮廓…

LETTERS(DFS)

【题目描述】 给出一个rowcolrowcol的大写字母矩阵&#xff0c;一开始的位置为左上角&#xff0c;你可以向上下左右四个方向移动&#xff0c;并且不能移向曾经经过的字母。问最多可以经过几个字母。 【输入】 第一行&#xff0c;输入字母矩阵行数RR和列数SS&#xff0c;1≤R,S≤…

Day2-2:前端项目uniapp壁纸实战

再在wallpaper新建一个目录components 在components下新建组件common-title 记得点击创建同名目录 在index加 <view class"select"><common-title></common-title></view> 图片换了下&#xff0c;原来的有点丑&#xff0c;图片可按自己喜欢…

其他 vector 操作详解(四十)

介绍 除去向 vector 添加元素&#xff08;如 push_back&#xff09;之外&#xff0c;vector 还提供了许多其他操作&#xff0c;这些操作大多与 string 的操作类似。通过掌握这些操作&#xff0c;我们可以方便地查询、修改和比较 vector 中的元素&#xff0c;从而构建灵活、高效…

【Leetcode 每日一题】368. 最大整除子集

问题背景 给你一个由 无重复 正整数组成的集合 n u m s nums nums&#xff0c;请你找出并返回其中最大的整除子集 a n s w e r answer answer&#xff0c;子集中每一元素对 ( a n s w e r [ i ] , a n s w e r [ j ] ) (answer[i], answer[j]) (answer[i],answer[j]) 都应当…

python基础-13-处理excel电子表格

文章目录 【README】【13】处理Excel电子表格【13.1】Excel文档【13.2】安装openpyxl模块【13.3】读取Excel文档【13.3.1】使用openpyxl模块打开excel文档【13.3.2】从工作簿取得工作表【13.3.3】从工作表sheet获取单元格cell【13.3.5】从表中获取行和列【13.3.6】工作簿、工作…

ABS函数c++

简介&#xff1a; abs 函数用于计算一个数的绝对值&#xff0c;在 C 中它继承自 C 语言的标准库&#xff0c;其历史可以追溯到早期的 C 语言发展历程&#xff0c;以下是详细介绍&#xff1a; 早期编程语言的需求 在计算机编程的早期阶段&#xff0c;处理数学运算就是一项基本…