c++11中的多线程std::thread

程序员小x大约 9 分钟C++

c++11中的多线程std::thread

简介

在c++11中提供了新的多线程的创建方式std::thread, 丰富了对于多线程的使用。本文将从下面几个角度对std::thread进行讲解。

  • std::thread的原型
  • std::thread创建线程的方式
  • std::thread的销毁
  • std::thread的传参
  • 如何获取线程的返回值

std::thread的原型

std::thread类的原型如下所示:

namespace std {
class thread {
public:
    // 类型声明:
    class id;
    typedef implementation-defined native_handle_type;

    // 构造函数、拷贝构造函数和析构函数声明:
    thread() noexcept;
    template <class F, class ...Args> explicit thread(F&& f, Args&&... args);
    ~thread();
    thread(const thread&) = delete;
    thread(thread&&) noexcept;
    thread& operator=(const thread&) = delete;
    thread& operator=(thread&&) noexcept;

    // 成员函数声明:
    void swap(thread&) noexcept;
    bool joinable() const noexcept;
    void join();
    void detach();
    id get_id() const noexcept;
    native_handle_type native_handle();

    // 静态成员函数声明:
    static unsigned hardware_concurrency() noexcept;
};
}

其包含了一个默认的构造函数和一个模板构造函数。该模板构造函数是explicit,代表不允许隐式转换。 其拷贝构造函数声明为delete,代表不可以使用一个线程初始化另一个线程。 其拥有一个移动构造函数。

对于模板构造函数,可以看到,其入参就和函数的入参很类似,因此创建一个线程的方式大大简化。在此之前,在linux平台,创建线程需要使用pthread_create函数,其要求将参数封装为void*指针, 这个在写代码的时候是不太方便的。

template <class Fn, class... Args> 
explicit thread (Fn&& fn, Args&&... args);

c++11中thread和pthread相比有如下的一些优点:

  • 跨平台,pthread只能用在POSIX系统上
  • 简单,易用,传参方便,过去pthread需要将数据传递给void*指针, c++11直接像函数传参一样传递参数
  • 提供了std::future,std::promise等高级功能
  • 风格上更加像c++, pthread更像c的。

和pthread相比, 也有一些缺点:

  • 没有实现读写锁
  • pthread提供的功能更加多,例如设置CPU的亲和性

下面看看使用std::thread创建线程的几种方式。

std::thread创建线程的方式

使用普通函数创建线程

#include<iostream>
#include <thread>

void myprint()
{
    std::cout<<"thread start to run" << std::endl;
}

int main()
{
    std::thread th(myprint);
    th.join();
    std::cout << "main thread end" << std::endl;
}

使用仿函数创建线程

#include<iostream>
#include <thread>

class Test
{
public:
    void operator()() const
    {
        std::cout << "function object starts to run" << std::endl;
    }
};

int main()
{
    Test obj;
    std::thread th(obj);
    th.join();

    std::this_thread::sleep_for(std::chrono::milliseconds(300));
    std::cout << "main thread end" << std::endl;
}

使用lambda表达式创建线程

#include<iostream>
#include <thread>

int main()
{
    auto func = [](){
        std::cout << "lambda function thread start to run" << std::endl;
    };

    std::thread th(func);
    th.join();

    std::this_thread::sleep_for(std::chrono::milliseconds(300));
    std::cout << "main thread end" << std::endl;
}

使用成员函数创建线程

使用成员函数创建线程有下面几种实现的思路

  • 直接传参给std::thread
#include <iostream>
#include <algorithm>
#include <vector>
#include <thread>

class Test{
public:
    Test() = default;
    ~Test() = default;
public:
    void func(){
        std::cout << "test func" << std::endl;
    }
};

int main()
{
    Test testobj;
    std::thread th1{&Test::func, &testobj};
    th1.join();
}
  • 使用std::bind生成function对象再传给std::thread
#include <iostream>
#include <algorithm>
#include <vector>
#include <thread>
#include <functional>

class Test{
public:
    Test() = default;
    ~Test() = default;
public:
    void func(){
        std::cout << "test func" << std::endl;
    }
};

int main()
{
    Test testobj;
    std::function<void()> testfunc = std::bind(&Test::func, &testobj);
    std::thread th1{testfunc};
    th1.join();
}
  • 封装成lambda函数再传给std::thread
#include <iostream>
#include <algorithm>
#include <vector>
#include <thread>
#include <functional>

class Test{
public:
    Test() = default;
    ~Test() = default;
public:
    void func(){
        std::cout << "test func" << std::endl;
    }
};

int main()
{
    Test testobj;
    auto testfunc = [&testobj](){
        testobj.func();
    };

    std::thread th1{testfunc};
    th1.join();
}

std::thread的销毁

std::thread的析构函数中有下面的判断,即当一个线程在析构时处于可以join的状态,那么将会调用std::terminate方法,这将会异常终止程序的运行。

    ~thread()
    {
      if (joinable())
	    std::terminate();
    }

因此如果一个线程是可以join的,那么在析构前一定要join掉。**effective modern c++**一书中的Item37也曾提到,使std::thread在所有路径最后都不可结合(unjoinable)。

这里特别的提一下detach。detach函数将当前线程从主线程中分离出来,使其变成一个独立的线程,并在其执行完成后自动销毁,而不会阻塞主线程。

一个有趣的问题的是,如果main函数退出了,detach的线程会怎样?open in new window

例如下面的例子:

#include <iostream>
#include <string>
#include <thread>
#include <chrono>

void thread_fn() {
  std::this_thread::sleep_for (std::chrono::seconds(1)); 
  std::cout << "Inside thread function\n";   
}

int main()
{
    std::thread t1(thread_fn);
    t1.detach();

    return 0; 
}

在linux下,使用gcc编译,上面的程序没有任何输出。当主线程结束时,已经detach的线程会被强行杀掉。注意,这里使用了强行两个字,意味的杀掉的方式将是暴力的。

看下面的例子:

#include <iostream>
#include <string>
#include <thread>
#include <chrono>

class A
{
public:
	A() 
	{
		std::cout << "A()" << std::endl;
	}
	~A()
	{
		std::cout << "~A()" << std::endl;
	}
};

void thread_fn() {
  A a;
  std::this_thread::sleep_for (std::chrono::seconds(1)); 
  std::cout << "Inside thread function\n";   
}

int main()
{
    std::thread t1(thread_fn);
    t1.detach();

    return 0; 
}

执行结果:

A()

上面的程序在线程中创建了a对象,从执行结果可以看到a对象的构造函数已经打印。但是析构函数没有打印。

这里我推测的原因是子线程被强行杀掉,因此没有必要调用~A(),直接由操作系统回收所有资源。

从这个例子也看出,主线程退出,detach的子线程被强行杀掉将会造成一些奇怪的现象和问题,因此平常开发时要避免这一点,主线程退出前要让子线程安全的退出。

std::thread的传参

在上面的例子中,我们创建的函数都是没有入参的。下面我们讨论下如何传参并且需要注意的一些点。

#include <iostream>
#include <thread>

class String
{
public:
    String(const char* cstr) { std::cout << "String()" << std::endl; }

    // 1
    String(const String& v)
    { std::cout << "String(const String& v)" << std::endl; }
  
    // 2
    String(const String&& v) noexcept
    { std::cout << "String(const String&& v)" << std::endl; }

    // 3
    String& operator=(const String& v)
    { std::cout << "String& operator=(const String& v)" << std::endl; return *this; }
};

void test(int i, const String & s) {}

int main()
{
    String s("hello");
    std::cout << "----------------" << std::endl;

    // 输出 1, 2
    std::thread t1(test, 3, s);//拷贝构造
    t1.join();
    std::cout << "----------------" << std::endl;

    // 输出 2, 2
    std::thread t2(test, 3, std::move(s));//移动构造
    t2.join();
    std::cout << "----------------" << std::endl;

    // 只输出 1
    std::thread t3(test, 3, "hello");//拷贝指针,构造函数
    t3.join();
    std::cout << "----------------" << std::endl;

    // 无输出
    std::thread t4(test, 3, std::ref(s));//无拷贝
    std::cout << "----------------" << std::endl;
    t4.join();
}

执行结果如下所示:

String()
----------------
String(const String& v)
----------------
String(const String&& v)
----------------
String()
----------------
----------------

第一个例子,(1) s被copy到了新的memory space里去,所以call的是copy constructor 输出了1。(2) 第一步的结果生成了一个rvalue,所以传参数去函数的时候用的是move constructor,所以输出了2。

第二个例子,(1) s被move到了新thread的memory space里,所以用的是move constructor,输出2。(2) 同上,输出2。

第三个例子,输出的是这个String(const char* cstr) { std::cout << "String()" << std::endl; }。(1) 你在这里copy过去的其实是一个const char*指针,所以第一步没任何输出。(2) 这时你用const char*来构造一个String,所以输出0.

第四个例子,你的一切活动都是指向最初的那个s,所以没有任何constructor被调用,所以不输出任何东西。

还是上面的例子,这里将test函数中的const去除void test(int i, String & s) {}, 会如何?

#include <iostream>
#include <thread>

class String
{
public:
    String(const char* cstr) { std::cout << "String()" << std::endl; }

    // 1
    String(const String& v)
    { std::cout << "String(const String& v)" << std::endl; }
  
    // 2
    String(const String&& v) noexcept
    { std::cout << "String(const String&& v)" << std::endl; }

    // 3
    String& operator=(const String& v)
    { std::cout << "String& operator=(const String& v)" << std::endl; return *this; }

};

void test(int i, String & s) {}

int main()
{
    String s("hello");
    std::cout << "----------------" << std::endl;

    // 输出 1, 2
    std::thread t1(test, 3, s);//拷贝构造
    t1.join();
    std::cout << "----------------" << std::endl;

    // 输出 2, 2
    std::thread t2(test, 3, std::move(s));//移动构造
    t2.join();
    std::cout << "----------------" << std::endl;

    // 只输出 1
    std::thread t3(test, 3, "hello");//拷贝指针,构造函数
    t3.join();
    std::cout << "----------------" << std::endl;

    // 无输出
    std::thread t4(test, 3, std::ref(s));//无拷贝
    std::cout << "----------------" << std::endl;
    t4.join();
}

这里会看到无法通过编译。为什么?

一个实参从主线程传递到子线程的线程函数中,需要经过两次传递。第1次发生在std::thread构造时,此次参数按值并以副本形式被保存。第2次发生在向线程函数传递时,此次传递是由子线程发起,并将之前std::thread内部保存的副本以右值的形式(std::move())传入线程函数中的。

```String & s`````` 是不可以指向一个右值的。这里不熟悉的可以重新温故一下左值引用,右值引用。

形参Tconst T&T&& 可以接受右值, T &不可以接受右值。 因此如果函数形参是T &, 则传参时必须要使用std::ref

如何获取线程的返回值

  • 通过promise-future获取线程函数返回值
#include <iostream>
#include <iomanip>
#include <string>
#include <random>
#include <any>
#include <functional>
#include <future>
#include <thread>
#include <chrono>
#include <cstdlib>

void SetPromise(std::promise<int>& promiseObj) {
    std::cout << "In a thread, making data...\n";
    std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    promiseObj.set_value(10);
    std::cout << "Finished\n";
}

int main() {
    std::promise<int> promiseObj;
    std::future<int> futureObj = promiseObj.get_future();
    std::thread t(&SetPromise, std::ref(promiseObj));
    std::cout << futureObj.get() << std::endl;
    t.join();

    return 0;
}
  • 通过packaged_task获取函数返回值
#include <iostream>     // std::cout
#include <future>       // std::packaged_task, std::future
#include <chrono>       // std::chrono::seconds
#include <thread>       // std::thread, std::this_thread::sleep_for

// count down taking a second for each value:
int countdown (int from, int to) {
    for (int i=from; i!=to; --i) {
        std::cout << i << '\n';
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    std::cout << "Finished!\n";
    return from - to;
}

int main ()
{
    std::packaged_task<int(int,int)> task(countdown); // 设置 packaged_task
    std::future<int> ret = task.get_future(); // 获得与 packaged_task 共享状态相关联的 future 对象.

    std::thread th(std::move(task), 10, 0);   //创建一个新线程完成计数任务.

    int value = ret.get();                    // 等待任务完成并获取结果.

    std::cout << "The countdown lasted for " << value << " seconds.\n";

    th.join();
    return 0;
}
  • 使用std::async结合std::launch::async策略启动线程
#include <iostream>
#include <future>
#include <thread>
#include <chrono>
using namespace std::chrono_literals;
 
int main()
{
    std::future<int> future = std::async(std::launch::async, [](){
        std::this_thread::sleep_for(3s);
        return 8;
    });
 
    std::cout << "waiting...\n";
    std::future_status status;
    do {
        switch(status = future.wait_for(1s); status) {
            case std::future_status::deferred: std::cout << "deferred\n"; break;
            case std::future_status::timeout: std::cout << "timeout\n"; break;
            case std::future_status::ready: std::cout << "ready!\n"; break;
        }
    } while (status != std::future_status::ready);
 
    std::cout << "result is " << future.get() << '\n';
}

std::thread的native handle

c++中的线程库std::thread所提供的线程控制能力非常有限, 线程创建完成后即开始运行,只提供了joinable, join, detach,为了弥补这个不足,c++提供了一个std::thread::native_handle() 函数来获取与特性线程库实现相关的handle,以此来提供更多线程控制能力。

#include <thread>
#include <mutex>
#include <iostream>
#include <chrono>
#include <cstring>
#include <pthread.h>

std::mutex iomutex;
void f(int num)
{
    std::this_thread::sleep_for(std::chrono::seconds(1));

    sched_param sch;
    int policy;
    pthread_getschedparam(pthread_self(), &policy, &sch);
    std::lock_guard<std::mutex> lk(iomutex);
    std::cout << "Thread " << num << " is executing at priority "
              << sch.sched_priority << '\n';
}

int main()
{
    std::thread t1(f, 1), t2(f, 2);

    sched_param sch;
    int policy;
    pthread_getschedparam(t1.native_handle(), &policy, &sch);
    sch.sched_priority = 20;
    if (pthread_setschedparam(t1.native_handle(), SCHED_FIFO, &sch)) {
        std::cout << "Failed to setschedparam: " << std::strerror(errno) << '\n';
    }

    t1.join(); t2.join();
}

总结

  • c++11提供的std::thread提供了简便的创建线程的方式
  • 确保线程在所有路径上都是unjoinable的
  • 如果std::thread提供的接口不能满足你的开发需求,可以尝试用native handle操调用线程库原生的接口进行操作。
Loading...