c++进阶篇——初窥多线程(三)cpp中的线程类

简介: C++11引入了`std::thread`,提供对并发编程的支持,简化多线程创建并增强可移植性。`std::thread`的构造函数包括默认构造、移动构造及模板构造(支持函数、lambda和对象)。`thread::get_id()`获取线程ID,`join()`确保线程执行完成,`detach()`使线程独立,`joinable()`检查线程状态,`operator=`仅支持移动赋值。`thread::hardware_concurrency()`返回CPU核心数,可用于高效线程分配。

前言

在cpp11之前其实一直也没有对并发编程有什么语言级别上的支持,而在cpp11中加入了线程以及提供了对线程的封装类,一方面降低了并发编程的难度,另一方面也加强了了多线程程序的可移植性。

线程类(std::thread)

前言

在cpp11中为我们提供了std::thread作为线程类,基于这个类我们可以快速的创建一个新的线程,接下来我们来看一下它的一些常见api:

线程类的构造函数

在我们进行多线程程序的编写时,常见的构造函数主要有以下几种:

thread() noexpect;  //普通的构造函数
thread(thread&& other); //移动构造函数
template<class Function,Args... args>
explict thread(Fuction &&f,Args&& ... args);  //①
thread(const thread&) =delete; //禁止拷贝构造函数

上面就是主要会使用的多线程构造函数,这里我们主要讲解一下第三个构造函数,其他三个比较好懂,而第三个涉及的cpp11新特性比较多,这里我们着重讲一下:

  • 首先是template<class Function,Args... args>

    • class Function表示的是你想要在线程中执行的函数类型或者是可调用对象的类型,比如普通函数、lambda表达式、函数对象等.
    • Args... args是一个变长模板参数包,表示Function所接受的参数类型序列。...表示可以有任意数量的参数,每个Args代表一个参数类型
  • 然后是explict thread(Fuction &&f,Args&& ... args)

    • explictexplict关键字用于防止隐式类型转换,确保这个构造函数只能被显式调用。这意味着你不能在不需要明确指定类型的情况下意外地创建一个std::thread对象,例如通过自动类型推导或者一些隐式转换操作。
    • thread(Fuction &&f,Args&& ... args):这里函数形参使用右值引用使得左值引用与右值引用都可以绑定在上面,实现了参数的完美转发机制,详细的请参考:
      cpp随笔——浅谈右值引用,移动语义与完美转发

线程类的公有成员函数

  • get_id()
    应用程序启动之后默认只有一个线程,这个线程一般称之为主线程或父线程,通过线程类创建出的线程一般称之为子线程,每个被创建出的线程实例都对应一个线程ID,这个ID是唯一的,可以通过这个ID来区分和识别各个已经存在的线程实例,这个获取线程ID的函数叫做get_id(),示例代码如下:
    ```cpp

    include

    include

    include

    using namespace std;

void func(int num, string str)
{
for (int i = 0; i < 10; ++i)
{
cout << "子线程: i = " << i << "num: "
<< num << ", str: " << str << endl;
}
}

void func1()
{
for (int i = 0; i < 10; ++i)
{
cout << "子线程: i = " << i << endl;
}
}

int main()
{
cout << "主线程的线程ID: " << this_thread::get_id() << endl;
thread t(func, 520, "i love you");
thread t1(func1);
cout << "线程t 的线程ID: " << t.get_id() << endl;
cout << "线程t1的线程ID: " << t1.get_id() << endl;
}

这里我们用`thread()函数来创建`类一个线程,`func`是线程的工作函数,然后在`thread`函数中传递工作函数所需的参数。

上述代码存在一个小问题,主线程会在子线程结束前退出,而主线程退出会导致子线程提前退出导致无法得到我们所需的效果。
- **join()**
这个join的使用基本与c语言中的`pthread_join`函数功能相像,我们可以基于这个来修复一下上面代码的bug:
```cpp
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;

void func(int num, string str)
{
    for (int i = 0; i < 10; ++i)
    {
        cout << "子线程: i = " << i << "num: "
            << num << ", str: " << str << endl;
    }
}

void func1()
{
    for (int i = 0; i < 10; ++i)
    {
        cout << "子线程: i = " << i << endl;
    }
}

int main()
{
    cout << "主线程的线程ID: " << this_thread::get_id() << endl;
    thread t(func, 520, "i love you");
    thread t1(func1);
    cout << "线程t 的线程ID: " << t.get_id() << endl;
    cout << "线程t1的线程ID: " << t1.get_id() << endl;
    t.join();
    t1.join();
    return 0;
}

这样就可以得到我们想要的结果了。

  • detach()
    std::thread 类的 detach() 成员函数用于将线程与创建它的线程(即主线程)分离。调用 detach() 后,被分离的子线程将成为一个独立的执行单元,它有自己的生命周期,不再受主线程的控制。
    ```cpp

    include

    include

    include

    using namespace std;

void func(int num)
{
for (int i = 0; i < 10; ++i)
{
cout << "子线程: i = " << i << "num: "
<< num << endl;
}
}

void func1()
{
for (int i = 0; i < 10; ++i)
{
cout << "i = " << i << endl;
}
}

int main()
{
cout << "主线程的线程ID: " << this_thread::get_id() << endl;
thread t(func, 520);
thread t1(func1);
cout << "线程t 的线程ID: " << t.get_id() << endl;
cout << "线程t1的线程ID: " << t1.get_id() << endl;
t.detach();
t1.join();
return 0;
}

**注意**:

 1. 线程在被`detach`后不能再进行`join`操作
 2. 线程对象在销毁前必须选择是`detach`或`join`,否则就会出现异常。

- **joinable()**
`joinable()`函数用于判断主线程和子线程是否处理关联(连接)状态,一般情况下,二者之间的关系处于关联状态,该函数返回一个布尔类型:
`返回值为true`:主线程和子线程之间有关联(连接)关系
`返回值为false`:主线程和子线程之间没有关联(连接)关系

- **operator=**
线程的资源是不可复制的,所以我们无法通过重载赋值符获得两个一模一样的线程对象,我们来看一下它的声明:
```cpp
thread& opreator=(thread&& t)noexcept;
thread& opreator=(thread& t) =delete;

我们可以看到它只进行资源所有权的转移而不进行对对象资源的复制。

静态函数

thread线程类还提供了一个静态方法,用于获取当前计算机的CPU核心数,根据这个结果在程序中创建出数量相等的线程,每个线程独自占有一个CPU核心,这些线程就不用分时复用CPU时间片,此时程序的并发效率是最高的。

int thread::hardware_concurrency();

示例代码:

#include <iostream>
#include <thread>
#include <chrono>
using namespace std;

int main()
{
   
    int num = thread::hardware_concurrency();
    cout << num;
    return 0;
}
相关文章
|
7天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
33 4
|
8天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
27 4
|
22天前
|
存储 并行计算 安全
C++多线程应用
【10月更文挑战第29天】C++ 中的多线程应用广泛,常见场景包括并行计算、网络编程中的并发服务器和图形用户界面(GUI)应用。通过多线程可以显著提升计算速度和响应能力。示例代码展示了如何使用 `pthread` 库创建和管理线程。注意事项包括数据同步与互斥、线程间通信和线程安全的类设计,以确保程序的正确性和稳定性。
|
1月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
27 4
|
30天前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
21 1
|
1月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
43 1
C++ 多线程之初识多线程
|
23天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
17 3
|
23天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
16 2
|
23天前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
28 2
|
23天前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
28 1