C++ 并发编程实战 第二章 线程管控

第二章 线程管控

std::thread 简介

构造和析构函数

/// 默认构造
/// 创建一个线程,什么也不做
thread() noexcept;

/// 带参构造
/// 创建一个线程,以 A 为参数执行 F 函数
template <class Fn, class... Args>
explicit thread(Fn&& F, Args&&... A);

/// 拷贝构造(不可用)
thread(thread& Other) noexcept = delete;

/// 移动构造
/// 移交线程的归属权
thread(thread&& x) noexcept;

/// 析构函数
~thread();

常用成员函数

/// 等待线程结束并清理资源(会阻塞)
void join();

/// 返回线程是否可以执行 join() 成员函数
bool joinable();

/// 将线程与调用其的线程分离,彼此独立执行(此函数必须在线程创建时立即调用,
/// 且调用此函数会使其不能被join)
void detach();

/// 获取线程id
std::thread::id get_id();

/// 见移动构造函数
/// 如果对象是 joinable 的,那么会调用 std::terminate() 结果程序
thread& operator=(thread&& Other) noexcept;

使用线程类完成线程的基本管控

发起线程

  • 线程通过构建std::thread对象而启动,该对象指明线程要运行的任务。
void do_some_work();
std::thread my_thread(do_some_work);
  • 对应复杂的任务,可以使用函数对象。
class background_task{
public:
    void operator()() const{
        do_something();
        do_something_else();
    }
};
background_task f;
std::thread my_thread1(f);

// 使用匿名函数对象
// std::thread my_thread2(background_task());这样写会出错,
// 编译器会解释成函数声明,返回值为 std::thread, 函数名为 my_thread2,
// 函数参数为函数指针类型 background_task (*)(void), 因此改为如下两种方式:
std::thread my_thread2((background_task()));

// 采用新式的统一初始化语法(uniform initialization syntax,又名列表初始化)
std::thread my_thread2{background_task()};

等待线程完成

一旦启动了线程,我们就需明确是要等待它结束(与之汇合 join()),还是任由它独自运行(与之分离 detach()

std::thread my_thread1(f);
// ...
my_thread1.join();// 等待线程结束并清理资源

❗❗❗ 同一个线程的 .join() 方法不能被重复调用,否则程序会 abort()

thread 封装——thread_guard

基于 RAII 原理,对 std::thread 进行封装

// std::thread 封装(基于RAII)
class thread_guard{
    thread& m_thread;
public:
    explicit thread_guard(thread& t):m_thread(t){}
    ~thread_guard() {
        // 检查是否joinable是有必要的,重复 join() 会出错
        if (m_thread.joinable()){
            m_thread.join();
        }
    }
    // C++11 "=delete" 标记,声明拷贝构造和复制赋值操作为被删除的函数
    // 防止拷贝导致重复调用 join()
    thread_guard(thread_guard const&) = delete;
    thread_guard& operator=(thread_guard const&) = delete;
};

int main(){
    thread t([] {cout << "hello" << endl; });
    thread_guard g(t);
}

这样线程,将随着类的析构,自动被回收。

线程后台运行

  • 调用 std::thread 对象的成员函数 detach(),会令线程在后台运行,遂无法与之直接通信。
  • 线程被分离,就无法等待它完结,也不可能获得与它关联的 std::thread 对象,因而无法汇合该线程。
  • 分离的线程确实仍在后台运行,其归属权和控制权都转移给C++运行时库(runtime library,又名运行库),由此保证,一旦线程退出,与之关联的资源都会被正确回收。
  • UNIX操作系统中,有些进程叫作守护进程(daemon process),它们在后台运行且没有对外的用户界面;沿袭这一概念,分离出去的线程常常被称为守护线程(daemon thread)
std::thread t(do_background_work);
t.detach();
assert(!t.joinable());// C++ 断言,表达式为假就输出诊断消息并调用abort()函数中止程序

向线程函数传递参数

void f(int i,std::string const& s);
std::thread t(f,3,"hello");
  • thread 执行带有引用类型参数的函数
int b = 10;
// 必须使用 std::ref 指定参数按引用传递,否则参数默认以右值引用传递
// 右值引用无法转换为引用类型参数
thread t([](int& a) { cout << a << endl; }, ref(b));
t.join();
  • thread 执行类中的成员函数
class Person {
public:
    void show(string const& name, unsigned int const& age) {
        cout << "name: " << name << endl;
        cout << "age: " << age << endl;
    }
};

int main(){
    Person p;
    // 每个对象的成员函数都有一个默认的参数,就是该对象的指针(this 指针)
    // 因此,这里第一个参数要传入对象的地址
    thread t(&Person::show, &p, "Tom", 22);
    t.join();
}
  • thread 执行的函数中,参数使用了移动构造
void process_big_object(std::unique_ptr<big_object>);

std::unique_ptr<big_object> p(new big_object);
p->prepare_data(42);
std::thread t(process_big_object,std::move(p));// 必须使用 std::move() 将左值转换为右值

在C++标准库中,有几个类的归属权语义与 std::unique_ptr 一样,std::thread 类就是其中之一。

它们没有拷贝构造和复制赋值(=delete),只能移动不能复制,保证了任意时刻对于唯一的资源,只有唯一的对象与其对应。

对于 std::thread 只允许将线程归属权从一个对象移动给另一个对象。

移交线程的归属权

/// 移动构造
/// 移交线程的归属权
thread(thread&& x) noexcept;

❗❗❗

只能从一个已关联执行线程的 thread 对象将线程归属权移交给还未关联任何执行线程的 thread 对象。如果后者也关联了某个执行线程,那么操作会使程序 abort()

因此重要原则是:只要std::thread对象正管控着一个线程,就不能简单地向它赋新值,否则该线程会因此被遗弃。

std::thread 支持移动操作的意义是,函数可以便捷地向外部转移线程的归属权

  • 从函数内部返回 std::thread 对象
std::thread f(){
    void some_function();
    return std::thread(some_function);
}
std::thread g(){
    void some_other_function(int);
    std::thread t(some_other_function,42);
    return t;
}
  • 线程归属权转移到函数内部
void f(std::thread t);
void g(){
    void some_function();
    f(std::thread(some_function));
    
    std::thread t(some_function);
    
    f(std::move(t));
}

thread_guard 升级

  • 使得可以直接使用封装好的类创建线程对象,并由类掌管。而不必现先在类外创建具名变量。
  • 封装好的类对线程有唯一归属权,不必担心其他对象执行汇合或分离操作。
class soped_thread {
    thread m_thread;
public:
    explicit soped_thread(thread t) : m_thread(move(t)){
        if (!m_thread.joinable()) throw logic_error("No thread");
    }
    ~soped_thread() {
        m_thread.join();
    }
    soped_thread(soped_thread const&) = delete;
    soped_thread& operator=(soped_thread const&) = delete;
};

int main()
{
    soped_thread t{ thread([] {
        int i = 10;
        while (i--)
        {
            cout << i << endl;
            this_thread::sleep_for(chrono::seconds(2));
        }
    })};
}

使用示例

例1:thread 的基本使用

线程管控的自动化:若要为多个线程分别直接创建独立变量,还不如将它们集结成组,统一处理。

void do_work(unsigned id) {
    // 可以测试一下 printf 和 cout << id << endl;两种方式输出
    // 使用 cout << id << endl; 输出id和输出endl的连续动作可能被其他线程打断。
    printf("%d\n", id);
}

int main()
{
    vector<thread> threads;
    for (int i = 0; i < 10; i++){
        threads.emplace_back(do_work, i);
    }
    for (auto& entry : threads) entry.join();
}

例2:线程分离的应用场景

场景描述:

一个文字处理的应用程序

同时编辑多个文件,多个独立的顶层窗口,分别与正在编辑的文件逐一对应,窗口各有自己的选项单。

解决方案:使用多线程并行处理。

  • 在同一应用程序的实例中运行。
  • 相应的内部处理是,每个文件的编辑窗口都在各自线程内运行;每个线程运行的代码相同,而处理的数据有别,因为这些数据与各文件和对应窗口的属性关联。
  • 打开一个新文件就需启动一个新线程。新线程只处理打开新文件的请求,并不牵涉等待其他线程的运行和结束。对其他线程而言,该文件由新线程负责,与它们无关。

综上,运行分离线程就成了首选方案。

void edit_document(std::string const& filename)
{
    open_document_and_display_gui(filename);
    while(!done_editing())
    {
        user_command cmd = get_user_input();// 获取用户输入命令
        // 如果用户输入命令是打开新文件,就开启一个新线程,在新线程下打开文件
        if(cmd.type == open_new_document)
        {
            std::string const new_name = get_filename_from_user();
            // 开启新线程打开该文件
            std::thread t(edit_document,new_name);
            t.detach();// 分离新线程
        }
        else
        {
            // 执行用户命令
            process_user_input(cmd);
        }
    }
}

例3:thread 类的封装 joining_thread 类

曾经有一份C++17标准的备选提案,主张引入新的类joining_thread,它与std::thread类似,但只要其执行析构函数,线程即能自动汇合,这点与scoped_thread非常像。可惜C++标准委员会未能达成共识,结果C++17标准没有引入这个类,后来它改名为std::jthread,依然进入了C++20标准的议程(现已被正式纳入C++20标准)。除去这些,实际上joining_thread类的代码相对容易编写,代码清单2.7展示了一个可行的实现。

class joining_thread
{
	std::thread m_thread;
public:
	joining_thread() noexcept = default;
	template<typename Callable, typename ... Args>
	explicit joining_thread(Callable&& func, Args&& ... args)
		: m_thread(std::forward<Callable>(func), std::forward<Args>(args)...)
	{}
	explicit joining_thread(std::thread t) noexcept
		: m_thread(std::move(t))
	{}
	joining_thread& operator=(joining_thread&& other) noexcept
	{
		if (joinable()) join();
		m_thread = std::move(other.m_thread);
		return *this;
	}
	joining_thread& operator=(std::thread other) noexcept
	{
		if (joinable()) join();
		m_thread = std::move(other);
		return *this;
	}
	~joining_thread() noexcept
	{
		if (joinable()) join();
	}
	void swap(joining_thread& other) noexcept
	{
		m_thread.swap(other.m_thread);
	}
	std::thread::id get_id() const noexcept 
	{
		return m_thread.get_id();
	}
	bool joinable() const noexcept
	{
		return m_thread.joinable();
	}
	void join(){ m_thread.join(); }
	void detach() { m_thread.detach(); }

	std::thread& as_thread() noexcept { return m_thread; }

	const std::thread& as_thread() const noexcept
	{
		return m_thread;
	}
};

原文链接:https://www.cnblogs.com/Critical-Thinking/p/17298575.html

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:C++ 并发编程实战 第二章 线程管控 - Python技术站

(0)
上一篇 2023年4月17日
下一篇 2023年4月18日

相关文章

  • C++/Qt网络通讯模块设计与实现(总结)

    至此,C++/Qt网络通讯模块设计与实现已分析完毕,代码已应用于实际产品中。 C++/Qt网络通讯模块设计与实现(一) 该章节从模块的功能需求以及非功能需求进行分析,即网络通讯模块负责网络数据包的发送、接收以及对外提供功能调用以及接口回调,其不进行产品业务的实现,达到平台化复用的目的,给出了类图,如下所示::   符合先设计再开发的思想,各类的功能也有详细描…

    C++ 2023年4月18日
    00
  • 2023.5.5 面向对象程序设计实验报告

    实验项目名称:模板 一、实验目的 1、熟练掌握函数模板和类模板的定义格式。 2、熟练运用函数模板和类模板解决实际问题。 二、实验内容 1、复数类Complex有两个数据成员:a和b, 分别代表复数的实部和虚部,并有若干构造函数和一个重载-(减号,用于计算两个复数的距离)的成员函数。 要求设计一个函数模板 template < class T > …

    C++ 2023年5月5日
    00
  • L1-080 乘法口诀数列*(使用C++)

    L1-080 乘法口诀数列 分数 20 全屏浏览题目 切换布局 作者 陈越单位 浙江大学   本题要求你从任意给定的两个 1 位数字 a1​ 和 a2​ 开始,用乘法口诀生成一个数列 {an​},规则为从 a1​ 开始顺次进行,每次将当前数字与后面一个数字相乘,将结果贴在数列末尾。如果结果不是 1 位数,则其每一位都应成为数列的一项。 输入格式: 输入在一行…

    C++ 2023年4月18日
    00
  • 网络流的C++代码实现与过程讲解

    网络流是一种非常重要的图论算法,它在许多实际问题中得到广泛应用。本文将介绍网络流算法的C++代码实现与过程讲解。 算法概述 网络流算法是通过将图中的边看作流量通道,将图的点看作流量的起点或终点,来求解图中的最大或最小流量的问题。它是一种非常重要的最优化算法,广泛应用于图论、运筹学、计算机网络等领域。 网络流算法有很多种,其中最著名的是Ford-Fulkers…

    C++ 2023年4月22日
    00
  • MordernC++之左值(引用)与右值(引用)

    左值与右值 C++中左值与右值的概念是从C中继承而来,一种简单的定义是左值能够出现再表达式的左边或者右边,而右值只能出现在表达式的右边。 int a = 5; // a是左值,5是右值 int b = a; // b是左值,a也是左值 int c = a + b; // c是左值,a + b是右值 另一种区分左值和右值的方法是:有名字、能取地址的值是左值,没…

    C++ 2023年4月17日
    00
  • 【Visual Leak Detector】在 VS 高版本中使用 VLD

    说明 使用 VLD 内存泄漏检测工具辅助开发时整理的学习笔记。 本篇介绍如何在 VS 高版本中使用 vld2.5.1。同系列文章目录可见 《内存泄漏检测工具》目录 目录 说明 1. 使用前的准备 2. 在 VS 2015 及更早版本中使用 VLD 3. 在 VS 高版本中使用 VLD 3.1 参考资料:在 VS 2017 中使用 VLD 3.2 参考资料:在…

    C++ 2023年5月6日
    00
  • 【Visual Leak Detector】VS 中 VLD 输出解析

    说明 使用 VLD 内存泄漏检测工具辅助开发时整理的学习笔记。同系列文章目录可见 《内存泄漏检测工具》目录 目录 说明 1. 使用方式 2. 输出报告 1. 使用方式 在 VS 中使用 VLD 的方法可以查看另外一篇博客:在 VS 2015 中使用 VLD。 2. 输出报告 在 VS 中使用 VLD 时的输出报告,与在 QT 中使用时是一致的,输出内容的解析…

    C++ 2023年4月17日
    00
  • STL容器之queue

    是什么 循环队列, FIFO先进先出 怎么用 初始化 //C11 deque<int> deq{1,2,3,4,5}; //拷贝构造,可以拷贝deque queue<int> que(deq); //100个5 queue<int> que2(100,5); //运算符重载 que2 = que; 操作 //队尾添加元素 …

    C++ 2023年4月17日
    00
合作推广
合作推广
分享本页
返回顶部