C++11开始支持多线程编程,并在之后的版本中不断完善。

Hello World单线程写法:

1
2
3
4
5
#include <iostream>
int main()
{
    std::cout << "Hello World\n";
}

Hello World多线程写法:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <thread> // 1
void hello()      // 2
{
    std::cout << "Hello Concurrent World\n";
}
int main()
{
    std::thread t(hello)// 3
    t.join();             // 4
}

量产线程的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <thread>
void do_work(unsigned id)
{
    std::cout << i << std::endl;
}

void f()
{
    std::vector<std::thread> threads;
    for (unsigned i = 0; i < 20; ++i)
    {
        threads.emplace_back(do_work, i); // 产生线程
    }
    for (auto &entry : threads) // 对每个线程调用 join()
        entry.join();
}

使用 std::lock_guard 保护共享数据:

C++中通过实例化std::mutex创建互斥量实例,通过成员函数 lock() 对互斥量上锁,unlock() 进行解锁。不过,实践中不推荐直接去调用成员函数,调用成员函数就意味着,必须在每个函数出口都要去调用 unlock(),也包括异常的情况。C++标准库为互斥量提供了一个RAII语法的模板类std::lock_guard,在构造时就能提供已锁的互斥量,并在析构的时候进行解锁,从而保证了一个已锁互斥量能被正确解锁。

代码来源于 std::lock_guard

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <thread>
#include <mutex>
#include <iostream>

int g_i = 0;
std::mutex g_i_mutex; // 保护 g_i

void safe_increment()
{
    std::lock_guard<std::mutex> lock(g_i_mutex);
    ++g_i;

    std::cout << std::this_thread::get_id() << ": " << g_i << '\n';

    // g_i_mutex 在锁离开作用域时自动释放
}

int main()
{
    std::cout << "main: " << g_i << '\n';

    std::thread t1(safe_increment);
    std::thread t2(safe_increment);

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

    std::cout << "main: " << g_i << '\n';
}

使用 std::shared_timed_mutex 避免数据竞争

shared_timed_mutex 类是能用于保护数据免受多个线程同时访问的同步原语。与其他促进排他性访问的互斥类型相反,拥有二个层次的访问:

  • 共享 - 多个线程能共享同一互斥的所有权。
  • 排他性 - 仅一个线程能占有互斥。

共享互斥通常用于多个读线程能同时访问同一资源而不导致数据竞争,但只有一个写线程能访问的情形。

代码来源于std::shared_timed_mutex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <mutex>
#include <shared_mutex>

class R
{
    mutable std::shared_timed_mutex mut;
    /* 数据 */
public:
    R &operator=(const R &other)
    {
        // 要求排他性所有权以写入 *this
        std::unique_lock<std::shared_timed_mutex> lhs(mut, std::defer_lock);
        // 要求共享所有权以读取 other
        std::shared_lock<std::shared_timed_mutex> rhs(other.mut, std::defer_lock);
        std::lock(lhs, rhs);
        /* 赋值数据 */
        return *this;
    }
};

int main()
{
    R r;
}

使用 std::scoped_lock 避免死锁

线程有对锁的竞争:一对线程需要对他们所有的互斥量做一些操作,其中每个线程都有一个互斥量,且等待另一个解锁。这样没有线程能工作,因为他们都在等待对方释放互斥量。这种情况就是死锁,它的最大问题就是由两个或两个以上的互斥量来锁定一个操作。

C++标准库有办法解决这个问题,std::scoped_lock——可以一次性锁住多个(两个以上)的互斥量,并且没有副作用(死锁风险),而且是RAII风格喵~。

以下示例用 std::scoped_lock 锁定互斥对而不死锁,且为 RAII 风格。

代码来源于std::scoped_lock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include <mutex>
#include <thread>
#include <iostream>
#include <vector>
#include <functional>
#include <chrono>
#include <string>

struct Employee
{
    Employee(std::string id) : id(id) {}
    std::string id;
    std::vector<std::string> lunch_partners;
    std::mutex m;
    std::string output() const
    {
        std::string ret = "Employee " + id + " has lunch partners: ";
        for (const auto &partner : lunch_partners)
            ret += partner + " ";
        return ret;
    }
};

void send_mail(Employee &, Employee &)
{
    // 模拟耗时的发信操作
    std::this_thread::sleep_for(std::chrono::seconds(1));
}

void assign_lunch_partner(Employee &e1, Employee &e2)
{
    static std::mutex io_mutex;
    {
        std::lock_guard<std::mutex> lk(io_mutex);
        std::cout << e1.id << " and " << e2.id << " are waiting for locks" << std::endl;
    }

    {
        // 用 std::scoped_lock 取得二个锁,而无需担心
        // 其他对 assign_lunch_partner 的调用死锁我们
        // 而且它亦提供便利的 RAII 风格机制

        std::scoped_lock lock(e1.m, e2.m);

        // 等价代码 1 (用 std::lock 和 std::lock_guard )
        // std::lock(e1.m, e2.m);
        // std::lock_guard<std::mutex> lk1(e1.m, std::adopt_lock);
        // std::lock_guard<std::mutex> lk2(e2.m, std::adopt_lock);

        // 等价代码 2 (若需要 unique_lock ,例如对于条件变量)
        // std::unique_lock<std::mutex> lk1(e1.m, std::defer_lock);
        // std::unique_lock<std::mutex> lk2(e2.m, std::defer_lock);
        // std::lock(lk1, lk2);
        {
            std::lock_guard<std::mutex> lk(io_mutex);
            std::cout << e1.id << " and " << e2.id << " got locks" << std::endl;
        }
        e1.lunch_partners.push_back(e2.id);
        e2.lunch_partners.push_back(e1.id);
    }

    send_mail(e1, e2);
    send_mail(e2, e1);
}

int main()
{
    Employee alice("alice")bob("bob")christina("christina")dave("dave");

    // 在并行线程中指派,因为就午餐指派发邮件消耗很长时间
    std::vector<std::thread> threads;
    threads.emplace_back(assign_lunch_partner, std::ref(alice), std::ref(bob));
    threads.emplace_back(assign_lunch_partner, std::ref(christina), std::ref(bob));
    threads.emplace_back(assign_lunch_partner, std::ref(christina), std::ref(alice));
    threads.emplace_back(assign_lunch_partner, std::ref(dave), std::ref(bob));

    for (auto &thread : threads)
        thread.join();
    std::cout << alice.output() << '\n'
              << bob.output() << '\n'
              << christina.output() << '\n'
              << dave.output() << '\n';
}

使用更为灵活的std::unique_lock

std::unqiue_lock 使用更为自由的不变量,这样 std::unique_lock 实例不会总与互斥量的数据类型 相关,使用起来要比 std:lock_guard 更加灵活。首先,可将 std::adopt_lock 作为第二个参数传入 构造函数,对互斥量进行管理;也可以将 std::defer_lock 作为第二个参数传递进去,表明互斥量应 保持解锁状态。这样,就可以被 std::unique_lock 对象(不是互斥量)的 lock() 函数所获取,或传递 std::unique_lock 对象到 std::lock() 中。

当你想要锁定互斥锁时,可以创建类型为 std::unique_lock的局部变量,并将该互斥锁作为参数传递。 构造unique_lock时,它将锁定互斥锁,并且销毁该互斥锁后,它将解锁该互斥锁。 更重要的是:如果引发异常,则将调用 std::unique_lock 析构函数,因此互斥量将被解锁。

示例 1 代码来源于Stack Overflow

1
2
3
4
5
6
7
8
9
10
11
12
// 示例 1
#include <mutex>
int some_shared_var = 0;

int func()
{
    int a = 3;
    {  // Critical section
        std::unique_lock<std::mutex> lock(my_mutex);
        some_shared_var += a;
    }  // End of critical section
}

示例 2 代码来源于std::unique_lock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//  示例 2
#include <mutex>
#include <thread>
#include <chrono>

struct Box
{
    explicit Box(int num) : num_things{num} {}

    int num_things;
    std::mutex m;
};

void transfer(Box &from, Box &to, int num)
{
    // 仍未实际取锁
    std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
    std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);

    // 锁两个 unique_lock 而不死锁
    std::lock(lock1, lock2);

    from.num_things -= num;
    to.num_things += num;

    // 'from.m' 与 'to.m' 互斥解锁于 'unique_lock' 析构函数
}

int main()
{
    Box acc1(100);
    Box acc2(50);

    std::thread t1(transfer, std::ref(acc1), std::ref(acc2), 10);
    std::thread t2(transfer, std::ref(acc2), std::ref(acc1), 5);

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

小结

我们现在会用C++写多线程代码啦,但是如何避免死锁,何种情况该用std::lock_guardstd::shared_timed_mutexstd::scoped_lockstd::unique_lock,仍需要多加练习噢。

本文采用CC-BY-SA-3.0协议,转载请注明出处
Author: 樱花雨