一、进程与线程

  1. 进程是资源分配和调度的一个独立单位;而线程是进程的一个实体,是 CPU 调度和分配的基本单位
  2. 同一个进程中的多个线程的内存资源是共享的,各线程都可以改变进程中的变量。因此在执行多线程运算的时候要注意执行顺序

二、并行与并发

  • 并行(parallellism)指的是多个任务在同一时刻同时在执行
  • 并发(concurrency)是指在一个时间段内,多个任务交替进行。虽然看起来像在同时执行,但其实是交替的

三、多任务处理

多线程是多任务处理的一种特殊形式,一般情况下,有基于进程和基于线程的两种类型的多任务处理方式。

  • 基于进程的多任务处理是程序的并发执行
  • 基于线程的多任务处理是同一程序的片段的并发执行

四、C++11 线程管理 thread

  • C++11 提供了多线程库,使用时需要 #include <thread> 头文件,该头文件主要包含了对线程的管理类 std::thread 以及其他管理线程相关的类
  • 每个应用程序至少有一个进程,而每个进程至少有一个主线程,除了主线程外,在一个进程中还可以创建多个子线程。每个线程都需要一个入口函数,入口函数返回退出,该线程也会退出,主线程就是以 main 函数作为入口函数的线程
  • 主线程退出后,运行中的子线程也会被销毁
  • std::thread 的构造函数需要的是可调用(callable)类型,除了函数外,还可以调用 lambda 表达式、重载了 () 运算符的类的实例
  • 把函数对象传入 std::thread 时,应传入函数名称(不带括号)
  • 当启动一个线程后,一定要在该线程 thread 销毁前,调用 join() 或者 detach(),确定以何种方式等待线程执行结束
    • detach 方式,启动的线程自主在后台运行,当前的代码继续往下执行,不等待新线程结束
    • join 方式,等待关联的线程完成,才会继续执行 join() 后的代码
    • 在以 detach 的方式执行线程时,要将线程访问的局部数据复制到线程的空间(使用按值传递),一定要确保线程没有使用局部变量的引用或者指针,除非你能肯定该线程会在局部作用域结束前执行结束

1、调用全局函数启动线程

#include <thread>

using namespace std;

void func(int i){
    cout << i << endl;
}
int main()
{
    for(int i = 0; i < 4; ++i){
        // 创建一个线程t,第一个参数为调用的函数,第二个参数为传递的参数
        thread t(func, i);
        // 表示允许该线程在后台运行
        t.detach();
    }
    return 0;
}

输出:

Start
10
2
3

0
Finish

2、调用类成员函数启动线程

class Test{
public:
    explicit Test(int a){
        this->a = a;
    }
    void fuc1(int n){
        cout << "fuc1() n = " << n * a << endl;
    }
    static void fuc2(int n){
        cout << "static fuc2() n = " << n << endl;
    }
private:
    int a;
};
int main()
{
    Test t(5);
    thread t1(&Test::fuc1, &t, 10);
    t1.join();
    thread t2(&Test::fuc2, 100);
    t2.join();
    return 0;
}

输出:

Start
fuc1() n = 50
static fuc2() n = 100
0
Finish

3、转移线程的所有权

thread 是可移动的 (movable) 的,但不可复制的 (copyable)。可以通过 move 来改变线程的所有权,灵活的决定线程在什么时候 join 或者 detach

thread t1(f1);
thread t3(move(t1));

将线程从 t1 转移给 t3,这时候 t1 就不再拥有线程的所有权,调用 t1.join 或 t1.detach 会出现异常,要使用 t3 来管理线程。这也就意味着 thread 可以作为函数的返回类型,或者作为参数传递给函数,能够更为方便的管理线程

4、线程标识的获取

线程的标识类型为 std:🧵:id,有两种方式获得到线程的 id:

  • 通过 thread 的实例调用 get_id() 直接获取
  • 在当前线程上调用 this_thread::get_id() 获取

5、线程暂停

如果让线程从外部暂停会引发很多并发问题,这也是为什么std::thread没有直接提供pause函数的原因。如果线程在运行过程中,确实需要停顿,就可以用 this_thread::sleep_for

class Test{
public:
    explicit Test(int a){
        this->a = a;
    }
    void fuc1(int n){
        this_thread::sleep_for(chrono::seconds(3));
        cout << "fuc1() n = " << n * a << endl;
    }
    static void fuc2(int n){
        cout << "static fuc2() n = " << n << endl;
    }
private:
    int a;
};
int main()
{
    Test t(5);
    thread t1(&Test::fuc1, &t, 10);
    cout << t1.get_id() << endl;
    t1.join();
    //t1.detach();    // 主线程销后,t1 这个等待线程也会被销毁(没有执行fuc1函数的输出)
    thread t2(&Test::fuc2, 100);
    cout << t2.get_id() << endl;
    t2.join();
    cout << "main thread id: " << this_thread::get_id() << endl;
    return 0;
}

输出:

Start
140270821496576
140270813103872
static fuc2() n = 100
main thread id: 140270844794624
0
Finish

6、异常情况下等待线程完成

为了避免主线程出现异常时将子线程终结,就要保证子线程在函数退出前完成,即在函数退出前调用 join()

  • 方法一:异常捕获
void func() {
    thread t([]{
        cout << "hello C++ 11" << endl;
    });
 
    try
    {
        do_something_else();
    }
    catch (...)
    {
        t.join();
        throw;
    }
    t.join();
}
  • 方法二:资源获取即初始化(RAII)

无论是何种情况,当函数退出时,对象 guard 调用其析构函数销毁,从而能够保证 join 一定会被调用

class thread_guard
{
    private:
        thread &t;
    public:
        /*加入explicit防止隐式转换*/
        explicit thread_guard(thread& _t) {
            t = _t;
        } 
        thread_guard(const thread_guard&) = delete;  //删除默认拷贝构造函数
        thread_guard& operator=(const thread_guard&) = delete;  //删除默认赋值运算符
 
        ~thread_guard()
        {
            if (t.joinable())
                t.join();
        }
};
 
void func(){ 
    thread t([]{
        cout << "Hello thread" <<endl ;
    });
 
    thread_guard guard(t);
}