《C++ Concurrency In Action》Chapter2学习笔记

前言

期盼好久的书到了,听说这本书重新翻译后质量不错,花了 69 支持的正版,结果印刷的还不如影印版…有点失望。不过一想到能学到东西,就又开心了回来,hhh

这本书初看下对读者有一定要求,起码您要提前学过多线程,多基本概念有所涉及因为其不会解释的很清楚,并且对 C++ 语法要求较高,书中会用到 万能转发 和 简单的可变参模板 等概念


Chapter2 线程管控

即介绍最基本的线程API,C++11后的线程被从语义层面支持了,这是一大好事。不用背 pthread 那套冗长的API 就可以方便地使用线程了。不过还是更喜欢 go 的协程,不知道 C++20 的协程怎么样。

2.1 线程的基本管控

发起线程

std::thread(func);

这样这个线程就启动了,相当简单
这里 func 是可调用(callable)类型,如 lambda 或 重载了的类、函数指针。见下

class func {
public:
    int &i;
    explicit func(int& i_) : i(i_) {}
    void operator() () const {
        int sum = 0;
        for (int j = 0; j < i; ++j)
            sum += j;
        std::cout << sum << std::endl;
    }
}; 

线程初始化,往往因函数声明造成二义性。
如:std::thread g(func())
这还有可能是以 g 为函数名,std::thread 为返回对象,func() 为参数的函数。
所以初始化线程,请这样:
std::thread g(( func() )) 或 std::thread g{func()}

在使用线程时应该格外注意生存期的问题。如上面的 func 中的成员是一个引用

std::thread build() {
    // wrong !!! j is a local object
    int j = 100;
    std::thread t3{func(j)};
    return t3;
}

这里最后 j 显然已经被释放了,但 t3 线程拿取的引用将是未定义的。

线程的完成
调用 join() 或 detach()

这引出一个问题,如果线程启动后抛出异常,则 join 将被略过。所以我们只能 try catch 捕获异常处理。然而,这样的话就有些复杂了,并且作用于域更加凌乱。
我们自然想到使用 RAII 来管理线程。这是一个无比常见的方法,在某个地方如果您担心某人会忘了干什么,您应该将其交给系统来管理。下图的 thread_guard 类便是这样一个类。
就算发生异常,C++也保证其一定会调用析构函数,从而正确 JOIN。

#include 
#include 

class func {
public:
    int &i;
    explicit func(int& i_) : i(i_) {}
    void operator() () const {
        int sum = 0;
        for (int j = 0; j < i; ++j)
            sum += j;
        std::cout << sum << std::endl;
    }
};
// RAII
class thread_guard {
    std::thread &t;
public:
    explicit thread_guard(std::thread &t_) : t(t_) {}
    thread_guard(const std::thread &) = delete;
    thread_guard& operator=(const std::thread &) = delete;
    ~thread_guard() {
        // check whether the thread is joinable
        if (t.joinable())
            t.join();
    }
};

std::thread build(int &j) {
    // wrong !!! j is a local object
    // int j = 100;
    std::thread t3{func(j)};
    return t3;
}
int main() {
    int i = 10;
    func myfunc(i);
    // must be initialized by {} or (())
    std::thread t1{myfunc};
    std::thread t2 = std::move(t1);
    // return value must be a rvalue
    int j = 100;
    std::thread t4 = build(j);
    thread_guard g1(t2);
    thread_guard g2(t4);
}

如上图,我们可以看到

  1. 线程所有权从函数中返回
  2. 我们非常优雅地进行了资源的管理,完全不用再操心于线程的 join 啊 异常处理 啊啥的
  3. 我们不允许 thread_guard 发生复制,原因是我们无法保证复制出的对象的生存周期。
  4. 多次运行,结果是不一致的,有时 ‘4950 50’ 有时 ‘50 4950’ 这说明我们不能尝试预测线程的顺序

2.2 向线程函数传递参数

直接在构造函数后面添加参数即可。但
参数会被复制到线程的内部存储空间,这些参数被当做右值传递

void f(std::string &a);

// 错!传入的是指针
std::thread t{f, "hello"};
// 对
std::thread t{f, std::string("hello")};

std::string temp = "hello";
//错,temp被当做右值传递,而非const左值引用不能绑定右值
std::thread t{f, temp};
//对
std::thread t{f, std::ref(temp)};

2.3 移交线程的归属权

线程是 可移对象,其所有权可以灵活通过移动进行转换。
基于此,我们可以写出一个 scoped_thread 类,其可以直接构造线程,并管理线程。

#include 
#include 

class func {
public:
    int &i;
    explicit func(int& i_) : i(i_) {}
    void operator() () const {
        int sum = 0;
        for (int j = 0; j < i; ++j)
            sum += j;
        std::cout << sum << std::endl;
    }
};
// RAII
class scoped_thread {
    std::thread t;
public:
    template<typename Callable, typename ...Args>
    explicit scoped_thread(Callable &&func, Args&&... args)
    : t(std::forward<Callable>(func), std::forward<Args>(args)...) {}
    explicit scoped_thread(std::thread &&t_) : t(std::move(t_)) {
        if (!t.joinable())
            throw std::logic_error("No thread");
    }
    scoped_thread(const scoped_thread &) = delete;
    scoped_thread& operator=(const scoped_thread &) = delete;
    ~scoped_thread() {
        t.join();
    }
};

std::thread build(int &j) {
    // wrong !!! j is a local object
    // int j = 100;
    std::thread t3{func(j)};
    return t3;
}
int main() {
    int i = 10;
    func myfunc(i);
    // return value must be a rvalue
    int j = 100;
    std::thread t4 = build(j);
    scoped_thread g1(std::thread {myfunc});
    scoped_thread g2(std::move(t4));
    int k = 1000;
    func myfunc1(k);
    scoped_thread g3(myfunc1);
}

我的实现还添加了直接构造的构造函数,用转发进行构造。

C++ 20 引入 jthread
给线程的析构函数自带了 join,这个类类似上面的scoped_thread

我们还可以将多个线程放入 vector 管理

void do_word(unsigned id);
void f () {
	std::vector<std::thread> threads;
	for (unsigned i = 0; i < 20; ++i)
		threads.emplace_back(do_work, i);
	for (auto &entry : threads)
		entry.join();
}

2.5 识别线程

线程ID 类型:std::thread::id
调用 get_id()获取,这类 ID 是全序的。

#include 
#include 

class func {
public:
    int &i;
    explicit func(int& i_) : i(i_) {}
    void operator() () const {
        int sum = 0;
        for (int j = 0; j < i; ++j)
            sum += j;
        std::cout << sum << std::endl;
    }
};
// RAII
class scoped_thread {
    std::thread t;
public:
    template<typename Callable, typename ...Args>
    explicit scoped_thread(Callable &&func, Args&&... args)
            : t(std::forward<Callable>(func), std::forward<Args>(args)...) {
        std::cout << "thread ID : #" << t.get_id() << std::endl;
            }
    explicit scoped_thread(std::thread &&t_) : t(std::move(t_)) {
        if (!t.joinable())
            throw std::logic_error("No thread");
        std::cout << "thread ID : #" << t.get_id() << std::endl;
    }
    scoped_thread(const scoped_thread &) = delete;
    scoped_thread& operator=(const scoped_thread &) = delete;
    ~scoped_thread() {
        t.join();
    }
};
int main() {
    int i = 10;
    func myfunc(i);
    scoped_thread g1(std::thread {myfunc});
    int k = 1000;
    func myfunc1(k);
    scoped_thread g2(myfunc1);
}

总结

学完这一章简单的操作都掌握了,我们可以编写简单的资源管理类来保证线程正常完成。我们完全不能保证线程的顺序,这也导致考虑锁的时候将会很头大,hhh,不过这是后话了。

你可能感兴趣的:(C++,Concurrency,In,Action,c++,学习,笔记)