从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)(上)

简介: 从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)

此篇建议学了Linux系统多线程部分再来看。

1. C++多线程

       在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差。

       C++11中最重要的特性就是支持了多线程编程,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。


1.1 thread库

查下文档:


如图所示,C++11提供了thread库,thread是一个类,在使用的时候需要包含头文件pthread。

构造函数:

  • 默认构造函数thread()使用该构造函数创建的线程对象仅是创建对象,线程并没有被创建,也没有允许。
  • thread(Fn&& fn, Args&&... args),这是一个万能引用模板。使用该构造函数时,第一个参数是可调用对象,可以是左值也可以是右值,比如函数指针,仿函数对象,lambda表达式等等。后面的可变参数就是传给线程函数的实参,是一个参数包,也就是可变参数。
  • thread(const thread&) = delete,线程之间是禁止拷贝的。
  • thread(thread&& x),移动构造函数。

成员函数:

  • get_id,用来获取当前线程的tid值。调用该函数通常都是当前线程,但是当前的从线程从并没有自己的thread对象

       所以线程库由提供了一个命名空间,该空间中有上图所示的几个函数,可以通过命名空间来直接调用,如:

this_thread::get_id(); // 获取当前线程tid值

哪个线程执行这条语句就返回哪个线程的tid值,命名空间中的其他几个函数的用法也是这样。

  • yield调用该接口的线程会让其CPU,让CPU调度其他线程。
  • sleep_until调用该接口的线程会延时至一个确定的时间点。
  • sleep_for调用该接口的线程会延时一个时间段,如1s。
  • operator=(thread&& t),移动赋值。

将一个线程对象赋值给另一个线程对象,通常这么用:

  thread t1; // 仅创建对象,不创建线程
  t1 = thread(func); // t1线程函数并且执行

此时原本只创建的线程对象就有一个线程在跑了。

注意:只能赋右值,不能赋左值,因为赋值运算符重载被禁掉了,只有移动赋值


  • join,线程等待,用来回收线程资源。一般主线程会调用该函数,以t.join()的形式,t就是需要被等待的线程对象,此时主线程会阻塞在这里,直到从线程运行结束。

     如上面的多线程一样,必须使用join,否则线程资源不会回收,而且如果从线程运行的时间比主线程长的话,主线程会直接运行完并且回收所有资源,导致从线程被强制结束。


  • joinable,用来判断线程是否有效。

如果是以下任意情况则线程无效:

  1. 采用无参构造函数构造的线程对象
  2. 线程对象的状态已经转移给其他线程对象
  3. 线程已经调用 join 或者 detach 结束
  • detach,线程分离,从线程结束后自动回收资源。

其他的就不介绍了,用到的时候自行查文档即可。

要谨记:thread是禁止拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值


使用一下:

#include <iostream>
#include <thread>
using namespace std;
 
void Print(int n, int& x)
{
  for (int i = 0; i < n; ++i)
  {
    cout << this_thread::get_id() << ":" << i << endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    ++x;
  }
}
int main()
{
  int count = 0;
  thread t1(Print, 10, ref(count)); // 可调用对象和其实参
  thread t2(Print, 10, ref(count));
 
  t1.join();
  t2.join();
 
  cout << count << endl;
 
  return 0;
}

       多次运行的结果不一样,可能会出现像第一行一样的抢着打印的问题(学了Linux多线程应该比较清楚),下面就应该想到加锁了。


1.2 mutex库

如上图所示,C++11提供了mutex库,mutex同样是一个类,在使用的时候要包含头文件mutex。

构造函数:

  • 只有默认构造函数mutex(),在创建互斥锁的时候不需要传任何参数。
  • mutex(const mutex&)=delete,禁止拷贝。

其他成员函数:

  • lock(),给临界区加锁,加锁成功继续向下执行,失败则阻塞等待。
  • unlock(),给临界区解锁。
  • try_lock(),给临界区尝试加锁,加锁成功返回true,加锁失败返回false。使用try_lock时,如果申请失败则不阻塞,跳过申请锁的部分,执行非临界区代码。来看伪代码:
mutex mtx;
 
if(mtx.try_lock())
{
  // 临界区代码
  // ......
}
else
{
  // 非临界区代码
  // ......
}

mutex不能递归使用,如下面伪代码所示:

    void Func(int n)
  {
    lock(); // 加锁
    // 临界区代码
    // ......
    Func(n - 1); // 递归调用
    unlock(); // 解锁
  }


在递归中不能使用这样的锁,会造成死锁。

最上面例子的正确使用如下:

#include <iostream>
#include <thread>
#include <mutex>
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;
}

后面再来看看怎么实现交错打印的效果,再看看另一种用法:(lambda)

int main()
{
  mutex mtx;
  int x = 0;
  int n = 10;
  thread t1([&](){
    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();
    }
  });
 
  thread t2([&](){
    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();
    }
  });
 
  t1.join();
  t2.join();
  cout << x << endl;
  return 0;
}

上面代码的问题:如果加锁解锁之间存在抛异常就死锁了,这时就要用到RAII锁。


1.3 RAII锁

lock_guard是一个类,采用了RAII方式来加锁解锁——将锁的生命周期和对象的生命周期绑定在一起。看下在Linux篇章写的代码:(把锁封装了)

#pragma once
#include <iostream>
#include <pthread.h>
 
class Mutex
{
public:
    Mutex(pthread_mutex_t* mtx) 
        :_pmtx(mtx)
    {}
    void lock()
    {
        pthread_mutex_lock(_pmtx);
        std::cout << "进行加锁成功" << std::endl;
    }
    void unlock()
    {
        pthread_mutex_unlock(_pmtx);
        std::cout << "进行解锁成功" << std::endl;
    }
    ~Mutex()
    {}
protected:
    pthread_mutex_t* _pmtx;
};
 
class lockGuard // RAII风格的加锁方式
{
public:
    lockGuard(pthread_mutex_t* mtx) // 因为不是全局的锁,所以传进来,初始化
        :_mtx(mtx)
    {
        _mtx.lock();
    }
    ~lockGuard()
    {
        _mtx.unlock();
    }
protected:
    Mutex _mtx;
};

看库里的构造函数:

  • lock_guard(mutex_type& m),在创建这个对象的时候需要传入一把锁,在构造函数中,进行了加锁操作。
  • lcok_guard(const lock_guard&)=delete,该对象禁止拷贝,因为互斥锁就不可以拷贝。

析构函数的作用就是将lock_guard对象的资源释放,也就是进行解锁操作。

lock_guard只有构造函数和析构函数,使用该类对象加锁时不需要我们去关心锁的释放,但是它不能在对象生命周期结束之前主动解锁。

看一下unique_lock:

unique_lock也是一种RAII的加锁对象,它和lock_guard的功能一样,将锁的生命周期和对象的生命周期绑定在一起,但是又有区别。

  • unique_lock(mutex_type& m),这个和lock_guard的用法一样,在构造函数中加锁。
  • unique_lock(const unique_lock&)=delete,同样禁止拷贝。

析构函数中和lock_guard一样,也是进行解锁操作。

  • lock,加锁。
  • unlock,解锁。
  • try_lock,尝试加锁。

lock_guard中就没有这几个接口,所以unique_lock可以在析构之前主动解锁,主动解锁后仍然可以再主动加锁,这一点lock_guard是不可以的。

  • try_lock_for,尝试加锁一段时间,时间到后自动解锁。
  • try_lock_until,尝试加锁到指定时间,时间到来后自动解锁。

用法很多,需要使用的时候可以结合库文档来使用。用一下lock_guard和lambda的另一种用法:

int main()
{
  mutex mtx;
  int n = 10;
  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();
  }
  return 0;
}

从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)(中):https://developer.aliyun.com/article/1522534

目录
相关文章
|
5月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
225 0
|
2月前
|
缓存 算法 程序员
C++STL底层原理:探秘标准模板库的内部机制
🌟蒋星熠Jaxonic带你深入STL底层:从容器内存管理到红黑树、哈希表,剖析迭代器、算法与分配器核心机制,揭秘C++标准库的高效设计哲学与性能优化实践。
C++STL底层原理:探秘标准模板库的内部机制
|
9月前
|
编译器 C++ 容器
【c++丨STL】基于红黑树模拟实现set和map(附源码)
本文基于红黑树的实现,模拟了STL中的`set`和`map`容器。通过封装同一棵红黑树并进行适配修改,实现了两种容器的功能。主要步骤包括:1) 修改红黑树节点结构以支持不同数据类型;2) 使用仿函数适配键值比较逻辑;3) 实现双向迭代器支持遍历操作;4) 封装`insert`、`find`等接口,并为`map`实现`operator[]`。最终,通过测试代码验证了功能的正确性。此实现减少了代码冗余,展示了模板与仿函数的强大灵活性。
264 2
|
5月前
|
数据采集 监控 调度
干货分享“用 多线程 爬取数据”:单线程 + 协程的效率反超 3 倍,这才是 Python 异步的正确打开方式
在 Python 爬虫中,多线程因 GIL 和切换开销效率低下,而协程通过用户态调度实现高并发,大幅提升爬取效率。本文详解协程原理、实战对比多线程性能,并提供最佳实践,助你掌握异步爬虫核心技术。
|
9月前
|
存储 算法 C++
【c++丨STL】map/multimap的使用
本文详细介绍了STL关联式容器中的`map`和`multimap`的使用方法。`map`基于红黑树实现,内部元素按键自动升序排列,存储键值对,支持通过键访问或修改值;而`multimap`允许存在重复键。文章从构造函数、迭代器、容量接口、元素访问接口、增删操作到其他操作接口全面解析了`map`的功能,并通过实例演示了如何用`map`统计字符串数组中各元素的出现次数。最后对比了`map`与`set`的区别,强调了`map`在处理键值关系时的优势。
494 73
|
6月前
|
Java 数据挖掘 调度
Java 多线程创建零基础入门新手指南:从零开始全面学习多线程创建方法
本文从零基础角度出发,深入浅出地讲解Java多线程的创建方式。内容涵盖继承`Thread`类、实现`Runnable`接口、使用`Callable`和`Future`接口以及线程池的创建与管理等核心知识点。通过代码示例与应用场景分析,帮助读者理解每种方式的特点及适用场景,理论结合实践,轻松掌握Java多线程编程 essentials。
405 5
|
11月前
|
监控 Kubernetes Java
阿里面试:5000qps访问一个500ms的接口,如何设计线程池的核心线程数、最大线程数? 需要多少台机器?
本文由40岁老架构师尼恩撰写,针对一线互联网企业的高频面试题“如何确定系统的最佳线程数”进行系统化梳理。文章详细介绍了线程池设计的三个核心步骤:理论预估、压测验证和监控调整,并结合实际案例(5000qps、500ms响应时间、4核8G机器)给出具体参数设置建议。此外,还提供了《尼恩Java面试宝典PDF》等资源,帮助读者提升技术能力,顺利通过大厂面试。关注【技术自由圈】公众号,回复“领电子书”获取更多学习资料。
|
10月前
|
存储 缓存 C++
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
C++ 标准模板库(STL)提供了一组功能强大的容器类,用于存储和操作数据集合。不同的容器具有独特的特性和应用场景,因此选择合适的容器对于程序的性能和代码的可读性至关重要。对于刚接触 C++ 的开发者来说,了解这些容器的基础知识以及它们的特点是迈向高效编程的重要一步。本文将详细介绍 C++ 常用的容器,包括序列容器(`std::vector`、`std::array`、`std::list`、`std::deque`)、关联容器(`std::set`、`std::map`)和无序容器(`std::unordered_set`、`std::unordered_map`),全面解析它们的特点、用法
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
|
9月前
|
存储 算法 C++
【c++丨STL】set/multiset的使用
本文深入解析了STL中的`set`和`multiset`容器,二者均为关联式容器,底层基于红黑树实现。`set`支持唯一性元素存储并自动排序,适用于高效查找场景;`multiset`允许重复元素。两者均具备O(logN)的插入、删除与查找复杂度。文章详细介绍了构造函数、迭代器、容量接口、增删操作(如`insert`、`erase`)、查找统计(如`find`、`count`)及`multiset`特有的区间操作(如`lower_bound`、`upper_bound`、`equal_range`)。最后预告了`map`容器的学习,其作为键值对存储的关联式容器,同样基于红黑树,具有高效操作特性。
398 3
|
10月前
|
Python
python3多线程中使用线程睡眠
本文详细介绍了Python3多线程编程中使用线程睡眠的基本方法和应用场景。通过 `time.sleep()`函数,可以使线程暂停执行一段指定的时间,从而控制线程的执行节奏。通过实际示例演示了如何在多线程中使用线程睡眠来实现计数器和下载器功能。希望本文能帮助您更好地理解和应用Python多线程编程,提高程序的并发能力和执行效率。
380 20

热门文章

最新文章