C++多线程 第二章 管理线程

第二章 管理线程


启动线程

线程通过构造 std::thread 对象开始的,该对象指定了线程上要运行的任务.

  • 通过构造std::thread启动一个线程:
void do_some_work();
std::thread my_thread(do_some_work);

与许多C++标准库像是,std::thread可以与任何 可调用(callable) 类型一同工作.

  • 结合callable类型启动线程:
class background_task
{
public:
    void operator()()const
    {
        do_something();
        do_something_else();
    }
};
backgroud_task f;
std::thread my_thread(f);

这种情况下,所提供的函数对象被 复制(copied) 到属于新创建的执行线程存储器中,并在那被调用.

但是需要注意的: 不要传递一个临时的且未被命名的变量到线程实参中! 参见下面的例子:

std::thread my_thread(background_task());
//你以为的:调用构造函数,然后将匿名临时对象传入
//实际上的:std::thread在其中调用名为background_task的函数

如果一定想将临时对象传入,解决方法有两个:

  • 使用一组额外的括号:
std::thread my_thread((background_task()));
//这样使得background_task()首先被执行
  • 使用新的统一初始化语法:
std::thread my_thread{background_task()};
//这样就使得直接将其视为值

而如果你想一劳永逸地解决可调用对象类型临时对象带来的风险问题,可以使用C++11加入的 lambda表达式(lambda expression).

std::thread my_thread([]{
    do_something();
    do_something_else();
});

管理线程启动模式

  • 设置线程为结合模式:
my_thread.join();
  • 设置线程为分离模式:
my_thread.detach();

下面给出一个通过join结合的例子:

#include <iostream>
#include <thread>

void do_something() 
{
    std::cout << "do something" << std::endl;
}
void do_something_else() 
{
    std::cout << "do something else" << std::endl;
}

int main()
{
    std::thread my_thread([] {
        do_something();
        do_something_else();
    });
    my_thread.join();

    return 0;
}

在线程执行完之前,程序并不会直接退出.

而下面又给出了一个detach分离线程的例子.

#include <iostream>
#include <thread>
#include <windows.h>

void do_something() 
{
    std::cout << "do something" << std::endl;
}
void do_something_else() 
{
    MessageBox(NULL, L"现在你收到了来自分离线程的文本框", L"detach线程", MB_OK);
}

int main()
{
    std::thread my_thread(do_something_else);
    my_thread.detach();

    for (int i = 0; i < 1000; i++) {
        std::thread t(do_something);
        t.join();
    }

    return 0;
}

detach分离出去的的那个线程在某一刻突然得到了执行,不是吗?

如果你需要等待线程完成,你应当使用 std:🧵:join();反之,使用 std:🧵:detach().

需要注意的是,你需要在std::thread对象被销毁前就已调用join()或detach()函数.

join()很简单:要么等待一个线程执行完,要么就不等.和前面学过的OpenMP中的atomic有一定相似.

不过,join()的调用也会清理所有与该线程相关联的存储器,这样std::thread就不再与现已完成的线程相关联,它也不与任何线程相关联.

这就意味着: 你只能对一个给定的线程调用一次join(). 一旦你调用了join(),该std::thread便不再是可连接的,std:🧵:joinable()将返回false.

下面是一个简单的实验:

#include <iostream>
#include <thread>
#include <format>

void do_something() 
{
    std::cout << "do something" << std::endl;
}

int main()
{
    std::thread my_thread(do_something);
    my_thread.join();

    std::cout << std::format("the joinable is: {}",my_thread.joinable()) << std::endl;

    return 0;
}

得到的结果:

do something
the joinable is: false

然而,注意到下面这点:当在调用join()之前抛出了异常,对join()的调用就容易被跳过.

#include <iostream>
#include <thread>
#include <format>

void do_something()
{
    std::cout << "do something" << std::endl;
}

void func()
{
    throw "something wrong";
}

int main()
{
    std::thread my_thread(do_something);
    try {
        func();
        my_thread.join();
    }
    catch (...){ }
    
    std::cout << std::format("the joinable is: {}", my_thread.joinable()) << std::endl;
    return 0;
}

因而为了避免程序在引发异常时被终止,应当使用标准的 RAII惯用语法.

下面是使用RAII惯用语法的一个例子:

#include <iostream>
#include <thread>
#include <format>

class thread_guard
{
private:
    std::thread& m_t;
public:
    explicit thread_guard(std::thread& t) :m_t(t) { return; }
    ~thread_guard()
    {
        if (this->m_t.joinable())
            this->m_t.join();
        return;
    }
    thread_guard(thread_guard const&) = delete;
    thread_guard& operator=(thread_guard const&) = delete;
};

void do_something()
{
    std::cout << "线程已被join." << std::endl;
}

void func()
{
    auto f = [] {throw "error"; };
    std::thread t(do_something);
    thread_guard tg(t);
    f();

    return;
}

int main()
{
    try {
        func();
    }
    catch (...){ }

    return 0;
}

使用RAII语法可以很好的保证join()绝对被执行,哪怕中途程序发生异常而退出.

下面是使用teriminate()的更为极端的例子:

#include <iostream>
#include <thread>
#include <format>

class thread_guard
{
private:
    std::thread& m_t;
public:
    explicit thread_guard(std::thread& t) :m_t(t) { return; }
    ~thread_guard()
    {
        if (this->m_t.joinable())
            this->m_t.join();
        return;
    }
    thread_guard(thread_guard const&) = delete;
    thread_guard& operator=(thread_guard const&) = delete;
};

void do_something()
{
    std::cout << "线程已被join." << std::endl;
}

void func()
{
    auto f = [] {std::terminate();};
    std::thread t(do_something);
    thread_guard tg(t);
    f();

    return;
}

int main()
{
    try {
        func();
    }
    catch (...){ }

    return 0;
}

其中,默认拷贝构造函数与拷贝赋值运算运算符被标记为删除,是为了确保它们不会被编译器自动提供.赋值或拷贝一个这样的对象可能是危险的.

如果你并不需要等待线程执行完毕,你应当使用detach()来分离线程.

但是同样的,你应当使用RAII语法来保证在任何情况下线程被detach().

下面是使用detach的一个情景:

#include <iostream>
#include <thread>
#include <format>
#include <windows.h>

class thread_guard
{
private:
    std::thread& m_t;
public:
    explicit thread_guard(std::thread& t) :m_t(t) { return; }
    ~thread_guard()
    {
        if (this->m_t.joinable())
            this->m_t.detach();
        return;
    }
    thread_guard(thread_guard const&) = delete;
    thread_guard& operator=(thread_guard const&) = delete;
};

void do_something()
{
    MessageBox(NULL, L"一个线程已被detach", L"RAII", MB_OK);
}

void func()
{
    for (int i = 0; i < 10; i++) {
        std::thread t(do_something);
        thread_guard tg(t);
    }

    return;
}

int main()
{
    try {
        func();
    }
    catch (...){ }
    Sleep(2000);

    return 0;
}

在这个程序中,我们同时创建了十个不同的线程同时执行MessageBox().

这里使用detach()是因为它们做的都是相同的工作,但是却需要并行处理不同的对象.

参考UNIX的 守护线程(daemon process) 概念,这些被分离的线程通常被称为 守护线程(daemon process).它们运行在后台.

传递参数给线程函数

通常而言,传递参数给可调用对象或函数的方式基本上就是简单地将额外的参数传递给std::thread构造函数.但是更重要的,参数会以默认的方式被 复制(copied) 到内部存储空间.

为了探究向线程传递参数的方式,我们在这里探究使用std::thread实现SPMD的方式.

#include <iostream>
#include <thread>
#include <format>
#include <windows.h>

const int NTHREAD = 11;
const int ARR_SIZE = 100;

class thread_guard
{
public:
    thread_guard(std::thread& t) :m_t(t) { return; }
    ~thread_guard()
    {
        if (this->m_t.joinable())
            m_t.detach();
        return;
    }
    thread_guard(thread_guard& const) = delete;
    thread_guard& operator=(thread_guard& const) = delete;
private:
    std::thread& m_t;
};

void func(double* arr, int index)
{
    for (int i = index; i < ARR_SIZE; i+=NTHREAD)
        arr[i] = i * i;
    return;
}

int main()
{
    double arr[ARR_SIZE];
    for (int i = 0; i < NTHREAD; i++) {
        std::thread t(func, arr, i);
        thread_guard tg(t);
    }
    Sleep(10);
    for (int i = 0; i < ARR_SIZE; i++)
        std::cout << std::format("the {} block is {}",
            i,
            arr[i]
        ) << std::endl;

    return 0;
}

所以我们可以总结得到:

  • 给线程传递参数:
std::thread name(func,[,[clause]...]);

然而,我们需要注意的是,在对线程进行传参时,存在一些问题:

  • 在线程传参时可能无法隐式数据转换,导致未定义的行为.
  • 在涉及引用时,线程无法根据数据类型分辨,需要显式声明std::ref

因而针对上述问题,我们提供以下解决问题:

  • 在线程传参时通过形参与实参类型的严格一致避免数据类型转换的需求
  • 在涉及内存修改时,使用std::ref或指针

还有一个特殊的内容是成员函数的线程执行:

通常形式为:

  • 给线程传递成员方法:
std::thread name(&class::func,&object[,[clause]...]);

如此可以通过函数指针与对象指针对其进行访问.

我们在这里给出相关的一个例子:

#include <iostream>
#include <thread>
#include <format>
#include <windows.h>

const int NTHREAD = 20;
const int ARR_SIZE = 1000;

class thread_guard
{
public:
    thread_guard(std::thread& t) :m_t(t) { return; }
    ~thread_guard()
    {
        if (this->m_t.joinable())
            m_t.detach();
        return;
    }
    thread_guard(thread_guard& const) = delete;
    thread_guard& operator=(thread_guard& const) = delete;
private:
    std::thread& m_t;
};

struct memory_block
{
public:
    void func(int index)
    {
        for (int i = index; i < ARR_SIZE; i += NTHREAD)
            arr[i] = i * i;
        return;
    }
    double& get(int index)
    {
        return this->arr[index];
    }
private:
    double arr[ARR_SIZE];
};


int main()
{
    memory_block block;

    for (int i = 0; i < NTHREAD; i++) {
        std::thread t(&memory_block::func, &block, i);
        thread_guard tg(t);
    }
    Sleep(10);
    for (int i = 0; i < ARR_SIZE; i++)
        std::cout << std::format("the {} block is {}",
            i,
            block.get(i)
        ) << std::endl;

    return 0;
}

这是C++SPMD的OOP实现.

转移线程的所有权

线程的所有权实际上也是一个有趣的问题.

为了将变量的所有权传入到线程中,一般使用std::move对其进行操作.这很常规.

然而,有趣的是,和std::unique_ptr一样,std::thread实际上是 可移动的(moveable),尽管他们不是可复制的(copyable).也就是说,可以通过std::move在线程之间进行所有权的移动.

在这里,给出一个例子:

#include <iostream>
#include <thread>

void func_1()
{
    std::cout << "now is func_1" << std::endl;
}

void func_2()
{
    std::cout << "now is func_2" << std::endl;
}

int main()
{
    std::thread t_1(func_1);
    std::thread temp = std::move(t_1);
    temp.join();

    std::thread t_2 = std::thread(func_2);
    t_1 = std::move(t_2);
    t_1.join();

    return 0;
}

通过这个例子,我们可以发现,std::thread可以通过std::move转移所有权.而且,临时std::thread变量给线程赋值默认视作为所有权转移.

注意,当试图转移一个线程的所有权到一个已经有任务的线程对象中去时,会发生std::terminate()来终止程序.

由于线程关于所有权转移的这种设计,函数可以方便地转出线程的所有权:

  • 从函数中转出线程所有权:
std::thread func()
{
    std::thread t;
    //do something
    return std::move(t);
}

同时,考虑到thread_guard类并不拥有线程的所有权,为了设计一个既符合RAII语法,又能拥有线程所有权的类,我们引出下文中的scoped_thread类来解决相关问题.

同时,我们将其与thread_guard进行对比:

  • thread_guard:
class thread_guard
{
public:
    thread_guard(std::thread& t) :m_t(t) { return; }
    ~thread_guard()
    {
        if (this->m_t.joinable())
            m_t.join();
        return;
    }
    thread_guard(thread_guard& const) = delete;
    thread_guard& operator=(thread_guard& const) = delete;
private:
    std::thread& m_t;
};
  • scoped_thread:
class scoped_thread
{
public:
    scoped_thread(std::thread t) :m_t(std::move(t)) 
    {
        if(!this->m_t.joinable())
            throw std::logic_error("No thread");
        return; 
    }
    ~scoped_thread()
    {
        if (this->m_t.joinable())
            m_t.join();
        return;
    }
    scoped_thread(scoped_thread& const) = delete;
    scoped_thread& operator=(scoped_thread& const) = delete;
private:
    std::thread m_t;
};

而下面的一个例子则很好地解释了scoped_thread的使用方法:

#include <iostream>
#include <thread>

class scoped_thread
{
public:
    scoped_thread(std::thread t) :m_t(std::move(t))
    {
        if (!this->m_t.joinable())
            throw std::logic_error("No thread");
        return;
    }
    ~scoped_thread()
    {
        if (this->m_t.joinable())
            m_t.join();
        return;
    }
    scoped_thread(scoped_thread& const) = delete;
    scoped_thread& operator=(scoped_thread& const) = delete;
private:
    std::thread m_t;
};

void func_1()
{
    std::cout << "now is func_1" << std::endl;  
}

void func_2()
{
    std::cout << "now is func_2" << std::endl;
}

int main()
{
    scoped_thread st_1{std::thread(func_1)};

    std::thread t(func_2);
    scoped_thread st_2(std::move(t));

    return 0;
}

std::thread对移动的支持同样考虑了std::thread对象的容器,如果那些对象是移动感知的,这意味着可以生成一批线程,然后等待它们完成.

#include <iostream>
#include <thread>
#include <vector>
#include <algorithm>
#include <functional>
#include <format>

const int NTHREAD = 20;

void outPutNum(int id)
{
    std::cout << std::format("the {} thread is called",id) << std::endl;
}

int main()
{
    std::vector<std::thread>v;
    for (int i = 0; i < NTHREAD; i++)
        v.push_back(std::thread(outPutNum, i));

    std::for_each(v.begin(), 
        v.end(),
        std::mem_fn(&std::thread::join)
    );
    std::cout << "all threads has been called" << std::endl;

    return 0;
}

通过如此的结构,我们已经基本了解了并发解决问题的一些方式.

下面我们结合上面生成一组线程的方式,通过C++实现SPMD.

#include <iostream>
#include <thread>
#include <vector>
#include <functional>
#include <format>

const int NTHREAD = 20;
const int ARR_SIZE = 1000;

struct memory_block
{
public:
    void func(int index)
    {
        for (int i = index; i < ARR_SIZE; i += NTHREAD)
            arr[i] = i * i;
        return;
    }
    double& get(int index)
    {
        return this->arr[index];
    }
private:
    double arr[ARR_SIZE];
};


int main()
{
    memory_block block;
    std::vector<std::thread>threads;

    for (int i = 0; i < NTHREAD; i++) {
        std::thread t(&memory_block::func, &block, i);
        threads.push_back(std::move(t));
    }
    for (auto& iter : threads)
        iter.join();
    for (int i = 0; i < ARR_SIZE; i++)
        std::cout << std::format("the {} block is {}",
            i,
            block.get(i)
        ) << std::endl;

    return 0;
}

在前面的程序中,为了等待线程都执行完成,我们手动添加了Sleep(10)来等待其全部执行完毕.而通过范围for语句(或std::for_each函数),我们解决了等待线程全部完成的问题.

运行时线程数量

为了对进行时线程进行调控,通常而言需要使用到 std:🧵:hardware_currency() 来获得硬件线程的数量.

下面对前文SPMD的内容做进一步的修改,令其线程数在运行时调控.

#include <iostream>
#include <thread>
#include <vector>
#include <functional>
#include <format>

const int ARR_SIZE = 1111;

struct memory_block
{
public:
    memory_block(int nthreads) :NTHREAD(nthreads) 
        { return; }
    void func(int index)
    {
        for (int i = index; i < ARR_SIZE; i += NTHREAD)
            arr[i] = i * i;
        return;
    }
    double& get(int index)
    {
        return this->arr[index];
    }
private:
    double arr[ARR_SIZE];
    int NTHREAD;
};

int main()
{
    const int min_per_thread = 100;
    const int max_threads = (ARR_SIZE + min_per_thread - 1) / min_per_thread;
    const int hardware_threads = std::thread::hardware_concurrency();
    const int NTHREAD = std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);

    memory_block block(NTHREAD);
    std::vector<std::thread>threads;

    for (int i = 0; i < NTHREAD; i++) {
        std::thread t(&memory_block::func, &block, i);
        threads.push_back(std::move(t));
    }
    for (auto& iter : threads)
        iter.join();
    for (int i = 0; i < ARR_SIZE; i++)
        std::cout << std::format("the {} block is {}",
            i,
            block.get(i)
        ) << std::endl;

    return 0;
}

这个SPMD相较前面增加了对线程数量的计算.这在主函数的前四行不难看出.

其中,min_per_thread是最小分块大小,max_threads是最大线程数,hardware_threads是硬件线程数,NTHREAD则是最后得到的最佳线程数.

NTHREAD是计算出的最大值和硬件线程数量的最小值.运行比硬件线程数更多的线程会造成 超额订阅(oversubscription), 因为上下位切换意味着更多的线程会降低性能.

线程的标识

  • 获得std::thread对象的std:🧵:id标识符:
std::thread t;
t.get_id();

如果std::thread对选哪个没有相关联的执行线程,那么对get_id()函数的调用将返回一个默认构造的std:🧵:id对象,表示"没有线程".

通常而言,std:🧵:id是线程的通用标识符.

下面是一个标识线程的例子:

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

std::mutex some_lock;

void func()
{
	std::lock_guard<std::mutex> guard(some_lock);
	std::thread::id temp_id = std::this_thread::get_id();
	std::cout << temp_id << std::endl;
	return;
}


int main()
{
	std::vector<std::thread>threads;
	const int NTHREAD = 10;

	for (int i = 0; i < NTHREAD; i++) {
		std::thread temp_t(func);
		threads.push_back(std::move(temp_t));
	}
	for (auto& iter : threads)
		iter.join();

	return 0;
}
posted @ 2024-01-29 05:50  Mesonoxian  阅读(7)  评论(2编辑  收藏  举报