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

lesson9: C++多线程

1.线程库

1.1 thread类的简单介绍

C++11 中引入了对 线程的支持 了,使得 C++ 并行编程时 不需要依赖第三方库
而且在原子操作中还引入了 原子类 的概念。要使用标准库中的线程,必须包含 < thread > 头文件
函数名
功能
thread()
构造一个线程对象,没有关联任何线程函数,即没有启动任何线程
thread(fn, args1, args2, ...)
构造一个线程对象,并关联线程函数fn,args1,args2,...为线程函数的
参数
get_id()
获取线程id
jionable()
线程是否还在执行,joinable代表的是一个正在执行中的线程。
jion()
该函数调用后会 阻塞住线程 ,当该线程结束后,主线程继续执行
detach()
在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离
的线程变为后台线程,创建的线程的"死活"就与主线程无关
  1. 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的
    状态。
  2. 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程

1.2 线程对象关联线程函数

#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.函数指针2.lambda表达式3.函数对象
  • 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程

1.2.1 注意

  1. thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以 移动构造 移动赋值 ,即将一个
    线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。
  2. 可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效
    1. 采用无参构造函数构造的线程对象
    2. 线程对象的状态已经转移给其他线程对象
    3. 线程已经调用jion或者detach结束

1.3 线程函数参数

#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<vector>
#include<atomic>
using namespace std;void Print(int n, int& x,mutex& mtx)
{for (int i = 0; i < n; ++i){mtx.lock();cout <<this_thread::get_id()<<":"<< i << endl;std::this_thread::sleep_for(std::chrono::milliseconds(100));++x;mtx.unlock();}}int main()
{mutex m;int count = 0;thread t1(Print, 10, ref(count),ref(m));thread t2(Print, 10, ref(count),ref(m);t1.join();t2.join();cout << count << endl;return 0;
}

  •  线程函数的参数先传递给thread的,并以值拷贝的方式拷贝到线程栈空间中的
  • 如果不给线程函数的参数不借助 ref函数
    • 即使线程参数为 引用类型 ,在线程中修改后也 不能修改外部实参
    • 因为其实际引用的是线程栈中的拷贝,而不是外部实参

#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<vector>
#include<atomic>
using namespace std;int main()
{mutex mtx;int x = 0;int n = 10;int m;cin >> m;vector<thread> v(m);//v.resize(m);for (int i = 0; i < m; ++i){// 移动赋值给vector中线程对象v[i] = thread([&](){for (int i = 0; i < n; ++i){mtx.lock();cout << this_thread::get_id() << ":" << i << endl;std::this_thread::sleep_for(std::chrono::milliseconds(100));++x;mtx.unlock();}});}for (auto& t : v){t.join();}cout << x << endl;return 0;
}
  •  借助lambda表达式中的引用捕捉也可以实现上面那个函数,就可以不用借助ref函数

1.3.1 线程并行 && 并发的讨论 

  • 并行:任务的同时进行
  • 并发: 任务的调动和切换
  • 在这个函数中其实是并行的速度更快,因为线程切换十分耗时间

1.4 原子性操作库(atomic) 

多线程最主要的问题是共享数据带来的问题 ( 即线程安全 )
当一个或多个线程要 修改 共享数据时,就会产生很多潜在的麻烦
#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<vector>
#include<atomic>
using namespace std;int main()
{mutex mtx;atomic<int> x = 0;// int x = 0;int n = 1000000;int m;cin >> m;vector<thread> v(m);for (int i = 0; i < m; ++i){// 移动赋值给vector中线程对象v[i] = thread([&](){for (int i = 0; i < n; ++i){// t1 t2 t3 t4++x;}});}for (auto& t : v){t.join();}cout << x << endl;return 0;
}

 

  •  C++98中传统的解决方式:可以对共享修改的数据加锁保护
    • 加锁的问题: 这个线程执行的时候, 其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁
  • C++11 中使用atomic类模板,定义出需要的任意原子类型
    • 程序员 不需要 对原子类型变量进行 加锁解锁 操作,线程能够对原子类型变量互斥的访问。

1.4.1 注意 

#include <atomic>
int main()
{atomic<int> a1(0);//atomic<int> a2(a1);   // 编译失败atomic<int> a2(0);//a2 = a1;               // 编译失败return 0;
}
  • 原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,
  • 因此在C++11 中,原子类型只能从其模板参数中进行构造不允许原子类型进行拷贝构造、移动构造以及 operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了 

1.5 lock_guardunique_lock  

多线程 环境下, 原子性 只能保证 某个变量的安全性

多线程环境下,而需要保证一段代码的安全性,就只能通过加锁的方式实现

1.5.1  lock_guard

#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<vector>
#include<atomic>
using namespace std;//RAII
template<class Lock>
class LockGuard
{
public:LockGuard(Lock& lk):_lock(lk){_lock.lock();cout << "thread:" << this_thread::get_id() << "加锁" << endl;}~LockGuard(){cout << "thread:" << this_thread::get_id() << "解锁" << endl << endl;_lock.unlock();}
private:Lock& _lock;// 成员变量是引用
};int main()
{mutex mtx;atomic<int> x = 0;//int x = 0;int n = 100;int m;cin >> m;vector<thread> v(m);for (int i = 0; i < m; ++i){// 移动赋值给vector中线程对象v[i] = thread([&](){for (int i = 0; i < n; ++i){{lock_guard<mutex> lk(mtx);cout << this_thread::get_id() << ":" << i << endl;}std::this_thread::sleep_for(std::chrono::milliseconds(100));}});}for (auto& t : v){t.join();}cout << x << endl;return 0;
}
  •  lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封
  • 调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。
  • lock_guard 缺陷 太单一,用户没有办法对该锁进行控制

1.5.2 unique_lock 

lock_guard 不同的是, unique_lock 更加的灵活,提供了更多的成员函数
  • 上锁/解锁操作locktry_locktry_lock_fortry_lock_untilunlock
  • 修改操作:移动赋值、交换(swap:与另一个unique_lock对象互换所管理的互斥量所有 )、释放(release:返回它所管理的互斥量对象的指针,并释放所有权)
  • 获取属性owns_lock(返回当前对象是否上了锁)operator bool()(owns_lock()的功能相 )mutex(返回当前unique_lock所管理的互斥量的指针)

1.6 支持两个线程交替打印,一个打印奇数,一个打印偶数

1.6.1 错误案例

#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<vector>
#include<atomic>
using namespace std;int main()
{int i = 0;int n = 100;mutex mtx;thread t1([&](){while (i < n){mtx.lock();cout << this_thread::get_id() << ":" << i << endl;i += 1;mtx.unlock();}});this_thread::sleep_for(chrono::microseconds(100));thread t2([&](){while (i < n){mtx.lock();cout << this_thread::get_id() << ":" << i << endl;i += 1;mtx.unlock();}});t1.join();t2.join();return 0;
}

  • 在线程切换的中间时间也会发现线程竞争抢锁的问题 

1.6.2 正确案例 

#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<vector>
#include<atomic>
using namespace std;int main()
{int i = 0;int n = 100;mutex mtx;condition_variable cv;// 条件变量bool ready = true;// t1打印奇数thread t1([&](){while (i < n){{unique_lock<mutex> lock(mtx);cv.wait(lock, [&ready](){return !ready; });// 等待线程cout << "t1--" << this_thread::get_id() << ":" << i << endl;i += 1;ready = true;cv.notify_one();// 解除线程等待}//this_thread::yield();this_thread::sleep_for(chrono::microseconds(100));}});// t2打印偶数thread t2([&]() {while (i < n){unique_lock<mutex> lock(mtx);cv.wait(lock, [&ready](){return ready; });cout <<"t2--"<<this_thread::get_id() << ":" << i << endl;i += 1;ready = false;cv.notify_one();}});this_thread::sleep_for(chrono::seconds(3));cout << "t1:" << t1.get_id() << endl;cout << "t2:" << t2.get_id() << endl;t1.join();t2.join();return 0;
}

 

  • cv.wait(lock, [&ready]() {return !ready; });
    • ready返回的是false时,这个线程就会阻塞
    • 阻塞当前线程,并自动调用lock.unlock()允许其他锁定的线程继续执行
  •  cv.notify_one();
    • 唤醒当前线程并自动调用lock.lock();就只允许自己一个线程执行

1.7 shared_ptr的多线程问题

#include<iostream>
#include<thread>
#include<mutex>
#include<vector>
#include<atomic>
#include<memory>
using namespace std;namespace bit
{template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr), _pRefCount(new int(1)), _pMutex(new mutex){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pRefCount(sp._pRefCount), _pMutex(sp._pMutex){AddRef();}void Release(){bool flag = false;_pMutex->lock();if (--(*_pRefCount) == 0 && _ptr){cout << "delete:" << _ptr << endl;delete _ptr;delete _pRefCount;flag = true;}_pMutex->unlock();if (flag)delete _pMutex;}void AddRef(){_pMutex->lock();++(*_pRefCount);_pMutex->unlock();}shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._ptr){Release();_ptr = sp._ptr;_pRefCount = sp._pRefCount;_pMutex = sp._pMutex;AddRef();}return *this;}int use_count(){return *_pRefCount;}~shared_ptr(){Release();}// 像指针一样使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get() const{return _ptr;}private:T* _ptr;int* _pRefCount;// 使用时需要加锁mutex* _pMutex;// 锁指针};
}int main()
{// shared_ptr是线程安全的吗?bit::shared_ptr<double> sp1(new double(1.11));bit::shared_ptr<double> sp2(sp1);mutex mtx;vector<thread> v(2);int n = 100000;for (auto& t : v){t = thread([&](){for (size_t i = 0; i < n; ++i){// 拷贝是线程安全的bit::shared_ptr<double> sp(sp1);// 访问资源不是mtx.lock();(*sp)++;mtx.unlock();}});}for (auto& t : v){t.join();}cout << sp1.use_count() << endl;cout << *sp1 << endl;return 0;
}
  •  在多线程中,shared_ptr也应该对自己的引用计数进行加锁处理

  • 在多线程中, shared_ptr拷贝是线程安全的,但访问资源不是,所以访问资源也需要加锁

1.8 单例模式的多线程问题 

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
class Singleton
{
public:static Singleton* GetInstance(){// 保护第一次,后续不需要加锁// 双检查加锁if (_pInstance == nullptr){unique_lock<mutex> lock(_mtx);if (_pInstance == nullptr){_pInstance = new Singleton;}}return _pInstance;}private:// 构造函数私有Singleton(){};// C++11Singleton(Singleton const&) = delete;Singleton& operator=(Singleton const&) = delete;static Singleton* _pInstance;static mutex _mtx;
};Singleton* Singleton::_pInstance = nullptr;
mutex Singleton::_mtx; int main()
{Singleton::GetInstance();Singleton::GetInstance();return 0;
}
  •  在多线程的情况下, 第一次创建对象时也是需要加锁保护

1.8.1 巧妙的解决方案

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;class Singleton
{
public:static Singleton* GetInstance(){static Singleton _s;// 局部的静态对象,第一次调用时初始化return &_s;}private:// 构造函数私有Singleton() {};// C++11Singleton(Singleton const&) = delete;Singleton& operator=(Singleton const&) = delete;
};int main()
{Singleton::GetInstance();Singleton::GetInstance();return 0;
}
  • 局部的静态对象,第一次调用时初始化
  • 在C++11之前是不能保证线程安全的
    静态对象的构造函数调用初始化并不能保证线程安全的原子性
  • C++11的时候修复了这个问题,所以这种写法,只能在支持C++11以后的编译器上玩
http://www.lryc.cn/news/134909.html

相关文章:

  • 安卓修改SwitchCompat色值
  • pytorch内存泄漏
  • 20230821-字符串相乘-给树命名(unordered_map)
  • [Go版]算法通关村第十二关黄金——字符串冲刺题
  • neovim为工作区添加本地clangd配置
  • 信号处理--基于EEG脑电信号的眼睛状态的分析
  • Redis高可用:主从复制详解
  • [Flutter]有的时候调用setState(() {})报错?
  • 利用屏幕水印学习英语单词,无打扰英语单词学习
  • 开学必备物品清单!这几款优先考虑!
  • 聊聊调制解调器
  • Go语言入门指南:基础语法和常用特性(下)
  • 【MFC常用问题记录】
  • ThreadLocal内存泄漏问题
  • 微服务基础概念【内含图解】
  • Dockerfile创建 LNMP 服务+Wordpress 网站平台
  • 消息中间件篇
  • 基本定时器
  • MySQL 中文全文检索
  • Redis——list类型详解
  • npm 安装 git 仓库包
  • 问题来了!你知道你穿的防砸劳保鞋的保护包头都是什么材料
  • 计算机网络-物理层(三)编码与调制
  • Linux面试笔试题(6)
  • qt中窗口的布局
  • 玄子Share - HTML Emmet 语法详细介绍
  • Linux上安装和使用git到gitoschina和github上_亲测
  • 合宙Air724UG LuatOS-Air LVGL API--简介
  • 「Vue|网页开发|前端开发」01 快速入门:用vue-cli快速写一个Vue的HelloWorld项目
  • 7.5.tensorRT高级(2)-RAII接口模式下的生产者消费者多batch实现