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

『 C++ 』线程与原子操作:高效并发编程的利器

文章目录

  • 为什么使用C++线程
  • 一、`C++11`std::thread`类的简单介绍
    • 1.1 函数名与功能
    • 1.2`std::thread`类的简单介绍
    • 1.3 线程函数参数
  • 二、 线程同步与锁
    • 2.1 线程同步与锁
    • 2.2死锁演示
  • 三、原子操作
    • 3.1 原子操作与线程安全
    • 3.2 原子操作的优势
    • 3.3 CAS操作与自旋锁
    • 3.4 原子操作与普通操作的汇编对比
  • 四、共享资源的线程安全问题
    • 4.1`std::shared_ptr`的线程安全问题
    • 4.2 使用原子类型保护共享资源
  • 五、单例模式的线程安全问题

  在现代多核处理器时代,并发编程是提升程序性能的关键手段。C++11引入了对线程的原生支持,简化了多线程编程的复杂性。本文将结合代码示例,探讨C++线程的使用场景、原子操作的优势,以及如何解决线程安全问题。

为什么使用C++线程



  在C++11之前,多线程编程依赖于平台相关的API,如Windows的CreateThread和Linux的pthread_create,这限制了代码的跨平台能力。C++11引入了std::thread类,提供了统一的线程管理接口,简化了线程的创建、同步和销毁。

一、C++11std::thread`类的简单介绍

1.1 函数名与功能


函数名功能
thread()构造一个线程对象,没有关联任何线程函数,即没有启动任何线程
thread(fn, args1, args2, ...)构造一个线程对象,并关联线程函数fnargs1args2,…为线程函数的参数
get_id()获取线程id
joinable()线程是否还在执行,joinable代表的是一个正在执行中的线程
join()该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行
detach()在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程的“死活”就与主线程无关

1.2std::thread类的简单介绍

  在C++11之前,涉及到多线程问题,都是和平台相关的,比如Windows和Linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含<thread>头文件。C++11中线程类。

 注意:

• 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态。

• 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。

get_id()的返回值类型为id类型,id类型实际为std::thread命名空间下封装的一个类,该类中包含了一个结构体:

   #include <thread>int main(){std::thread t1;std::cout << t1.get_id() << std::endl;return 0;}

• 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。线程函数一般情况下可按照以下三种方式提供:

• 函数指针

• lambda表达式

• 函数对象

std::thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不影响线程的执行。

• 可以通过joinable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效:

• 采用无参构造函数构造的线程对象

• 线程对象的状态已经转移给其他线程对象

• 线程已经调用join或者detach结束

 示例代码:

#include <iostream>
using namespace std;
#include <thread>void ThreadFunc(int a)
{cout << "Thread1" << a << endl;
}class TF
{
public:void operator()(){cout << "Thread3" << endl;}
};int main()
{// 线程函数为函数指针thread t1(ThreadFunc, 10);// 线程函数为lambda表达式thread t2([]{cout << "Thread2" << endl; });// 线程函数为函数对象TF tf;thread t3(tf);t1.join();t2.join();t3.join();cout << "Main thread!" << endl;return 0;
}

 面试题:并发与并行的区别?

1.3 线程函数参数

内容有点多,先放下面的链接了

C++多线程编程中的参数传递技巧

二、 线程同步与锁

2.1 线程同步与锁

在多线程环境中,同步是确保线程安全的关键。C++提供了多种同步机制,如互斥锁(std::mutex)、条件变量(std::condition_variable)等。

lock_guard与unique_lock

std::lock_guardstd::unique_lock是C++11中引入的RAII风格的锁管理类。它们通过构造函数自动加锁,析构函数自动解锁,有效避免了死锁问题。std::unique_lockstd::lock_guard更灵活,支持条件变量和定时锁。

std::mutex mtx;
std::condition_variable cv;
bool flag = false;void ThreadFunc() {std::unique_lock<std::mutex> lock(mtx);cv.wait(lock, [] { return flag; });std::cout << "ThreadFunc: Flag is true" << std::endl;
}int main() {std::thread t(ThreadFunc);{std::lock_guard<std::mutex> lock(mtx);flag = true;}cv.notify_one();t.join();return 0;
}

2.2死锁演示

死锁是多线程编程中常见的问题。以下是一个简单的死锁示例:

void func()
{srand(time(0));if (rand() % 2 == 0){throw exception("异常");}else{cout << "func()" << endl;}
}
//
//RAII
template<class Lock>
class LockGuard
{
public:LockGuard(Lock& lk):_lk(lk){_lk.lock();}~LockGuard(){_lk.unlock();}
private:Lock& _lk;
};//lock_guard
//unique_lock
// a. 可以跟time_mutex配合使用
// b. 支持手动解锁, 再加锁
// RTTI 运行阶段类型识别int main()
{mutex mtx;size_t n1 = 10000;size_t n2 = 10000;size_t x = 0;thread t1([n1, &x, &mtx]() {try {for (size_t i = 0; i < n1; i++){mtx.lock();//LockGuard<mutex> lg(mtx);/*lock_guard<mutex> lg(mtx);*/++x;func();//mtx.lock();}}catch (const exception& e){cout << e.what() << endl;}});thread t2([n2, &x, &mtx]() {for (size_t i = 0; i < n2; i++){mtx.lock();++x;mtx.unlock();}});t1.join();t2.join();cout << x << endl;return 0;
}

代码死锁的原因是线程t1func()函数可能抛出异常,导致mtx.lock()之后未能执行mtx.unlock(),从而使得线程t2无法获取锁,进而导致死锁。

三、原子操作

3.1 原子操作与线程安全

原子操作是解决线程安全问题的有效手段之一。C++11引入了std::atomic类模板,提供了对任意类型变量的原子操作支持。

3.2 原子操作的优势

原子操作确保了变量的读写操作不会被中断,从而避免了竞态条件。与互斥锁相比,原子操作的开销更小,适用于临界区较短的场景。

std::atomic<int> x(0);void Increment() {for (int i = 0; i < 1000; i++) {x++;}
}int main() {std::thread t1(Increment);std::thread t2(Increment);t1.join();t2.join();std::cout << x << std::endl;return 0;
}

3.3 CAS操作与自旋锁

std::atomic内部通常基于硬件的CAS(Compare-And-Swap)操作实现。CAS操作通过比较和交换机制确保操作的原子性。以下是一个CAS操作的示例:

std::atomic<int> x(0);void CASIncrement() {for (int i = 0; i < 1000000; i++) {int old, newval;do {old = x.load();newval = old + 1;} while (!x.compare_exchange_weak(old, newval));}
}int main() {std::thread t1(CASIncrement);std::thread t2(CASIncrement);t1.join();t2.join();std::cout << x << std::endl;return 0;
}

3.4 原子操作与普通操作的汇编对比

原子操作在底层通常通过特定的CPU指令实现。例如,x++操作在普通情况下可能被编译器拆分为多个指令,而在原子操作中,x++会被编译为一条不可分割的指令,从而确保操作的原子性。

四、共享资源的线程安全问题

共享资源的线程安全问题一直是并发编程中的难点。std::shared_ptr是一个典型的共享资源,其引用计数需要确保线程安全。

4.1std::shared_ptr的线程安全问题

std::shared_ptr本身是线程安全的,但其保护的资源可能不是线程安全的。例如,多个线程同时访问std::shared_ptr指向的对象时,可能会导致数据竞争。

std::shared_ptr<int> sp = std::make_shared<int>(0);void IncrementShared() {for (int i = 0; i < 1000000; i++) {(*sp)++;}
}int main() {std::thread t1(IncrementShared);std::thread t2(IncrementShared);t1.join();t2.join();std::cout << *sp << std::endl;return 0;
}

在这个例子中,std::shared_ptr的引用计数是线程安全的,但(*sp)++操作不是线程安全的。为了解决这个问题,可以使用互斥锁保护对共享资源的访问。

4.2 使用原子类型保护共享资源

另一种解决方案是使用原子类型保护共享资源。例如,可以将std::shared_ptr的引用计数改为std::atomic<int>,从而确保引用计数的线程安全。

std::shared_ptr<int> sp = std::make_shared<int>(0);
std::atomic<int> count(0);void IncrementShared() {for (int i = 0; i < 1000000; i++) {count.fetch_add(1);}
}int main() {std::thread t1(IncrementShared);std::thread t2(IncrementShared);t1.join();t2.join();std::cout << count << std::endl;return 0;
}

五、单例模式的线程安全问题

  单例模式是一种常见的设计模式,但在多线程环境中,单例模式的线程安全问题需要特别关注。

  懒汉模式的线程安全问题

5.1 懒汉模式的单例模式

  在多线程环境中可能会导致多个实例被创建。为了解决这个问题,可以使用双重检查锁定(Double-Checked Locking)或std::call_once
  下面是一个使用静态局部变量实现线程安全单例模式的 C++ 示例。

class Singleton {
public:static Singleton& GetInstance() {static Singleton instance;return instance;}private:Singleton() {}Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;
};int main() {Singleton s1 = Singleton::GetInstance();Singleton s2 = Singleton::GetInstance();assert(s1 == s2); // 验证单例return 0;
}

5.2 静态变量初始化的线程安全性

  C++11之后,静态变量初始化的线程安全性得到了显著改善。编译器会自动为静态局部变量的初始化提供互斥锁,确保其在多线程环境中的线程安全。这意味着在单例模式中,使用静态局部变量的方式是最简单且且最安全的实现方式。
  这是因为静态局部变量的初始化在C++11之后,由编译器自动保证线程安全。编译器会在静态局部变量的初始化过程中插入互斥锁机制,确保在多线程环境下,即使多个线程同时访问该静态局部变量,也只会有一个线程能够初始化它,


http://www.mrgr.cn/news/95362.html

相关文章:

  • 深度解读DeepSeek:源码解读 DeepSeek-V3
  • STM32八股【2】-----ARM架构
  • 面试康复训练-SQL语句
  • 如何为在线游戏选择合适的游戏盾?
  • 【数据结构】栈(Stack)、队列(Queue)、双端队列(Deque) —— 有码有图有真相
  • Maven安装与环境配置
  • 经典笔试题 小于 n 的最大整数 贪心 回溯 剪枝 全排列
  • 【yolo】使用 Netron 可视化深度学习模型:从 YOLOv1 到 YOLOv8 的探索
  • 【C++11】左值引用、右值引用、移动语义和完美转发
  • CentOS 7 64位安装Docker
  • 【UI设计】一些好用的免费图标素材网站
  • 【Agent】Dify Docker 安装问题 INTERNAL SERVER ERROR
  • sgpt 终端使用指南
  • 西门子200smart之modbus_TCP(做主站与第三方设备)通讯
  • Mysql表的增删改查
  • SpringBoot有几种获取Request对象的方法
  • win系统上自动化安装配置WSL linux和各种生信工具教程
  • Unity Shader编程】之透明物体渲染
  • springboot整合mybatis-plus【详细版】
  • 每日一题--C与C++的差别