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

C++学习:六个月从基础到就业——多线程编程:std::thread基础

C++学习:六个月从基础到就业——多线程编程:std::thread基础

本文是我C++学习之旅系列的第五十四篇技术文章,也是第四阶段"并发与高级主题"的第一篇,介绍C++11引入的多线程编程基础知识。查看完整系列目录了解更多内容。

引言

在现代计算机科学中,多线程编程已成为提高程序性能的关键技术。随着多核处理器的普及,有效利用并行计算能力变得日益重要。C++11标准引入了线程支持库,使C++开发者能够直接在语言层面进行多线程编程,无需依赖操作系统特定的API或第三方库。

本文将深入介绍C++11的std::thread类的基础知识,包括线程的创建、管理、参数传递、异常处理以及线程同步的基本概念。通过本文的学习,你将能够编写基本的多线程C++程序,为后续深入学习并发编程打下基础。

目录

  • 多线程编程:std::thread基础
    • 引言
    • 目录
    • 多线程编程基础
      • 并发与并行
      • 多线程的优势
      • 多线程的挑战
    • std::thread类
      • 线程的创建
      • 函数对象与Lambda表达式
      • 成员函数作为线程函数
    • 线程的参数传递
      • 基本参数传递
      • 引用参数的传递
      • 移动语义与线程
    • 线程的生命周期管理
      • join操作
      • detach操作
      • 可连接状态检查
    • 线程标识符与线程本地存储
      • 获取线程ID
      • 线程本地存储
    • 线程与异常处理
      • 线程函数中的异常
      • RAII与线程管理
    • 实际应用案例
      • 并行计算示例
      • 后台任务处理
      • 用户界面响应性改进
    • 常见问题与注意事项
      • 竞态条件
      • 死锁与活锁
      • 线程数量的选择
      • 调试多线程程序
    • 总结

多线程编程基础

并发与并行

在讨论多线程编程之前,我们需要理解两个基本概念:并发(Concurrency)和并行(Parallelism)。

并发是指程序的不同部分可以"同时"执行,但实际上可能是通过时间片轮转在单核处理器上交替执行。并发是一个程序结构概念,强调的是任务的独立性。

并行是指程序的不同部分真正同时执行,通常在多核处理器上。并行是一个执行概念,强调的是性能的提升。

#include <iostream>
#include <thread>void printMessage(const std::string& message) {std::cout << message << std::endl;
}int main() {// 创建两个线程,在多核处理器上可能并行执行std::thread t1(printMessage, "Hello from thread 1!");std::thread t2(printMessage, "Hello from thread 2!");// 等待线程完成t1.join();t2.join();return 0;
}

多线程的优势

多线程编程具有以下主要优势:

  1. 提高性能:通过并行处理,多线程可以更有效地利用多核处理器,加速计算密集型任务。

  2. 响应性增强:在用户界面应用中,使用独立线程处理耗时操作可以保持界面响应迅速。

  3. 资源利用率提高:当一个线程等待I/O操作完成时,其他线程可以继续执行,提高整体资源利用率。

  4. 简化复杂问题:某些问题在多线程模型下更容易表达和理解。

多线程的挑战

尽管多线程编程带来诸多优势,但也面临以下挑战:

  1. 同步问题:多线程访问共享资源需要适当同步,否则会导致数据竞争和不确定行为。

  2. 死锁风险:不当的线程同步可能导致死锁,使程序永久卡住。

  3. 调试困难:多线程程序的执行具有不确定性,使得调试更加复杂。

  4. 可伸缩性问题:创建过多线程会导致线程切换开销增加,反而降低性能。

  5. 设计复杂性:多线程程序的设计和实现通常比单线程程序更复杂。

std::thread类

C++11引入的std::thread类是C++标准库中进行多线程编程的核心组件。它封装了操作系统的线程API,提供了平台无关的线程管理功能。

线程的创建

创建线程的最基本方式是构造一个std::thread对象,并传递一个可调用对象(函数、函数对象或lambda表达式)作为线程函数:

#include <iostream>
#include <thread>// 普通函数作为线程函数
void hello() {std::cout << "Hello from thread!" << std::endl;
}int main() {// 创建线程,执行hello函数std::thread t(hello);// 等待线程完成t.join();std::cout << "Main thread continues execution." << std::endl;return 0;
}

线程创建后会立即开始执行,与主线程并发运行。在上面的例子中,主线程通过调用join()方法等待新线程完成。

函数对象与Lambda表达式

除了普通函数外,我们还可以使用函数对象和Lambda表达式作为线程函数:

#include <iostream>
#include <thread>// 函数对象
class Task {
public:void operator()() const {std::cout << "Task is executing in thread." << std::endl;}
};int main() {// 使用函数对象Task task;std::thread t1(task);t1.join();// 使用临时函数对象(需要额外的括号避免语法解析歧义)std::thread t2((Task()));  // 额外的括号t2.join();// 使用Lambda表达式std::thread t3([]() {std::cout << "Lambda is executing in thread." << std::endl;});t3.join();return 0;
}

注意,当使用临时函数对象时,需要额外的括号避免"最令人恐惧的解析"(most vexing parse)问题,否则编译器会将std::thread t2(Task());解释为一个函数声明,而不是对象定义。

成员函数作为线程函数

线程函数也可以是类的成员函数,但需要提供一个对象实例:

#include <iostream>
#include <thread>class Counter {
private:int count = 0;public:void increment(int times) {for (int i = 0; i < times; ++i) {++count;}std::cout << "Final count: " << count << std::endl;}
};int main() {Counter counter;// 创建线程执行成员函数std::thread t(&Counter::increment, &counter, 1000000);t.join();return 0;
}

在上面的例子中,我们传递了成员函数指针、对象指针和函数参数给std::thread构造函数。

线程的参数传递

基本参数传递

向线程函数传递参数非常简单,只需在std::thread构造函数中的线程函数参数后添加额外的参数:

#include <iostream>
#include <thread>
#include <string>void printMessage(const std::string& message, int count) {for (int i = 0; i < count; ++i) {std::cout << message << " " << i << std::endl;}
}int main() {// 传递两个参数给线程函数std::thread t(printMessage, "Message", 5);t.join();return 0;
}

需要注意的是,参数是以值传递的方式传给线程函数的,即使函数参数声明为引用类型。

引用参数的传递

如果要传递引用,需要使用std::refstd::cref包装器:

#include <iostream>
#include <thread>
#include <string>
#include <functional>  // 为std::ref和std::crefvoid modifyString(std::string& str) {str += " - Modified by thread";
}int main() {std::string message = "Original message";// 使用std::ref传递引用std::thread t(modifyString, std::ref(message));t.join();std::cout << "After thread: " << message << std::endl;return 0;
}

不使用std::ref的话,线程函数会收到message的一个副本,而不是引用,修改不会影响原始变量。

移动语义与线程

C++11的移动语义在线程参数传递中非常有用,尤其是对于不可复制但可移动的对象:

#include <iostream>
#include <thread>
#include <memory>
#include <vector>void processUniquePtr(std::unique_ptr<int> ptr) {// 处理独占指针*ptr += 10;std::cout << "Value in thread: " << *ptr << std::endl;
}int main() {// 创建一个独占指针auto ptr = std::make_unique<int>(42);// 使用std::move转移所有权到线程std::thread t(processUniquePtr, std::move(ptr));// 此时ptr为nullptrif (ptr == nullptr) {std::cout << "Original pointer is now nullptr" << std::endl;}t.join();return 0;
}

在上面的例子中,我们使用std::moveunique_ptr的所有权转移到线程函数中。这是必要的,因为unique_ptr不可复制,只能移动。

线程的生命周期管理

join操作

join()方法用于等待线程完成。调用线程会阻塞,直到目标线程执行完毕:

#include <iostream>
#include <thread>
#include <chrono>void longTask() {// 模拟耗时任务std::cout << "Long task started" << std::endl;std::this_thread::sleep_for(std::chrono::seconds(2));std::cout << "Long task completed" << std::endl;
}int main() {std::cout << "Main thread starting" << std::endl;std::thread t(longTask);std::cout << "Main thread waiting for worker thread..." << std::endl;t.join();  // 主线程阻塞,等待t完成std::cout << "Worker thread has completed. Main thread continues." << std::endl;return 0;
}

需要注意的是,一个线程只能被join()一次。尝试多次join()同一个线程会导致未定义行为,通常会抛出异常。

detach操作

detach()方法用于将线程与std::thread对象分离。分离后,线程会在后台独立运行,不再受std::thread对象的控制:

#include <iostream>
#include <thread>
#include <chrono>void backgroundTask() {std::this_thread::sleep_for(std::chrono::seconds(2));std::cout << "Background task completed" << std::endl;
}int main() {{std::cout << "Creating a detached thread" << std::endl;std::thread t(backgroundTask);t.detach();  // 线程在后台运行,不等待它完成std::cout << "Thread detached, main thread continues..." << std::endl;}  // t销毁,但线程继续在后台运行// 睡眠足够长的时间,确保能看到后台线程的输出std::this_thread::sleep_for(std::chrono::seconds(3));std::cout << "Main thread ending" << std::endl;return 0;
}

使用detach()时需要特别小心:

  • 分离后无法再获取线程的控制权
  • 主线程结束时,即使后台线程还在运行,程序也会终止
  • 要确保线程访问的资源在线程运行期间保持有效

可连接状态检查

线程对象有两种状态:可连接(joinable)和不可连接(non-joinable)。只有处于可连接状态的线程才能被join()detach()

#include <iostream>
#include <thread>void simpleTask() {std::cout << "Task executing..." << std::endl;
}int main() {// 默认构造的线程对象是不可连接的std::thread t1;std::cout << "t1 joinable: " << t1.joinable() << std::endl;  // 输出:0// 初始化后的线程是可连接的std::thread t2(simpleTask);std::cout << "t2 joinable: " << t2.joinable() << std::endl;  // 输出:1// join后线程变为不可连接t2.join();std::cout << "After join, t2 joinable: " << t2.joinable() << std::endl;  // 输出:0// 创建另一个线程std::thread t3(simpleTask);std::cout << "t3 joinable: " << t3.joinable() << std::endl;  // 输出:1// detach后线程变为不可连接t3.detach();std::cout << "After detach, t3 joinable: " << t3.joinable() << std::endl;  // 输出:0return 0;
}

以下情况下线程是不可连接的:

  • 默认构造的std::thread对象
  • 已经被join()detach()的线程
  • 通过移动操作转移了所有权的线程

线程标识符与线程本地存储

获取线程ID

每个线程都有一个唯一的标识符,可以通过get_id()方法或std::this_thread::get_id()获取:

#include <iostream>
#include <thread>
#include <sstream>// 打印当前线程ID的辅助函数
std::string getThreadIdString() {std::ostringstream oss;oss << std::this_thread::get_id();return oss.str();
}void threadFunction() {std::cout << "Thread function running in thread " << getThreadIdString() << std::endl;
}int main() {std::cout << "Main thread ID: " << getThreadIdString() << std::endl;std::thread t(threadFunction);std::cout << "Created thread with ID: " << t.get_id() << std::endl;t.join();// join后,线程ID变为默认值std::cout << "After join, thread ID: " << t.get_id() << std::endl;return 0;
}

线程ID可用于识别和区分不同的线程,在调试和日志记录中特别有用。

线程本地存储

线程本地存储(Thread Local Storage, TLS)允许每个线程拥有变量的私有副本。C++11引入了thread_local关键字来声明线程局部变量:

#include <iostream>
#include <thread>
#include <string>// 线程局部变量
thread_local int counter = 0;
thread_local std::string threadName = "Unknown";void incrementCounter(const std::string& name) {threadName = name;  // 设置此线程的名称for (int i = 0; i < 5; ++i) {++counter;  // 递增此线程的计数器std::cout << "Thread " << threadName << ": counter = " << counter << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(10));}
}int main() {// 在主线程中访问threadName = "Main";std::cout << "Initial counter in main thread: " << counter << std::endl;// 创建两个线程,各自拥有counter的副本std::thread t1(incrementCounter, "Thread1");std::thread t2(incrementCounter, "Thread2");// 在主线程中递增counterfor (int i = 0; i < 3; ++i) {++counter;std::cout << "Thread " << threadName << ": counter = " << counter << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(10));}t1.join();t2.join();// 主线程中的counter不受其他线程影响std::cout << "Final counter in main thread: " << counter << std::endl;return 0;
}

线程本地存储的使用场景:

  • 线程安全的单例模式
  • 每线程缓存
  • 线程特定的状态信息
  • 避免使用互斥量的简单线程隔离

线程与异常处理

线程函数中的异常

线程函数中抛出的异常不会传播到创建线程的上下文中。如果不在线程内部捕获异常,程序将调用std::terminate终止:

#include <iostream>
#include <thread>
#include <stdexcept>void threadWithException() {try {std::cout << "Thread starting..." << std::endl;throw std::runtime_error("Exception in thread!");}catch (const std::exception& e) {std::cout << "Caught exception in thread: " << e.what() << std::endl;}
}void threadWithUncaughtException() {std::cout << "Thread starting..." << std::endl;throw std::runtime_error("Uncaught exception in thread!");// 这个异常不会被捕获,程序将终止
}int main() {// 正确处理异常的线程std::thread t1(threadWithException);t1.join();std::cout << "After first thread" << std::endl;// 包含未捕获异常的线程 - 会导致程序终止// std::thread t2(threadWithUncaughtException);// t2.join();std::cout << "Main thread ending" << std::endl;return 0;
}

由于线程异常不会传播,正确的线程设计应在线程函数内部捕获和处理所有可能的异常。

RAII与线程管理

在C++中,我们常常使用RAII(Resource Acquisition Is Initialization)模式来确保资源的正确释放。对于线程管理,这一点也很重要,可以确保线程始终被正确地join()detach()

#include <iostream>
#include <thread>// 线程包装器,实现RAII
class ThreadGuard {
private:std::thread& t;public:// 构造函数接收线程引用explicit ThreadGuard(std::thread& t_) : t(t_) {}// 析构函数确保线程被join~ThreadGuard() {if (t.joinable()) {t.join();}}// 禁止复制和赋值ThreadGuard(const ThreadGuard&) = delete;ThreadGuard& operator=(const ThreadGuard&) = delete;
};void someFunction() {std::cout << "Thread function executing..." << std::endl;std::this_thread::sleep_for(std::chrono::seconds(1));std::cout << "Thread function completed." << std::endl;
}int main() {try {std::thread t(someFunction);ThreadGuard guard(t);  // RAII包装器确保t被join// 模拟异常// throw std::runtime_error("Simulated exception");std::cout << "Main thread continuing..." << std::endl;}catch (const std::exception& e) {std::cout << "Exception caught: " << e.what() << std::endl;}std::cout << "Main thread exiting safely." << std::endl;return 0;
}

C++17引入了std::jthread类,它是std::thread的改进版本,自动实现了RAII模式,并提供了取消线程的能力。在C++20中,它已成为标准的一部分。

实际应用案例

并行计算示例

以下是一个使用多线程并行计算向量点积的例子:

#include <iostream>
#include <vector>
#include <thread>
#include <numeric>
#include <functional>
#include <future>// 计算部分点积
double partialDotProduct(const std::vector<double>& v1, const std::vector<double>& v2,size_t start, size_t end) {return std::inner_product(v1.begin() + start, v1.begin() + end,v2.begin() + start, 0.0);
}// 并行计算点积
double parallelDotProduct(const std::vector<double>& v1,const std::vector<double>& v2,unsigned numThreads) {std::vector<std::future<double>> futures(numThreads);std::vector<std::thread> threads(numThreads);// 计算每个线程处理的元素数量size_t length = v1.size();size_t blockSize = length / numThreads;// 启动线程for (unsigned i = 0; i < numThreads; ++i) {// 计算当前线程处理的范围size_t start = i * blockSize;size_t end = (i == numThreads - 1) ? length : (i + 1) * blockSize;// 创建promise和futurestd::promise<double> promise;futures[i] = promise.get_future();// 创建线程threads[i] = std::thread([&v1, &v2, start, end, promise = std::move(promise)]() mutable {double result = partialDotProduct(v1, v2, start, end);promise.set_value(result);});}// 等待所有线程完成并获取结果double result = 0.0;for (unsigned i = 0; i < numThreads; ++i) {threads[i].join();result += futures[i].get();}return result;
}int main() {// 创建两个测试向量std::vector<double> v1(1'000'000, 1.0);std::vector<double> v2(1'000'000, 2.0);// 单线程计算auto start = std::chrono::high_resolution_clock::now();double singleThreadResult = std::inner_product(v1.begin(), v1.end(), v2.begin(), 0.0);auto end = std::chrono::high_resolution_clock::now();std::chrono::duration<double, std::milli> singleThreadTime = end - start;// 多线程计算start = std::chrono::high_resolution_clock::now();unsigned numThreads = std::thread::hardware_concurrency();  // 获取CPU核心数double multiThreadResult = parallelDotProduct(v1, v2, numThreads);end = std::chrono::high_resolution_clock::now();std::chrono::duration<double, std::milli> multiThreadTime = end - start;// 输出结果std::cout << "Single thread result: " << singleThreadResult << " (Time: " << singleThreadTime.count() << "ms)" << std::endl;std::cout << "Multi thread result: " << multiThreadResult << " (Time: " << multiThreadTime.count() << "ms)" << std::endl;std::cout << "Speedup: " << singleThreadTime.count() / multiThreadTime.count()<< "x" << std::endl;return 0;
}

在这个例子中,我们将大向量分成多个块,由不同线程计算部分点积,然后汇总结果。在多核处理器上,这种并行计算通常能显著提高性能。

后台任务处理

多线程也常用于执行不应阻塞主线程的后台任务,如下载、IO操作等:

#include <iostream>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <atomic>// 线程安全的任务队列
template<typename T>
class TaskQueue {
private:std::queue<T> queue_;std::mutex mutex_;std::condition_variable cond_;std::atomic<bool> quit_{false};public:// 添加任务到队列void push(T item) {{std::lock_guard<std::mutex> lock(mutex_);queue_.push(std::move(item));}cond_.notify_one();  // 通知一个等待线程}// 从队列获取任务bool pop(T& item) {std::unique_lock<std::mutex> lock(mutex_);// 等待直到队列有元素或收到退出信号cond_.wait(lock, [this] { return !queue_.empty() || quit_; });// 如果是退出信号且队列为空,返回falseif (queue_.empty()) return false;item = std::move(queue_.front());queue_.pop();return true;}// 设置退出信号void quit() {quit_ = true;cond_.notify_all();  // 通知所有等待线程}// 检查队列是否为空bool empty() const {std::lock_guard<std::mutex> lock(mutex_);return queue_.empty();}
};// 模拟文件下载任务
void downloadFile(const std::string& url) {std::cout << "Downloading: " << url << "..." << std::endl;// 模拟下载时间std::this_thread::sleep_for(std::chrono::seconds(2));std::cout << "Download completed: " << url << std::endl;
}// 后台下载线程函数
void downloadWorker(TaskQueue<std::string>& taskQueue) {std::string url;// 循环处理队列中的任务while (taskQueue.pop(url)) {downloadFile(url);}std::cout << "Download worker exiting..." << std::endl;
}int main() {TaskQueue<std::string> downloadQueue;// 创建后台工作线程std::thread workerThread(downloadWorker, std::ref(downloadQueue));// 添加下载任务downloadQueue.push("http://example.com/file1.zip");downloadQueue.push("http://example.com/file2.zip");downloadQueue.push("http://example.com/file3.zip");// 模拟主线程其他工作for (int i = 0; i < 5; ++i) {std::cout << "Main thread doing other work..." << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(500));}// 添加更多任务downloadQueue.push("http://example.com/file4.zip");downloadQueue.push("http://example.com/file5.zip");// 等待所有任务完成while (!downloadQueue.empty()) {std::cout << "Waiting for downloads to complete..." << std::endl;std::this_thread::sleep_for(std::chrono::seconds(1));}// 发送退出信号并等待工作线程结束downloadQueue.quit();workerThread.join();std::cout << "Main thread exiting." << std::endl;return 0;
}

这个示例实现了一个简单的后台任务处理系统,主线程可以向队列添加任务,而工作线程在后台处理这些任务。这种模式在GUI应用、服务器程序等场景中很常见。

用户界面响应性改进

多线程可以显著提高用户界面的响应性。下面是一个简化的示例,演示如何在后台线程执行耗时操作,同时保持主线程响应用户输入:

#include <iostream>
#include <thread>
#include <chrono>
#include <atomic>
#include <mutex>// 模拟耗时计算
void heavyComputation(std::atomic<double>& progress, std::atomic<bool>& shouldStop) {for (int i = 0; i <= 100; ++i) {// 检查是否应该停止if (shouldStop) {std::cout << "Computation cancelled!" << std::endl;return;}// 执行"计算"std::this_thread::sleep_for(std::chrono::milliseconds(100));// 更新进度progress = i;}std::cout << "Computation completed successfully!" << std::endl;
}// 显示进度的线程
void displayProgress(const std::atomic<double>& progress, const std::atomic<bool>& shouldStop) {while (!shouldStop && progress < 100) {std::cout << "Progress: " << progress << "%" << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(500));}
}int main() {std::atomic<double> progress(0);std::atomic<bool> shouldStop(false);std::cout << "Starting heavy computation..." << std::endl;std::cout << "Press 'c' to cancel or any other key to check progress." << std::endl;// 启动计算线程std::thread computationThread(heavyComputation, std::ref(progress), std::ref(shouldStop));// 启动显示进度的线程std::thread displayThread(displayProgress, std::ref(progress), std::ref(shouldStop));// 主线程处理用户输入char input;while (progress < 100 && !shouldStop) {input = std::cin.get();if (input == 'c' || input == 'C') {std::cout << "Cancellation requested." << std::endl;shouldStop = true;} else {std::cout << "Current progress: " << progress << "%" << std::endl;}}// 等待线程完成computationThread.join();displayThread.join();std::cout << "Program exiting." << std::endl;return 0;
}

在这个示例中,我们创建了两个线程:一个执行耗时计算,另一个定期显示进度。同时,主线程保持响应用户输入,允许用户随时取消计算。这种模式可以容易地扩展到实际的GUI应用程序中。

常见问题与注意事项

竞态条件

当多个线程同时访问共享数据,并且至少有一个线程修改数据时,就会发生竞态条件(Race Condition):

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>// 全局计数器
int counter = 0;
std::mutex counterMutex;  // 保护counter的互斥量// 不安全的递增函数 - 存在竞态条件
void incrementUnsafe(int numTimes) {for (int i = 0; i < numTimes; ++i) {++counter;  // 竞态条件!}
}// 安全的递增函数 - 使用互斥量
void incrementSafe(int numTimes) {for (int i = 0; i < numTimes; ++i) {std::lock_guard<std::mutex> lock(counterMutex);++counter;  // 受互斥量保护}
}int main() {int numThreads = 10;int incrementsPerThread = 100000;// 测试不安全的版本counter = 0;std::vector<std::thread> unsafeThreads;for (int i = 0; i < numThreads; ++i) {unsafeThreads.emplace_back(incrementUnsafe, incrementsPerThread);}for (auto& t : unsafeThreads) {t.join();}std::cout << "Unsafe counter value: " << counter << " (Expected: " << numThreads * incrementsPerThread << ")" << std::endl;// 测试安全的版本counter = 0;std::vector<std::thread> safeThreads;for (int i = 0; i < numThreads; ++i) {safeThreads.emplace_back(incrementSafe, incrementsPerThread);}for (auto& t : safeThreads) {t.join();}std::cout << "Safe counter value: " << counter << " (Expected: " << numThreads * incrementsPerThread << ")" << std::endl;return 0;
}

在不安全版本中,多个线程可能同时读取counter的值,增加它,然后写回,这可能导致某些递增操作被覆盖。安全版本使用互斥量确保每次只有一个线程可以修改counter,从而避免竞态条件。

死锁与活锁

死锁(Deadlock)是指两个或多个线程互相等待对方持有的资源,导致所有线程都无法继续执行:

#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>std::mutex mutexA;
std::mutex mutexB;// 可能导致死锁的函数
void deadlockFunction1() {std::cout << "Thread 1 trying to lock mutexA..." << std::endl;std::lock_guard<std::mutex> lockA(mutexA);std::cout << "Thread 1 locked mutexA" << std::endl;// 添加延迟增加死锁可能性std::this_thread::sleep_for(std::chrono::milliseconds(100));std::cout << "Thread 1 trying to lock mutexB..." << std::endl;std::lock_guard<std::mutex> lockB(mutexB);std::cout << "Thread 1 locked mutexB" << std::endl;std::cout << "Thread 1 releasing both locks" << std::endl;
}void deadlockFunction2() {std::cout << "Thread 2 trying to lock mutexB..." << std::endl;std::lock_guard<std::mutex> lockB(mutexB);std::cout << "Thread 2 locked mutexB" << std::endl;// 添加延迟增加死锁可能性std::this_thread::sleep_for(std::chrono::milliseconds(100));std::cout << "Thread 2 trying to lock mutexA..." << std::endl;std::lock_guard<std::mutex> lockA(mutexA);std::cout << "Thread 2 locked mutexA" << std::endl;std::cout << "Thread 2 releasing both locks" << std::endl;
}// 安全版本,使用std::lock防止死锁
void noDeadlockFunction1() {std::cout << "Safe Thread 1 trying to lock both mutexes..." << std::endl;std::scoped_lock lock(mutexA, mutexB);  // C++17的std::scoped_lockstd::cout << "Safe Thread 1 locked both mutexes" << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(100));std::cout << "Safe Thread 1 releasing both locks" << std::endl;
}void noDeadlockFunction2() {std::cout << "Safe Thread 2 trying to lock both mutexes..." << std::endl;std::scoped_lock lock(mutexB, mutexA);  // 注意顺序不同,但不会导致死锁std::cout << "Safe Thread 2 locked both mutexes" << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(100));std::cout << "Safe Thread 2 releasing both locks" << std::endl;
}int main() {// 示范死锁(注意:这会使程序卡住)std::cout << "Demonstrating deadlock (program will hang):" << std::endl;/*std::thread t1(deadlockFunction1);std::thread t2(deadlockFunction2);t1.join();t2.join();*/// 展示避免死锁的方法std::cout << "\nDemonstrating deadlock prevention:" << std::endl;std::thread t3(noDeadlockFunction1);std::thread t4(noDeadlockFunction2);t3.join();t4.join();return 0;
}

为避免死锁:

  1. 始终以相同顺序锁定多个互斥量
  2. 使用std::lockstd::scoped_lock同时锁定多个互斥量
  3. 避免在持有锁时调用用户代码(可能会尝试获取其他锁)
  4. 使用层次锁定,为每个互斥量分配层级,只允许按层级顺序锁定

活锁(Livelock)类似于死锁,但线程并非阻塞等待,而是持续尝试某个无法完成的操作,导致CPU资源被消耗而无进展。

线程数量的选择

选择适当的线程数量对于优化性能至关重要:

#include <iostream>
#include <thread>
#include <vector>
#include <chrono>
#include <numeric>
#include <algorithm>// 线程数量性能测试函数
void threadCountBenchmark(const std::vector<int>& data) {// 最大线程数为硬件并发线程数(通常是CPU核心数)unsigned int maxThreads = std::thread::hardware_concurrency();std::cout << "Hardware concurrency: " << maxThreads << " threads" << std::endl;// 测试不同线程数量for (unsigned int numThreads = 1; numThreads <= maxThreads * 2; numThreads += std::max(1u, maxThreads / 4)) {// 计算每个线程处理的元素数size_t blockSize = data.size() / numThreads;auto start = std::chrono::high_resolution_clock::now();std::vector<std::thread> threads;std::vector<long long> partialSums(numThreads);// 创建线程for (unsigned int i = 0; i < numThreads; ++i) {size_t startIdx = i * blockSize;size_t endIdx = (i == numThreads - 1) ? data.size() : (i + 1) * blockSize;threads.emplace_back([&data, &partialSums, i, startIdx, endIdx](){// 模拟计算密集型任务long long sum = 0;for (size_t j = startIdx; j < endIdx; ++j) {sum += data[j] * data[j];  // 计算平方和}partialSums[i] = sum;});}// 等待所有线程完成for (auto& t : threads) {t.join();}// 合并结果long long totalSum = std::accumulate(partialSums.begin(), partialSums.end(), 0LL);auto end = std::chrono::high_resolution_clock::now();auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();std::cout << "Threads: " << numThreads << ", Time: " << duration << "ms"<< ", Result: " << totalSum << std::endl;}
}int main() {// 创建大量数据const size_t dataSize = 100'000'000;std::vector<int> data(dataSize);for (size_t i = 0; i < dataSize; ++i) {data[i] = i % 100;  // 简单模式}// 运行基准测试threadCountBenchmark(data);return 0;
}

选择线程数量的一般指南:

  1. 对于计算密集型任务,线程数接近或等于CPU核心数通常是最优的
  2. 对于IO密集型任务,线程数可以超过CPU核心数,因为线程经常处于等待状态
  3. 避免创建过多线程,这会增加线程切换开销
  4. 考虑使用线程池来控制线程数量和重用线程

调试多线程程序

多线程程序的调试比单线程程序更具挑战性,主要原因在于线程执行顺序的不确定性。以下是一些有用的调试技巧:

  1. 使用线程ID标记日志
#include <iostream>
#include <thread>
#include <sstream>
#include <iomanip>
#include <mutex>std::mutex logMutex;  // 保护日志输出的互斥量// 带线程ID的日志记录函数
void log(const std::string& message) {std::lock_guard<std::mutex> lock(logMutex);std::ostringstream tid;tid << std::this_thread::get_id();std::cout << "[Thread " << std::setw(5) << tid.str() << "] " << message << std::endl;
}void workerFunction(int id) {log("Worker " + std::to_string(id) + " starting");std::this_thread::sleep_for(std::chrono::milliseconds(id * 100));log("Worker " + std::to_string(id) + " step 1");std::this_thread::sleep_for(std::chrono::milliseconds(id * 50));log("Worker " + std::to_string(id) + " finishing");
}int main() {log("Main thread starting");std::vector<std::thread> threads;for (int i = 0; i < 5; ++i) {threads.emplace_back(workerFunction, i + 1);}log("All workers started");for (auto& t : threads) {t.join();}log("All workers completed");return 0;
}
  1. 使用调试器的线程窗口:现代调试器如Visual Studio、GDB和LLDB都提供了线程窗口,可以查看所有线程的状态并在线程之间切换。

  2. 使用条件编译的调试帮助器:在关键点添加调试信息。

  3. 记录时间戳:在日志中添加时间戳,帮助分析事件顺序。

  4. 使用原子操作进行计数和检查:使用原子变量跟踪关键状态转换。

  5. 使用线程分析工具:如Intel Thread Checker、Valgrind的DRD和Helgrind工具等。

总结

在这篇文章中,我们介绍了C++11的std::thread类及其基本用法,包括线程的创建、参数传递、生命周期管理以及常见问题。多线程编程是现代C++开发中不可或缺的一部分,掌握这些基础知识将为你构建高性能、响应迅速的应用程序奠定基础。

主要要点回顾:

  1. 线程创建与基本操作:使用std::thread创建线程,传递函数、函数对象或lambda表达式作为线程函数。

  2. 参数传递:使用值传递、std::ref引用传递或移动语义传递参数到线程函数。

  3. 线程管理:使用join()等待线程完成或detach()允许线程在后台运行。

  4. 线程本地存储:使用thread_local关键字创建线程私有的变量。

  5. 异常处理:线程函数中的异常必须在线程内部捕获,否则程序将终止。

  6. 线程安全问题:了解竞态条件、死锁等多线程编程常见问题,以及防范措施。

  7. 实际应用:使用多线程可以提高计算性能、改善用户界面响应性、实现后台任务处理等。

然而,本文只是多线程编程的开始。在接下来的文章中,我们将深入探讨更多高级主题,如互斥量、锁、条件变量等同步原语,它们对于构建线程安全的数据结构和算法至关重要。


这是我C++学习之旅系列的第五十四篇技术文章。查看完整系列目录了解更多内容。

相关文章:

C++学习:六个月从基础到就业——多线程编程:std::thread基础

C学习&#xff1a;六个月从基础到就业——多线程编程&#xff1a;std::thread基础 本文是我C学习之旅系列的第五十四篇技术文章&#xff0c;也是第四阶段"并发与高级主题"的第一篇&#xff0c;介绍C11引入的多线程编程基础知识。查看完整系列目录了解更多内容。 引言…...

深入理解指针(一)

1.内存和地址 2.指针变量和地址 3.指针变量类型的意义 4.指针运算 1. 内存和地址 1.1 内存 在讲内存和地址之前&#xff0c;为了大家更好的理解举了这么个例子&#xff1a; 假如有一栋教学楼&#xff0c;刚好你今天在这栋楼的某一个课室上课&#xff0c;已知这栋楼有50个…...

添加currentSchema后,该模式下表报不存在(APP)

文章目录 环境文档用途详细信息相关文档 环境 系统平台&#xff1a;Linux x86-64 Red Hat Enterprise Linux 7 版本&#xff1a;4.5.7 文档用途 解决程序URL添加currentSchema后&#xff0c;访问该模式下的表&#xff0c;报错信息com.highgo.jdbc.util.PSQLException: ERROR…...

Python数据整合与转换全攻略

在大数据时代&#xff0c;企业平均使用16个不同数据源&#xff0c;但数据利用率不足30%。数据整合与转换能力已成为数据工程师的核心竞争力。本文将通过电商订单数据整合实战&#xff0c;系统讲解Python数据整合与转换的核心技术栈。 一、数据整合的三大挑战与应对策略 1. 数…...

ArcGIS操作16:添加经纬网

1、单击视图 > 数据框属性 2、单击格网选项卡 > 新建格网按钮 3、创建经纬网 4、经纬网标注间隔需要自己多次尝试&#xff0c;选择一个合适的值&#xff0c;这里江苏省选择50 5、继续设置合适的参数 6、点击应用&#xff0c;预览是否合适&#xff08;不合适再新建一个经…...

BioID技术:探索蛋白质相互作用的新方法

在细胞的复杂环境中&#xff0c;蛋白质并非孤立地执行其功能&#xff0c;而是通过与其他蛋白质相互协作来完成各种生物学过程。理解蛋白质相互作用网络对于揭示细胞机制和疾病发生发展具有至关重要的意义。近年来&#xff0c;一种名为BioID&#xff08;Biotin Identification&a…...

Java 大视界——Java大数据在智慧交通智能停车诱导系统中的数据融合与实时更新

智慧交通的快速发展对城市停车资源的高效利用提出了更高要求&#xff0c;而智能停车诱导系统作为缓解“停车难”问题的核心手段&#xff0c;亟需解决多源数据融合、实时状态更新及高并发访问等挑战。Java凭借其稳定的大数据生态、卓越的实时计算能力及跨平台兼容性&#xff0c;…...

【Redisson】快速实现分布式锁

大家好&#xff0c;我是jstart千语。之前给大家分享过使用redis的set nx ex命令实现分布式锁。但手动的实现方式不仅麻烦&#xff0c;而且不好管理。实现出来的锁也是不可重入的&#xff0c;不可重试的。那么在要求比较高的系统中&#xff0c;就不太适用了。虽然说重入问题可以…...

内核常见面试问题汇总

1、Linux 中主要有哪几种内核锁?它们各自的特点和适用场景是什么? 自旋锁 自旋锁是一种忙等待锁,当一个线程试图获取一个被占用的自旋锁时,他会一直循环在那里,不断地检查锁是否可用,而不会进入睡眠状态。 自旋锁的优点这是在锁被持有的时间很短的情况下,性能非常高,…...

laravel中如何使用Validator::make定义一个变量是 ,必传的,json格式字符串

在 Laravel 中&#xff0c;使用 Validator::make 定义一个变量为必传且为JSON 格式字符串时&#xff0c;可以通过以下方式实现&#xff1a; use Illuminate\Support\Facades\Validator;$validator Validator::make($request->all(), [your_field > required|json, // 必…...

【全解析】EN18031标准下的NMM网络监控机制

上一篇文章我们了解了RLM机制如何为设备抵御DoS攻击保驾护航&#xff0c;今天我们将目光转向 EN18031 标准中的另一个重要防线——NMM&#xff08;Network Monitoring Mechanism&#xff09;网络监控机制。 NMM - 1规定&#xff0c;如果设备是网络设备&#xff0c;应提供网络监…...

浏览器开发随笔

浏览器多进程架构&#xff08;Chrome&#xff09; ----------------------------- | Browser Process | |-----------------------------| | UI 线程、主控、导航、安全策略 | -----------------------------| | |↓ ↓ ↓ -------…...

漏洞类型与攻击技术

1.1 SQL注入 1.1.1 SQL注入简介与原理 SQL注入是通过用户输入的数据中插入恶意SQL代码,绕过应用程序对数据库的合法操作,进而窃取、篡改或删除数据的攻击方式。核心原理是应用程序未对用户输入进行严格过滤,导致攻击者可以操控SQL语句逻辑。 1.1.2 联合查询注入与报…...

day018-磁盘管理-案例

文章目录 1. 磁盘分区1.1 手动磁盘分区1.2 重装系统&#xff0c;保留分区1.2.1 选择从光盘启动1.2.2 保留系统盘分区1.2.3 挂载数据盘 2. 物理服务器使用流程3. swap3.1 增加swap3.2 关闭swap 4. 故障案例&#xff08;红帽类系统&#xff09;4.1 root密码忘记&#xff0c;重新设…...

spark调度系统核心组件SparkContext、DAGSchedul、TaskScheduler介绍

目录 1. SparkContext2.DAGScheduler3. TaskScheduler4. 协作关系Spark调度系统的核心组件主要有SparkContext、DAGScheduler和TaskScheduler SparkContext介绍 1. SparkContext 1、资源申请: SparkContext是Spark应用程序与集群管理器(如Standalone模式下的Master、Yarn模…...

python数据可视化第三章数值计算基础

numpy库 数组创建 import numpy as np #创建n维数组array a np.array([1, 2, 3]) b np.array([4, 5, 6]) #可以直接运算 print(a 1) # [2 3 4] print(ab) # [5 7 9] #数组的维度&#xff1a;ndim print(a.ndim) #1 #数组的形状&#xff1a;shape print(a.shape) #(3,) 一维…...

std::chrono类的简单使用实例及分析

author: hjjdebug date: 2025年 05月 20日 星期二 14:36:17 CST descrip: std::chrono类的简单使用实例及分析 文章目录 1.实例代码:2. 代码分析:2.1 auto t1 std::chrono::high_resolution_clock::now();2.1.1 什么是 system_clock2.1.2 什么是 chrono::time_point?2.1.3 什…...

MongoDB 学习(三)Redis 与 MongoDB 的区别

目录 一、NoSQL 数据库与关系型数据库的优缺点二、Redis 与 MongoDB 的区别 MongoDB 和 Redis 都是 NoSQL 数据库&#xff0c;采用 结构型数据存储&#xff0c;而非 MySQL、Oracle 等则属于传统的 关系型数据库。 一、NoSQL 数据库与关系型数据库的优缺点 关系型数据库&#…...

Java双指针法:原地移除数组元素

Java双指针法&#xff1a;原地移除数组元素 代码解析关键点示例特点 代码解析 class Solution {public int removeElement(int[] nums, int val) {int cur 0; // 初始化一个指针 cur&#xff0c;表示新数组的当前写入位置for (int i 0; i < nums.length; i) { // 遍历原数…...

第二届帕鲁杯时空交织的密语

题目描述&#xff1a; 我们在暗网服务器中发现了一个神秘文件&#xff0c;据说是某个黑客组织的「时空密钥」&#xff0c;文件内容似乎由大量时间戳构成。情报显示&#xff0c;只有将时间维度与二进制低语结合才能解开秘密。线索隐藏在时空的起点与终点之间。 解压得到timestr…...

【神经网络与深度学习】扩散模型之原理解释

引言&#xff1a; 在人工智能的生成领域&#xff0c;扩散模型&#xff08;Diffusion Model&#xff09;是一项极具突破性的技术。它不仅能够生成高质量的图像&#xff0c;还可以应用于音频、3D建模等领域。扩散模型的核心思想来源于物理扩散现象&#xff0c;其工作方式类似于从…...

Map更简洁的编码构建

Map的链式构建需求 jdk1.8&#xff0c;编码时需要手动构建一个Map 传统Map构建方式 - 声明列表&#xff0c;依次添加元素&#xff1b; - 利用匿名内部类 实例初始化块&#xff0c;比较简洁声明并添加元素 //1 先声明创建 Map<String, Object> createMap new HashMap&…...

深挖navigator.webdriver浏览器自动化检测的底层分析

本文将带你深入探索并实践如何从底层层面破解浏览器 navigator.webdriver 检测&#xff0c;结合爬虫代理等策略伪装、多线程加速等技术&#xff0c;在豆瓣图书搜索页面上批量采集图书评分、简介、作者等信息。文章面向初学者&#xff0c;采用分步教程型结构&#xff0c;并增设「…...

业务流程和数据结构之间如何对应

业务流程和数据结构之间存在着紧密的对应关系&#xff0c;它们相互依赖、相互影响。以下是它们之间的对应关系以及如何进行对应&#xff1a; 1. 业务流程中的步骤与数据结构的记录 业务流程&#xff1a;业务流程是由一系列的步骤组成的&#xff0c;每个步骤都有明确的任务和目…...

浜掕仈缃戝ぇ鍘傞潰璇曪細璋㈤鏈虹殑鐖嗙瑧姹傝亴涔嬫梾

浜掕仈缃戝ぇ鍘傞潰璇曪細璋㈤鏈虹殑鐖嗙瑧姹傝亴涔嬫梾 绗竴杞細鍩虹鐭ヨ瘑杞扮偢 闈㈣瘯瀹&#xfffd;锛氭垜浠厛鑱婅亰Java鐨勫熀纭&#xfffd;鐭ヨ瘑锛岃阿椋炴満锛屼綘鑳借В閲婁竴涓婮ava涓殑HashMap鏄浣曞伐浣滅殑锛焅n 璋㈤鏈&#xfffd;锛氬摝…...

关于Linux服务器数字取证一

本次讲解apachemysql类型的基本类型 仿真 导入我们的检材 默认是仅主机模式&#xff0c;改成nat模式 查看ip地址 ip addr 修改虚拟机网卡配置 连通性测试 查找根目录 现在我们可以通过ip访问网站&#xff0c;但是网站处于关闭状态 接下来我们去查找配置文件&#xff0c;去…...

电商运营数据分析指南之流量指标

本文电商运营数据分析指南之流量指标主要面向电商行业新人&#xff0c;针对数据分析师/运营人员日常监控中的常见困惑提供解决方案。适合刚接触电商数据监控缺乏思路&#xff0c;或对电商数据指标感兴趣希望系统学习的读者。 电商数据分析如同航海指南针&#xff0c;精准的数据…...

C++初阶-迭代器失效和vector::insert函数的最终实现

目录 1.vector::insert函数 1.1问题分析 1.2vector::insert函数的最终实现 1.3vector::insert函数的分析 2.第二种迭代器失效 3.第三种迭代器失效 4.迭代器失效deepseek的回答 1. 迭代器失效的原因 2. 不同容器的迭代器失效情况 &#xff08;1&#xff09;std::vecto…...

DeepSeek的走红,会不会带动芯片市场新一轮增长?

在大模型竞赛如火如荼的今天&#xff0c;国产AI大模型DeepSeek的迅速走红&#xff0c;无疑为中国AI行业注入了新的活力。从技术突破到商业落地&#xff0c;DeepSeek不仅在技术圈引发了广泛关注&#xff0c;也让“AI大模型”这一关键词再次登上产业链的风口浪尖。但一个更值得深…...

小土堆pytorch--神经网路-卷积层池化层

神经网路-卷积层&池化层 一级目录二级目录三级目录 1. 神经网路-卷积层2. 神经网路最大池化的应用 一级目录 二级目录 三级目录 1. 神经网路-卷积层 在PyTorch中&#xff0c;torch.nn.Conv2d函数定义了一个二维卷积层&#xff0c;其常用参数包括&#xff1a; in_channel…...

什么叫生成式人工智能?职业技能的范式转移与能力重构

当人们谈论人工智能时&#xff0c;常常混淆其不同类型与功能。生成式人工智能作为AI领域的重要分支&#xff0c;其本质不在于分析或分类已有数据&#xff0c;而在于创造新的内容——无论是文本、图像、音乐还是代码。理解这一技术的内涵&#xff0c;不仅关乎技术认知&#xff0…...

集星獭 | 重塑集成体验:新版编排重构仿真电商订单数据入库

概要介绍 新版服务编排以可视化模式驱动电商订单入库流程升级&#xff0c;实现订单、客户、库存、发票、发货等环节的自动化处理。流程中通过循环节点、判断逻辑与数据查询的编排&#xff0c;完成了低代码构建业务逻辑&#xff0c;极大提升订单处理效率与业务响应速度。 背景…...

小白成长之路-Linux磁盘管理(一)

文章目录 前言一、磁盘介绍1、磁盘的物理结构1.1硬盘结构1.2磁头数、磁道、柱面、扇区 2.CHS编号2.1磁道编号规则2.3扇区编号规则2.3通过CHS计算硬盘容量 3、磁盘存储划分3.1第一个扇区存储的数据3.2硬盘分区 4.开机流程5、要点6、磁盘存储数据的形式 二、Linux文件系统1、根文…...

【Linux】第二十四章 管理网络安全

1. 防火墙在 Linux 系统安全中有哪些重要的作用&#xff1f; 防止未经授权的访问 网络流量过滤&#xff0c;常见过滤方式包括&#xff1a;基于 IP 地址、端口、协议类型&#xff08;如 TCP、UDP&#xff09;、源和目标地址等。限制流量速率、阻止来自单个IP或多个IP的高频请求保…...

【解决】SSH 远程失败之路由配置问题

开发平台&#xff1a;RedHat 8   一、问题描述 使用 WindTerm SSH 远程连接 192.168.88.211 虚拟主机&#xff0c;无法连接。   二、问题追溯 VMWare 虚拟机配置 虚拟网卡IPVMNet 模式应用网卡1&#xff08;eth0&#xff09;192.168.88.11/24VMNet 8服务网卡2&#xff08;…...

中级网络工程师知识点9

1.在Linux中&#xff0c;负责配置DNS的文件是/etc/resolv.conf&#xff0c;包含了主机的域名搜索顺序和DNS服务器的地址 2.主域名服务器在接收到域名请求后&#xff0c;首先查询的是本地缓存 3.自动专用地址&#xff1a;169.254.X.X/16 4.FTP默认20端口&#xff0c;传输文件…...

网页前端开发(基础)

前端开发三件客&#xff1a;HTML&#xff0c;CSS&#xff0c;JavaScript。 web标准&#xff0c;也称网页标准&#xff0c;由3个组成部分。1.HTML,管理页面的元素和内容。2.CSS&#xff0c;负责网页的表现&#xff08;页面元素的外观&#xff0c;位置等页面样式。如&#xff1a;…...

Git命令使用全攻略:从创建分支到合并的完整流程

Git命令使用全攻略&#xff1a;从创建分支到合并的完整流程 引言一、初始化项目与基础配置1.1 克隆远程仓库1.2 查看当前分支状态 二、创建与管理分支2.1 从main分支创建新功能分支2.2 查看分支列表2.3 提交代码到新分支2.4 推送分支到GitHub 三、版本发布与标签管理3.1 创建轻…...

边缘智能与量子计算双轮驱动:IVX 开启实时 AI 开发新维度

一、技术跃迁&#xff1a;量子化组件架构如何颠覆传统 AI 开发流程 在传统 AI 开发范式中&#xff0c;将 GPT-4o、Mediapipe 等模型集成到业务系统需要经历 "模型训练 - API 对接 - 前端适配" 的三重技术壁垒。开发团队需同时掌握 TensorFlow、Flask、React 等技术栈…...

对冲策略加仓止损盈思路

外汇交易中的对冲策略&#xff0c;重点在于加仓和盈利出局的策略。该策略通过多种方法来管理头寸&#xff0c;旨在最大化盈利并控制风险。 加仓策略 金字塔加仓法 金字塔加仓法是一种逐步增加头寸的方法。在初始头寸盈利后&#xff0c;以较小的手数逐步增加头寸。这种方法可以在…...

ERP生产环境索引重建:高风险操作还是性能良药?何时动手,如何操刀?

ERP系统是企业运营的核心,其数据库性能至关重要。索引作为提升查询效率的关键,其维护(尤其是重建)操作却常常让DBA和运维工程师们如履薄冰。本文将深入探讨在生产ERP环境中重建索引的潜在风险、必要性评估、最佳实践以及不同数据库的注意事项,旨在帮助技术人员做出明智决策…...

PyTorch 之 torch.distributions.Categorical 详解

PyTorch 之 torch.distributions.Categorical 详解 PyTorch 之 torch.distributions.Categorical 详解一、创建分类分布&#xff08;一&#xff09;基本语法&#xff08;二&#xff09;示例 二、采样&#xff08;一&#xff09;方法&#xff08;二&#xff09;示例 三、计算概率…...

杰发科技AC7840——如何把结构体数据写到Dflash中

1. 结构体数据被存放在Pflash中 正常情况下&#xff0c;可以看到全局变量的结构体数据被存放在Pflash中 数字部分存在RAM中 2. 最小编程单位 8字节编程&#xff0c;因此如果结构体存放在Dfalsh中&#xff0c;进行写操作&#xff0c;需要写8字节的倍数 第一种办法&#xff1a;…...

vue路由小案例

vue路由小案例 案例需求案例实现小结 案例需求 创建二级路由&#xff0c;掌握嵌套路由点击内容&#xff0c;根据不同id实现页面的跳转&#xff0c;掌握传参方式利用routerlink标签封装的类&#xff0c;实现高亮实现重定向&#xff0c;自动跳转到二级页面 案例实现 &#xff…...

链表面试题9之环形链表进阶

那么上一题我们已经知道了双指针的变法以及拓展的用法&#xff0c;那么这里我们直接难度升级。 想回去复习的这里放个链接&#xff1a;链表的面试题8之环形链表-CSDN博客 题目链接&#xff1a;142. 环形链表 II - 力扣&#xff08;LeetCode&#xff09; 我们来看这道题目主要…...

CentOS 7上BIND9配置DNS服务器指南

详细说明如何在 CentOS 7 上配置 BIND9 扮演这四种不同的 DNS 角色&#xff0c;包括配置文件示例和注意事项。 BIND9 可以扮演 DNS 体系中的多种角色&#xff1a; 权威DNS服务器 (Authoritative DNS Server): 存储特定域名&#xff08;例如 example.com&#xff09;的官方DNS记…...

Pytorch针对不同电脑配置详细讲解+安装(CPU)

一、前言 安装pytorch前&#xff0c;应按照我前边的博文中&#xff0c;安装完anaconda和pycharm&#xff0c;并且配置完环境变量以后哈。 Pytorch是什么&#xff1f; 它是一个库,是一个开源的机器学习框架&#xff0c;专注于深度学习任务&#xff0c;由Facebook的人工智能研…...

知识体系_数据分析挖掘_基尼系数

1 概述 基尼系数(gini coefficient)表示在全部居民收入中,用于进行不平均分配的那部分收入占总收入的百分比。社会中每个人的收入都一样、收入分配绝对平均时,基尼系数是0;全社会的收入都集中于一个人、收入分配绝对不平均时,基尼系数是1。现实生活中,两种情况都不可能发…...

Fiddler抓包教程->HTTP和HTTPS基础知识

1.简介 有的伙伴可能会好奇&#xff0c;不是讲解和分享抓包工具,怎么这里开始讲解HTTP和HTTPS协议了。这是因为你对HTTP协议越了解&#xff0c;你就能越掌握Fiddler的使用方法&#xff0c;反过来你越使用Fiddler&#xff0c;就越能帮助你了解HTTP协议。 Fiddler无论对开发人员…...

超级维特根斯坦

AI智能体核心指令:语言智慧融合体 - 深度思辨、专业应用与协同创新大师 1. 角色设定 (Persona) 你将扮演一位“语言智慧融合体”AI,一个集大成的、具备卓越情境智能、精妙引导艺术与长时程战略规划能力的语言思想、艺术与应用科学伙伴。你的核心人格与方法论基于以下杰出贡…...