【C++学习】C++多线程初步入门---thread库

对于多线程编程,一个是并行,另外一个是并发。并行是指两个或多个独立的操作同时进行。并发是指在一个时间段内执行多个操作。例如,在常见的四核四线程可以并行四个线程,但是对于四核八线程是使用了超线程技术,把一个物理模拟为两个逻辑核心。

1 并发编程的方法

1.1 多进程并行

使用多进程并发就是将一个应用程序分为多个独立的进程(每一个进程只有一个线程),这些独立的进程之间可以相互通信。由于操作系统对进程提供了大量的保护机制,以避免一个进程修改了另一个进程的数据,使用多进程比多线程更容易写出安全的代码。但是相应的也有弊端:
(1)在进程件的通信,无论是使用信号、套接字,还是文件、管道等方式,其使用要么比较复杂,要么就是速度较慢或者两者兼而有之。
(2)运行多个进程的开销很大,操作系统要分配很多的资源对进程进行管理。

1.2 多线程并行

多线程并发指的是在同一个进程中执行多个线程。线程是轻量级的进程,每个线程可以独立的运行不同的指令序列,但是线程不独立的拥有资源,依赖于创建它的进程而存在。同一进程中的多个线程共享相同的地址空间,可以访问进程中的大部分数据,指针和引用可以在线程间进行传递。由于缺少操作系统提供的保护机制,在多线程共享数据及通信时,需要对共享数据段进行更好的处理。

2 C++11 线程的使用方式

我们使用C++11中的标准库进行测试,需要#include <thread>

#include <iostream>
#include <thread>

using namespace std;

void output(int i) {
    cout << i << endl;
}

int main() {
    for(uint8_t i=0; i < 4; ++i) {
        thread t(output, i);
        t.detach();
    }
    return 0;
}

使用thread创建一个线程对象,第一个参数是一个函数名句柄,第二参数是传入到函数名中的参数。使用detach()表明该线程是在后台进行处理,不进行绑定。在上面的代码中,每一个线程都是运行在不同的物理核心上面的,但是对于输出命令行在一个时间结点只能一个线程在占用。因此出现了一个关键的问题资源竞争

对于多线程程序而言,共享数据的管理以及线程之间的通信是两个比较大的问题。

3 线程管理

每个应用程序至少有一个进程,而每个进程至少有一个主线程,除了主线程外,在一个进程中还可以创建多个线程。每个线程都需要一个入口函数,主线程就是以main作为入口函数的线程。在C++11的标准库中,可以使用std::thread创建启动一个线程,也可以将线程挂起或者结束等操作。

3.1 启动一个线程

1.使用函数名进程创建

void function(args) {
    ...
}

std::thread thread(function, args);

2.使用匿名函数(lambda表达式)进行创建

for(int i=0; i < 4; ++i) {
        thread t([i]{
            cout << i << endl;
        });
        t.detach();
    }

3.重载()运算符的类

class Task
{
public:
    void operator()(int i)
    {
        cout << i << endl;
    }
};

int main()
{

    for (uint8_t i = 0; i < 4; i++)
    {
        Task task;
        thread t(task, i);
        t.detach(); 
    }
}

通过重载运算符隐藏了实际的线程控制的函数。

如果是类构建的对象,在创建线程的时候,如果不是一个已经创建好的对象,而是一个临时变量,那么不能使用上面的表现形式。

std::thread thread(Task());

这样子是错误的,会被认为是函数的声明式,因此应该改为下面的形式。

std::thread thread{Task{}};

4.关于detach的线程作用域

当线程启动之后,必须在线程相关联的thread对象销毁前,确定以何种方式等待线程执行结束。

(1)detach::启动的线程自主在后台执行,当前的代码往下执行,不等待新线程的结束。

(2)join:等待启动的线程完成,才会继续进行。每一个线程完成之后,才会进入下一次循环。

注意:对于detach方式,创建的新线程对当前作用域的变量的使用。创建新线程的作用域结束以后,可能线程还在继续,如果此时含有该线程对原先循环部分局部变量的引用或者是指针,那么就会出现错误。

auto fn = [](int *a){
    for(int i=0; i < 10; ++i) {
        cout << *a << endl;
    }
};

int main() {

    for(int i=0; i < 100; i++) {
        int a = 100;
        // 传递引用
        thread t(fn, &a);
        t.detach();
    }
    return 0;
}

输出结果:

100100100100
100
1937141752

在线程还在循环输出*a的时候,此时线程外部循环已经结束,因此再次调用&a的时候,找不到原有的内存位置。从而出现错误输出。因此在使用的时候,要么使用join的方式,要么使用传值方式,而不是传指针或者传引用。

3.2 异常情况下等待线程完成

对于detach模式,在执行函数thread.detach()之后,如果因为异常导致thread被销毁,但是线程依然可以在后台执行到结束。
对于join模式,如果在执行thread.join()函数前,发生异常,那么线程会跟随thread对象一起失效。因此需要对异常进行处理,在函数返回之前进行join()

void func() {
    thread t([]{
        cout << "Thread run!" << endl;
    });

    try
    {
        do_something_else();
    }
    catch (...)
    {
        t.join();
        throw;
    }
    t.join();
}

但是对于该问题还有一种更好的解决方案,RAII(资源获取即初始化)。其核心是把资源和对象的生命周期绑定,对象创建获取资源,对象销毁释放资源。因此,C++把底层的资源管理问题提升到了对象生命周期管理的更高层次。

class thread_guard {
public:

    explicit thread_guard(thread &thread_):thread1(thread_) {}

    ~thread_guard() {
        if(thread1.joinable()) {
            thread1.join();
        }
    }

    // 禁止使用guard对象进行初始化
    thread_guard(const thread_guard&) = delete;
    thread_guard& operator=(const thread_guard&) = delete;

private:

    thread &thread1;
};

void func() {
    thread thread1([] {
        cout << "Thread run!" << endl;
    });

    thread_guard _thread_guard(thread1);
}

int main() {
    func();
    return 0;
}

创建一个线程保护类,对线程进行控制,在执行构造函数的时候进行资源的分配,在执行析构函数的时候,进行资源的释放。join函数在调用时执行线程,函数返回时线程结束。

3.3 线程创建–传递参数

默认是会将传递的参数以拷贝的方式复制到线程空间。即使参数类型是引用类型。这里是指在线程启动的函数参数列表中是进行引用传值,会被默认修改。但是如果在创建thread的时候,直接传递的就是引用值,那么是很危险的(前文)。原因是即使函数接受的是引用类型,不过在创建线程的时候,会构建一个线程空间,从线程空间中进行引用。

void func(int *a, int n) {}
int buffer[10];
thread t(func, buffer, 10);
t.join();

但是如果是要在执行线程中进行对象更新,结果是无法进行的。其引用的是拷贝对象到线程空间的对象,而不是初始化希望改变的对象。因此需要使用传引用的方式。

使用普通实例化对象创建一个线程。

thread t(&对象名.函数名,参数列表)

3.4 转让线程所有权

对于thread,是可移动的(movable),但不可复制(copyable)。使用move可以修改线程的控制权。

thread t1(f1);
thread t2(move(t1));

此时调用t1.join()是错误的。可以通过get_id()获取到线程的编号。

4 总结

本文仅仅只是对C++11中的线程的基本使用进行一个简单的说明,主要参考的文章是C++ 11多线程–线程管理。对于后面的博客会对多线程的两大核心问题进行更加深入的讲解。

猜你喜欢

转载自blog.csdn.net/twt520ly/article/details/80969523