【C++】C++多线程库的使用(1)

简介: 【C++】C++多线程库的使用

在C++11之前,涉及到多线程问题,都是和平台相关的,比如windowslinux下各有自己的接口,这使得代码的可移植性比较差,如果想要多平台能够同时运行就要使用条件编译写两份代码。

C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含头文件。

一、线程库(thread)

使用线程库,必须包含 < thread > 头文件。

1、线程的id类

在了解线程库之前我们先讲一个前置知识,我们知道每个线程都要有自己的线程id,在线程库类中有一个内嵌类型id类,此类是用数字表示的是线程id,并且此id类支持比较和流插入运算符

2、线程对象的构造

线程对象的构造函数如下:

  1. thread提供了无参的构造函数,调用无参的构造函数创建出来的线程对象没有关联任何线程函数,内部只有线程对象的相关属性但是没有启动任何线程, 此时线程的id0
  2. thread的带参的构造函数,是一个可变参数模板函数,参数说明如下:
  • fn:可调用对象,比如函数指针、仿函数、lambda表达式、被包装器包装后的可调用对象等。
  • args... : 调用可调用对象fn时所需要的若干参数。
  1. 线程是不允许拷贝的,所以其拷贝函数是delete的。
  • 同理线程对象也只允许移动赋值
  1. thread提供了移动赋值函数,因此当后续需要让该无参的线程对象与线程关联时,可以以带参的方式创建一个匿名对象,然后调用移动赋值将该匿名对象关联线程转移给该无参的线程对象。
void func(int n)
{
  for (int i = 0; i <= n; i++)
  {
    cout << i << endl;
  }
}
int main()
{
  // 创建无参的线程对象
  thread t1;
  // 移动构造
  t1 = thread(func, 10);
  t1.join();
  return 0;
}

3、thread提供的其他成员函数

thread中常用的成员函数如下:

成员函数 功能
join 对该线程进行等待,在等待的线程返回之前,调用join函数的线程将会被阻塞
joinable 判断该线程是否已经执行完毕,如果是则返回true,否则返回false
detach 将该线程与创建线程进行分离,被分离后的线程不再需要创建线程调用join函数对其进行等待
get_id 获取该线程的id
swap 将两个线程对象关联线程的状态进行交换

说明:

  • thread类是防拷贝的,不允许拷贝构造和拷贝赋值,但是可以移动构造和移动赋值,可以将一个线程对象关联线程的状态转移给其他线程对象,并且转移期间不影响线程的执行。
  • 线程结束以后其线程id会变为0
  • 对同一个线程join两次会导致程序崩溃

4、this_thread命名空间

this_threadstd命名空间下的一个子命名空间,此命名空间提供了访问当前线程的一组函数。

函数 功能
get_id 获得当前线程的id
yield 当前线程“放弃”执行,让出时间片,让操作系统调度另一线程继续执行
sleep_until 让当前线程休眠到一个具体时间点
sleep_for 让当前线程休眠一个时间段

我们调用thread的成员函数get_id可以获取线程的id,但该方法必须通过线程对象来调用get_id函数,如果要在线程对象关联的线程函数中获取线程id,可以调用this_thread命名空间下的get_id函数。

void func()
{
  //获取线程id
  cout << this_thread::get_id() << endl;
}
int main()
{
  thread t(func);
  t.join();
  return 0;
}

5、线程函数的参数问题

线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,就算线程函数的参数为引用类型,在线程函数中修改后也不会影响到外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。比如:

void add(int& num)
{
  num = 10;
}
int main()
{
  int num = 0;
  thread t(add, num);
  t.join();
  cout << num << endl; //0
  return 0;
}

ps :这段代码可能在vs等编译器下报错,不过不用在意,这种写法的使用也不正确

如果我们想要通过形参改变外部实参时,必须采用以下3种方式:

  1. 如果是引用,传参时可以借助std::ref()函数。
  2. 通过地址的拷贝,利用解引用来修改实参
  3. 利用lambda函数的捕捉列表,以引用的方式对外部实参进行捕捉
#include <thread>
void ThreadFunc1(int& x)
{
  x += 10;
}
void ThreadFunc2(int* x)
{
  *x += 10;
}
int main()
{
  int a = 10;
  // 如果想要通过形参改变外部实参时,必须借助std::ref()函数
  thread t1(ThreadFunc1, std::ref(a));
  t1.join();
  cout << a << endl;
  // 利用地址的拷贝,也能改变外部实参
  thread t2(ThreadFunc2, &a);
  t2.join();
  cout << a << endl;
  // 利用lambda表达式的捕捉列表
  thread t3([&] {a += 10; });
  t3.join();
  cout << a << endl;
  return 0;
}

二、互斥量库(mutex)

使用互斥量库,必须包含 < mutex > 头文件。

1、mutex的种类

在C++11中,mutex总共包了四个互斥量的种类:


  • mutex

mutex锁是C++11提供的最基本的互斥量,mutex对象之间不能进行拷贝。

mutex中常用的成员函数如下:

成员函数 功能
lock 对互斥量进行加锁
try_lock 尝试对互斥量进行加锁,如果加锁失败就立即返回,不会阻塞在锁上
unlock 对互斥量进行解锁,释放互斥量的所有权

线程函数调用lock时,可能会发生以下三种情况:

  • 如果该互斥量当前没有被其他线程锁住,则调用线程将该互斥量锁住,直到调用unlock之前,该线程一致拥有该锁。
  • 如果该互斥量已经被其他线程锁住,则当前的调用线程会被阻塞。
  • 如果该互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

线程调用try_lock时,类似也可能会发生以下三种情况:

  • 如果该互斥量当前没有被其他线程锁住,则调用线程将该互斥量锁住,直到调用unlock之前,该线程一致拥有该锁。
  • 如果该互斥量已经被其他线程锁住,则try_lock调用返回false,当前的调用线程不会被阻塞。
  • 如果该互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

  • recursive_mutex

recursive_mutex叫做递归互斥锁,该锁专门用于递归函数中的加锁操作。

  • 如果在递归函数中使用mutex互斥锁进行加锁,那么在线程进行递归调用时,可能会重复申请已经申请到但自己还未释放的锁,进而导致死锁问题。
  • recursive_mutex允许同一个线程对互斥量多次上锁(即递归上锁),来获得互斥量对象的多层所有权,但是释放互斥量时需要调用与该锁层次深度相同次数的unlock

除此之外,recursive_mutex也提供了lock、try_lockunlock成员函数,其的特性与mutex大致相同。

我们看到下面的代码能够保证线程在递归时也能一直占有锁,只有当递归完成才会释放锁,两个线程对全局变量进行递归++操作。

#include <iostream>
#include <thread>
#include <mutex>
std::recursive_mutex mtx;
int  x = 0;
void recursiveFunction(int count)
{
    mtx.lock();
    std::cout << "Thread " << std::this_thread::get_id() << ": Lock acquired, count = "
        << count << std::endl;
    if (count > 0) 
    {
        x++;
        recursiveFunction(count - 1);
    }
    std::cout << "Thread " << std::this_thread::get_id() << ": Lock released, count = " 
        << count << std::endl;
    mtx.unlock();
}
int main()
{
    std::thread t1(recursiveFunction, 3);
    std::thread t2(recursiveFunction, 2);
    t1.join();
    t2.join();
    cout << "------------------------------------------" << endl;
    cout << "final result: " << x << endl;
    return 0;
}


  • timed_mutex

timed_mutex中提供了以下两个成员函数:

  • try_lock_for:接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间之内还是没有获得锁),则返回false。
  • try_lock_untill:接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间点到来时还是没有获得锁),则返回false。
  • 除此之外,timed_mutex也提供了lock、try_lockunlock成员函数,其的特性与mutex相同。

利用时间锁可以写一些有趣的程序,下面的运行结果是不能确定的

#include <iostream>       // std::cout
#include <chrono>         // std::chrono::milliseconds
#include <thread>         // std::thread
#include <mutex>          // std::timed_mutex
std::timed_mutex mtx;
void fireworks() 
{
    // 等待获得锁 : 每个线程每200ms打印"-"
    while (!mtx.try_lock_for(std::chrono::milliseconds(200))) 
    {
        std::cout << "-";
    }
    // 得到一个锁! 等待1秒,然后这个线程打印"*"
    std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    std::cout << "*\n";
    mtx.unlock();
}
int main()
{
    std::thread threads[10];
    // 启动 10 个线程:
    for (int i = 0; i < 10; ++i)
        threads[i] = std::thread(fireworks);
    // join 10个线程
    for (auto& th : threads) th.join();
    return 0;
}

  • std::recursive_timed_mutex

recursive_timed_mutex就是recursive_mutex和timed_mutex的结合,recursive_timed_mutex既支持在递归函数中进行加锁操作,也支持定时锁。


2、lock_guard和unique_lock

互斥锁的使用很简单,但是有些情况下互斥锁的使用可能会变得很棘手,例如:

  • 如果加锁的范围太大,那么极有可能在中途返回时忘记了解锁,那么以后其他线程申请这个互斥锁的就会被阻塞住,也就是造成了死锁问题。
  • 又或者是如果线程在锁的范围内抛异常,导致没有解锁,也很容易导致死锁问题。
#include <iostream>      
#include <thread>        
#include <mutex>
int x = 0;
mutex mtx;
void Func(int n)
{
  for (size_t i = 0; i < n; i++)
  {
    try
    {
      mtx.lock();
      ++x;
      cout << x << endl;
      if (rand() % 3 == 0)
      {
        throw exception("抛异常");
      }
      mtx.unlock();
    }
    catch (const std::exception& e)
    {
      cout << e.what() << endl;
    }
  }
}
int main()
{
  thread t1(Func, 10);
  thread t2(Func, 10);
  t1.join();
  t2.join();
  return 0;
}

为了解决上面的问题,C++11采用RAII的方式对锁进行了封装,于是就出现了lock_guardunique_lock


lock_guard

lock_guard是C++11中的一个模板类,其定义如下:

lock_guard类模板主要是通过RAII的方式,对其管理的互斥锁进行了封装。

  • 在需要加锁的地方,用互斥锁实例化一个lock_guard对象,在lock_guard的构造函数中会调用lock()进行加锁。
  • lock_guard对象出作用域前会调用析构函数,在lock_guard的析构函数中会调用unlock()自动解锁。
  • lock_guard对象定义到该对象析构,这段区域的代码都属于互斥锁的保护范围。
  • lock_guard类对象也是也是不支持拷贝的。

有了这种方法我们就不用害怕出现上面的情况了!

#include <iostream>              
#include <thread>     
#include <mutex>
int x = 0;
mutex mtx;
void Func(int n)
{
  for (size_t i = 0; i < n; i++)
  {
    try
    {
      lock_guard<mutex> lck(mtx);
      ++x;
      cout << x << endl;
      if (rand() % 3 == 0)
      {
        throw exception("抛异常");
      }
    }
    catch (const std::exception& e)
    {
      cout << e.what() << endl;
    }
  }
}
int main()
{
  thread t1(Func, 10);
  thread t2(Func, 10);
  t1.join();
  t2.join();
  return 0;
}

相关文章
|
2月前
|
API C++ Windows
Visual C++运行库、.NET Framework和DirectX运行库的作用及常见问题解决方案,涵盖MSVCP140.dll丢失、0xc000007b错误等典型故障的修复方法
本文介绍Visual C++运行库、.NET Framework和DirectX运行库的作用及常见问题解决方案,涵盖MSVCP140.dll丢失、0xc000007b错误等典型故障的修复方法,提供官方下载链接与系统修复工具使用指南。
626 2
|
5月前
|
负载均衡 算法 安全
基于Reactor模式的高性能网络库之线程池组件设计篇
EventLoopThreadPool 是 Reactor 模式中实现“一个主线程 + 多个工作线程”的关键组件,用于高效管理多个 EventLoop 并在多核 CPU 上分担高并发 I/O 压力。通过封装 Thread 类和 EventLoopThread,实现线程创建、管理和事件循环的调度,形成线程池结构。每个 EventLoopThread 管理一个子线程与对应的 EventLoop(subloop),主线程(base loop)通过负载均衡算法将任务派发至各 subloop,从而提升系统性能与并发处理能力。
307 3
|
2月前
|
Ubuntu API C++
C++标准库、Windows API及Ubuntu API的综合应用
总之,C++标准库、Windows API和Ubuntu API的综合应用是一项挑战性较大的任务,需要开发者具备跨平台编程的深入知识和丰富经验。通过合理的架构设计和有效的工具选择,可以在不同的操作系统平台上高效地开发和部署应用程序。
150 11
|
2月前
|
缓存 算法 程序员
C++STL底层原理:探秘标准模板库的内部机制
🌟蒋星熠Jaxonic带你深入STL底层:从容器内存管理到红黑树、哈希表,剖析迭代器、算法与分配器核心机制,揭秘C++标准库的高效设计哲学与性能优化实践。
C++STL底层原理:探秘标准模板库的内部机制
|
2月前
|
IDE 编译器 开发工具
msvcp100.dll,msvcp120.dll,msvcp140.dll,Microsoft Visual C++ 2015 Redistributable,Visual C++ 运行库安装
MSVC是Windows下C/C++开发核心工具,集成编译器、链接器与调试器,配合Visual Studio使用。其运行时库(如msvcp140.dll)为程序提供基础函数支持,常因缺失导致软件无法运行。通过安装对应版本的Microsoft Visual C++ Redistributable可解决此类问题,广泛应用于桌面软件、游戏及系统级开发。
360 2
|
3月前
|
并行计算 C++ Windows
|
7月前
|
Linux 程序员 API
CentOS如何使用Pthread线程库
这就是在CentOS下使用Pthread线程库的全过程。可见,即使是复杂的并发编程,只要掌握了基本的知识与工具,就能够游刃有余。让我们积极拥抱并发编程的魅力,编写出高效且健壮的代码吧!
203 11
|
2月前
|
Java
如何在Java中进行多线程编程
Java多线程编程常用方式包括:继承Thread类、实现Runnable接口、Callable接口(可返回结果)及使用线程池。推荐线程池以提升性能,避免频繁创建线程。结合同步与通信机制,可有效管理并发任务。
166 6
|
5月前
|
Java API 微服务
为什么虚拟线程将改变Java并发编程?
为什么虚拟线程将改变Java并发编程?
309 83