前言

经典并发案例~

基础知识

thread传参

推荐博客

thread函数都会把参数转成右值!

信号量

  • 信号量的类型:sem_t
  • int sem_init(sem_t *sem, int pshared, unsigned int value);
    • 功能:初始化信号量
    • 参数
      • sem:信号量变量的地址
      • pshared:0 用在线程间 ,非0 用在进程间
      • value:信号量中的值,代表容器大小
  • int sem_destroy(sem_t *sem);

    • 功能:释放资源
  • int sem_wait(sem_t *sem);【wait先判断再-】
    • 如果信号量的值大于0,sem_wait会立即返回,并将信号量的值减少1。
    • 如果信号量的值为0,sem_wait会阻塞,直到信号量的值变得大于0。这通常发生在另一个线程或进程增加了信号量的值,表示它已经释放了它先前持有的资源。
  • int sem_trywait(sem_t *sem);
  • int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
  • int sem_post(sem_t *sem);

    • 功能:对信号量解锁,调用一次对信号量的值+1
  • int sem_getvalue(sem_t *sem, int *sval);

future, promise和async

学习自:C++ 并发三剑客future, promise和async

async

std::async 是一个用于异步执行函数的模板函数,它返回一个 std::future 对象,该对象用于获取函数的返回值。

启动策略:

  1. std::launch::deferred:这种策略意味着任务将在调用std::future::get()std::future::wait()函数时延迟执行。换句话说,任务将在需要结果时同步执行。
  2. std::launch::async 函数异步执行
  3. std::launch::async | std::launch::deferred:这种策略是上面两个策略的组合。任务可以在一个单独的线程上异步执行,也可以延迟执行,具体取决于实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <future>
#include <chrono>
#include <thread>
using namespace std;
int fun(int num)
{
this_thread::sleep_for(chrono::seconds(3));
return num + 1;
}
signed main()
{
future<int> res = async(std::launch::deferred, fun, 1);
cout << "go here" << endl;
int cur = res.get();
cout << cur << endl;
return 0;
}

future

  • std::future::get() 用于获取并返回任务的结果,而 std::future::wait() 只是等待任务完成。
  • get() 只能调用一次,而 wait() 可以被多次调用。
  • 如果任务还没有完成,get()wait() 都会阻塞当前线程,但 get() 会一直阻塞直到任务完成并返回结果,而 wait() 只是在等待任务完成。
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
#include <iostream>
#include <future>
#include <chrono>
using namespace std;
int my_task()
{
std::this_thread::sleep_for(std::chrono::seconds(5));
std::cout << "my task run 5 s" << std::endl;
return 42;
}
void use_package()
{
// 创建一个包装了任务的 std::packaged_task 对象
std::packaged_task<int()> task(my_task);
// 获取与任务关联的 std::future 对象
std::future<int> result = task.get_future();
// 在另一个线程上执行任务
std::thread t(std::move(task));
t.detach(); // 将线程与主线程分离,以便主线程可以等待任务完成
// 等待任务完成并获取结果
int value = result.get();
std::cout << "The result is: " << value << std::endl;
}
signed main()
{
use_package();
return;
}
// ---------
#include <iostream>
#include <future>
#include <chrono>
#include <thread>

using namespace std;
int fun(int num)
{
this_thread::sleep_for(chrono::seconds(1));
return num + 1;
}
int main()
{
packaged_task<int(int)> task(fun);
future<int> res = task.get_future();
thread t(std::move(task), 1);
t.detach();
cout << res.get() << endl;
return 0;
}

promise和future结合【std::promise用于在某一线程中设置某个值或异常,而std::future则用于在另一线程中获取这个值或异常】

在C++中,std::promisestd::future 是一对配套的类,用于在不同线程之间传递值.【经典例子】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <future>
#include <chrono>
using namespace std;

void fun(promise<int> prom)
{
prom.set_value(10);
}
int main()
{
promise<int> prom;
future<int> fut = prom.get_future();
thread t(fun, std::move(prom));
cout << fut.get() << endl;
t.join();
return 0;
}

练习题

按序打印

1114. 按序打印

给你一个类:

1
2
3
4
5
public class Foo {
public void first() { print("first"); }
public void second() { print("second"); }
public void third() { print("third"); }
}

三个不同的线程 A、B、C 将会共用一个 Foo 实例。

  • 线程 A 将会调用 first() 方法
  • 线程 B 将会调用 second() 方法
  • 线程 C 将会调用 third() 方法

请设计修改程序,以确保 second() 方法在 first() 方法之后被执行,third() 方法在 second() 方法之后被执行。

提示:

  • 尽管输入中的数字似乎暗示了顺序,但是我们并不保证线程在操作系统中的调度顺序。
  • 你看到的输入格式主要是为了确保测试的全面性。

示例 1:

1
2
3
4
输入:nums = [1,2,3]
输出:"firstsecondthird"
解释:
有三个线程会被异步启动。输入 [1,2,3] 表示线程 A 将会调用 first() 方法,线程 B 将会调用 second() 方法,线程 C 将会调用 third() 方法。正确的输出是 "firstsecondthird"

示例 2:

1
2
3
4
输入:nums = [1,3,2]
输出:"firstsecondthird"
解释:
输入 [1,3,2] 表示线程 A 将会调用 first() 方法,线程 B 将会调用 third() 方法,线程 C 将会调用 second() 方法。正确的输出是 "firstsecondthird"

(1)信号量

信号量是用来实现对共享资源的同步访问的机制,通过主动等待和主动唤醒来实现的。

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
#include <memory>
#include <iostream>
#include <semaphore.h>
#include <functional>
#include <thread>
using namespace std;
// 确保 second() 方法在 first() 方法之后被执行,third() 方法在 second() 方法之后被执行。

class Foo
{
public:
sem_t firstJobDone;
sem_t secondJobDone;

public:
Foo()
{
sem_init(&firstJobDone, 0, 0);
sem_init(&secondJobDone, 0, 0);
}

void first(function<void()> printFirst)
{
// printFirst() outputs "first". Do not change or remove this line.
printFirst();
sem_post(&firstJobDone);
}

void second(function<void()> printSecond)
{
sem_wait(&firstJobDone);
// printSecond() outputs "second". Do not change or remove this line.
printSecond();
sem_post(&secondJobDone);
}

void third(function<void()> printThird)
{
sem_wait(&secondJobDone);
// printThird() outputs "third". Do not change or remove this line.
printThird();
}
};
int main()
{
Foo foo;
thread t1(&Foo::first, &foo, [&]()
{ cout << "first" << endl; });
thread t2(&Foo::second, &foo, [&]()
{ cout << "second" << endl; });
thread t3(&Foo::third, &foo, [&]()
{ cout << "third" << endl; });
t1.join();
t2.join();
t3.join();
// int num1 = 0;
// sem_getvalue(&foo.firstJobDone, &num1);
// cout << num1 << endl;
// getchar();
return 0;
}

(2)互斥锁

互斥锁是用来防止多个线程同时访问共享资源对象的机制,在同一时间只有一个线程可以拥有一个特定的锁对象,其他线程如果尝试获取锁会阻塞直到锁资源被释放或直接返回失败。

针对这道题我们可以用两个互斥锁来阻塞 second 和 third 函数,分别在 first 和 second 执行结束后解锁。

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
#include <memory>
#include <iostream>
#include <semaphore.h>
#include <functional>
#include <thread>
#include <mutex>
using namespace std;
// 确保 second() 方法在 first() 方法之后被执行,third() 方法在 second() 方法之后被执行。

class Foo
{
public:
mutex mx1, mx2;

public:
Foo()
{
mx1.lock();
mx2.lock();
}

void first(function<void()> printFirst)
{
// printFirst() outputs "first". Do not change or remove this line.
printFirst();
mx1.unlock();
}

void second(function<void()> printSecond)
{
// printSecond() outputs "second". Do not change or remove this line.
mx1.lock();
printSecond();
mx2.unlock();
}

void third(function<void()> printThird)
{
// printThird() outputs "third". Do not change or remove this line.
mx2.lock();
printThird();
}
};
int main()
{
Foo foo;
thread t1(&Foo::first, &foo, [&]()
{ cout << "first" << endl; });
thread t2(&Foo::second, &foo, [&]()
{ cout << "second" << endl; });
thread t3(&Foo::third, &foo, [&]()
{ cout << "third" << endl; });
t1.join();
t2.join();
t3.join();
// int num1 = 0;
// sem_getvalue(&foo.firstJobDone, &num1);
// cout << num1 << endl;
// getchar();
return 0;
}

(3)条件变量

条件变量一般和互斥锁搭配使用,互斥锁用于上锁,条件变量用于在多线程环境中等待特定事件发生。这里一把锁和2把锁都是OK的!

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
#include <memory>
#include <iostream>
#include <semaphore.h>
#include <functional>
#include <thread>
#include <condition_variable>
#include <mutex>
using namespace std;
// 确保 second() 方法在 first() 方法之后被执行,third() 方法在 second() 方法之后被执行。

class Foo
{
public:
int k = 0;
condition_variable cv;
mutex mtx1, mtx2;

public:
Foo()
{
}
void first(function<void()> printFirst)
{
// printFirst() outputs "first". Do not change or remove this line.
printFirst();
k = 1;
cv.notify_all();
}

void second(function<void()> printSecond)
{
// printSecond() outputs "second". Do not change or remove this line.
unique_lock<mutex> lock(mtx1);
cv.wait(lock, [this]()
{ return k == 1; });
printSecond();
k = 2;
cv.notify_all();
}

void third(function<void()> printThird)
{
// printThird() outputs "third". Do not change or remove this line.
unique_lock<mutex> lock(mtx2);
cv.wait(lock, [this]()
{ return k == 2; });
printThird();
}
};
int main()
{
Foo foo;
thread t1(&Foo::first, &foo, [&]()
{ cout << "first" << endl; });
thread t2(&Foo::second, &foo, [&]()
{ cout << "second" << endl; });
thread t3(&Foo::third, &foo, [&]()
{ cout << "third" << endl; });
t1.join();
t2.join();
t3.join();
// int num1 = 0;
// sem_getvalue(&foo.firstJobDone, &num1);
// cout << num1 << endl;
// getchar();
return 0;
}

(4)异步操作

异步操作是一种,在不需要等待被调用方返回结果之前,就让操作继续进行下去的方法。针对这道题可以使用基于 future/promise 的异步编程模型。

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
#include <memory>
#include <iostream>
#include <semaphore.h>
#include <functional>
#include <thread>
#include <condition_variable>
#include <mutex>
#include <future>
using namespace std;
// 确保 second() 方法在 first() 方法之后被执行,third() 方法在 second() 方法之后被执行。

class Foo
{
public:
promise<int> num1, num2;

public:
Foo()
{
}
void first(function<void()> printFirst)
{
// printFirst() outputs "first". Do not change or remove this line.
printFirst();
num1.set_value(1);
}

void second(function<void()> printSecond)
{
int num = num1.get_future().get();
// printSecond() outputs "second". Do not change or remove this line.
printSecond();
num2.set_value(1);
}

void third(function<void()> printThird)
{
// printThird() outputs "third". Do not change or remove this line.
int num = num2.get_future().get();
printThird();
}
};
int main()
{
Foo foo;
thread t1(&Foo::first, &foo, [&]()
{ cout << "first" << endl; });
thread t2(&Foo::second, &foo, [&]()
{ cout << "second" << endl; });
thread t3(&Foo::third, &foo, [&]()
{ cout << "third" << endl; });
t1.join();
t2.join();
t3.join();
// int num1 = 0;
// sem_getvalue(&foo.firstJobDone, &num1);
// cout << num1 << endl;
// getchar();
return 0;
}

(5)原子操作

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
#include <memory>
#include <iostream>
#include <semaphore.h>
#include <functional>
#include <thread>
#include <condition_variable>
#include <mutex>
#include <future>
#include <atomic>
using namespace std;
// 确保 second() 方法在 first() 方法之后被执行,third() 方法在 second() 方法之后被执行。

class Foo
{
public:
atomic<bool> a{false};
atomic<bool> b{false};

public:
Foo()
{
}
void first(function<void()> printFirst)
{
// printFirst() outputs "first". Do not change or remove this line.
printFirst();
a = true;
}

void second(function<void()> printSecond)
{
while (!a)
this_thread::sleep_for(chrono::milliseconds(1));
// printSecond() outputs "second". Do not change or remove this line.
printSecond();
b = true;
}

void third(function<void()> printThird)
{
// printThird() outputs "third". Do not change or remove this line.
while (!b)
this_thread::sleep_for(chrono::milliseconds(1));
printThird();
}
};
int main()
{
Foo foo;
thread t1(&Foo::first, &foo, [&]()
{ cout << "first" << endl; });
thread t2(&Foo::second, &foo, [&]()
{ cout << "second" << endl; });
thread t3(&Foo::third, &foo, [&]()
{ cout << "third" << endl; });
t1.join();
t2.join();
t3.join();
// int num1 = 0;
// sem_getvalue(&foo.firstJobDone, &num1);
// cout << num1 << endl;
// getchar();
return 0;
}
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
#include <memory>
#include <iostream>
#include <semaphore.h>
#include <functional>
#include <thread>
#include <condition_variable>
#include <mutex>
#include <future>
#include <atomic>
using namespace std;
// 确保 second() 方法在 first() 方法之后被执行,third() 方法在 second() 方法之后被执行。

class Foo
{
public:
bool a{false};
bool b{false};

public:
Foo()
{
}
void first(function<void()> printFirst)
{
// printFirst() outputs "first". Do not change or remove this line.
printFirst();
a = true;
}

void second(function<void()> printSecond)
{
while (!a)
this_thread::sleep_for(chrono::milliseconds(1));
// printSecond() outputs "second". Do not change or remove this line.
printSecond();
b = true;
}

void third(function<void()> printThird)
{
// printThird() outputs "third". Do not change or remove this line.
while (!b)
this_thread::sleep_for(chrono::milliseconds(1));
printThird();
}
};
int main()
{
Foo foo;
thread t1(&Foo::first, &foo, [&]()
{ cout << "first" << endl; });
thread t2(&Foo::second, &foo, [&]()
{ cout << "second" << endl; });
thread t3(&Foo::third, &foo, [&]()
{ cout << "third" << endl; });
t1.join();
t2.join();
t3.join();
// int num1 = 0;
// sem_getvalue(&foo.firstJobDone, &num1);
// cout << num1 << endl;
// getchar();
return 0;
}

交替打印 FooBar

交替打印 FooBar

给你一个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
class FooBar {
public void foo() {
for (int i = 0; i < n; i++) {
print("foo");
}
}

public void bar() {
for (int i = 0; i < n; i++) {
print("bar");
}
}
}

两个不同的线程将会共用一个 FooBar 实例:

  • 线程 A 将会调用 foo() 方法,而
  • 线程 B 将会调用 bar() 方法

请设计修改程序,以确保 "foobar" 被输出 n 次。

示例 1:

1
2
3
输入:n = 1
输出:"foobar"
解释:这里有两个线程被异步启动。其中一个调用 foo() 方法, 另一个调用 bar() 方法,"foobar" 将被输出一次。

示例 2:

1
2
3
输入:n = 2
输出:"foobarfoobar"
解释:"foobar" 将被输出两次。

提示:

  • 1 <= n <= 1000

(1)信号量

将其中一个信号量设置成1就可以啦。

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
#include <memory>
#include <iostream>
#include <semaphore.h>
#include <functional>
#include <thread>
#include <condition_variable>
#include <mutex>
#include <future>
#include <atomic>
using namespace std;

class FooBar
{
private:
int n;
sem_t sem1, sem2;

public:
FooBar(int n)
{
this->n = n;
sem_init(&sem1, 0, 0);
sem_init(&sem2, 0, 1);
}

void foo(function<void()> printFoo)
{

for (int i = 0; i < n; i++)
{

// printFoo() outputs "foo". Do not change or remove this line.
sem_wait(&sem2);
printFoo();
sem_post(&sem1);
}
}

void bar(function<void()> printBar)
{

for (int i = 0; i < n; i++)
{

// printBar() outputs "bar". Do not change or remove this line.
sem_wait(&sem1);
printBar();
sem_post(&sem2);
}
}
};
int main()
{
FooBar fooBar(2);
thread t1(&FooBar::foo, &fooBar, [&]()
{ cout << "foo" << endl; });
thread t2(&FooBar::bar, &fooBar, [&]()
{ cout << "bar" << endl; });

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

// int num1 = 0;
// sem_getvalue(&foo.firstJobDone, &num1);
// cout << num1 << endl;
// getchar();
return 0;
}

(2) 互斥锁

和上一题类似

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
class FooBar
{
private:
int n;
mutex mxt1, mtx2;

public:
FooBar(int n)
{
this->n = n;
mxt1.lock();
}

void foo(function<void()> printFoo)
{

for (int i = 0; i < n; i++)
{

// printFoo() outputs "foo". Do not change or remove this line.
mtx2.lock();
printFoo();
mxt1.unlock();
}
}

void bar(function<void()> printBar)
{

for (int i = 0; i < n; i++)
{

// printBar() outputs "bar". Do not change or remove this line.
mxt1.lock();
printBar();
mtx2.unlock();
}
}
};

(3)条件变量

和上一道的条件变量类似

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
class FooBar
{
private:
int n;
int k;
mutex mtx;
condition_variable cv;

public:
FooBar(int n)
{
this->n = n;
k = 1;
}

void foo(function<void()> printFoo)
{

for (int i = 0; i < n; i++)
{

// printFoo() outputs "foo". Do not change or remove this line.
unique_lock<mutex> lock(mtx);
cv.wait(lock, [this]()
{ return k == 1; });
printFoo();
k = 2;
cv.notify_all();
}
}

void bar(function<void()> printBar)
{

for (int i = 0; i < n; i++)
{

// printBar() outputs "bar". Do not change or remove this line.
unique_lock<mutex> lock(mtx);
cv.wait(lock, [this]()
{ return k == 2; });
printBar();
k = 1;
cv.notify_all();
}
}
};

(4)异步编程

Note:在你的代码中,你使用了 std::promisestd::future 来同步 foobar 方法。然而,你在每个循环迭代中都调用了 get() 方法,这导致了问题。std::future::get() 方法只能被调用一次,因为它会移动(而不是复制)值。如果你尝试第二次调用 get(),会抛出一个 std::future_error 异常。

解决这个问题的一种方法是在每次迭代中重新创建 std::promisestd::future 对象。你可以在循环的每一次迭代后用新的 std::promise 对象替换旧的对象,然后从新的 std::promise 对象获取新的 std::future 对象。

不过我这里用投机的方式创建一个新的对象即可。

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
class FooBar
{
private:
int n;
int k;
promise<int> p1, p2;

public:
FooBar(int n)
{
this->n = n;
k = 1;
p1.set_value(1);
}

void foo(function<void()> printFoo)
{

for (int i = 0; i < n; i++)
{

// printFoo() outputs "foo". Do not change or remove this line.
p1.get_future().wait();
printFoo();
p1 = std::promise<int>();
p2.set_value(1);
}
}

void bar(function<void()> printBar)
{

for (int i = 0; i < n; i++)
{

// printBar() outputs "bar". Do not change or remove this line.
p2.get_future().wait();
printBar();
p2 = std::promise<int>();

p1.set_value(1);
}
}
};

使用promise和future结合

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
class FooBar
{
private:
int n;
promise<int> a, b;
future<int> aa, bb;

public:
FooBar(int n)
{
this->n = n;
aa = a.get_future();
bb = b.get_future();
b.set_value(1);
}

void foo(function<void()> printFoo)
{

for (int i = 0; i < n; i++)
{

// printFoo() outputs "foo". Do not change or remove this line.
int tmp = bb.get();
printFoo();
promise<int> tmp_b;
future<int> tmp_bb;
b = move(tmp_b);
bb = move(tmp_bb);
bb = b.get_future();
a.set_value(1);
}
}

void bar(function<void()> printBar)
{

for (int i = 0; i < n; i++)
{

// printBar() outputs "bar". Do not change or remove this line.
int tmp = aa.get();
printBar();
promise<int> tmp_a;
future<int> tmp_aa;
a = move(tmp_a);
aa = move(tmp_aa);
aa = a.get_future();
b.set_value(1);
}
}
};

(5)原子操作

貌似用不用atomic都可以~可能这才是最纯粹的控制并发的方法

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
class FooBar
{
private:
int n;
int k;
bool a{false};

public:
FooBar(int n)
{
this->n = n;
k = 1;
}

void foo(function<void()> printFoo)
{

for (int i = 0; i < n; i++)
{

// printFoo() outputs "foo". Do not change or remove this line.
while (a)
{
this_thread::sleep_for(chrono::milliseconds(1));
}

printFoo();
a = true;
}
}

void bar(function<void()> printBar)
{

for (int i = 0; i < n; i++)
{

// printBar() outputs "bar". Do not change or remove this line.
while (!a)
{
this_thread::sleep_for(chrono::milliseconds(1));
}

printBar();
a = false;
}
}
};

打印零与奇偶数

打印零与奇偶数

现有函数 printNumber 可以用一个整数参数调用,并输出该整数到控制台。

  • 例如,调用 printNumber(7) 将会输出 7 到控制台。

给你类 ZeroEvenOdd 的一个实例,该类中有三个函数:zeroevenoddZeroEvenOdd 的相同实例将会传递给三个不同线程:

  • 线程 A:调用 zero() ,只输出 0
  • 线程 B:调用 even() ,只输出偶数
  • 线程 C:调用 odd() ,只输出奇数

修改给出的类,以输出序列 "010203040506..." ,其中序列的长度必须为 2n

实现 ZeroEvenOdd 类:

  • ZeroEvenOdd(int n) 用数字 n 初始化对象,表示需要输出的数。
  • void zero(printNumber) 调用 printNumber 以输出一个 0 。
  • void even(printNumber) 调用printNumber 以输出偶数。
  • void odd(printNumber) 调用 printNumber 以输出奇数。

示例 1:

1
2
3
输入:n = 2
输出:"0102"
解释:三条线程异步执行,其中一个调用 zero(),另一个线程调用 even(),最后一个线程调用odd()。正确的输出为 "0102"

示例 2:

1
2
输入:n = 5
输出:"0102030405"

提示:

  • 1 <= n <= 1000

(1)原子变量

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
84
85
86
#include <memory>
#include <iostream>
#include <semaphore.h>
#include <functional>
#include <thread>
#include <condition_variable>
#include <mutex>
#include <future>
#include <atomic>
using namespace std;

class ZeroEvenOdd
{
private:
int n;
atomic<int> flag{0};
atomic<int> cnt;

public:
ZeroEvenOdd(int n) : cnt(1)
{
this->n = n;
}

// printNumber(x) outputs "x", where x is an integer.
void zero(function<void(int)> printNumber)
{
for (int i = 1; i <= n; i++)
{
while (flag != 0)
{
this_thread::sleep_for(chrono::milliseconds(1));
}
printNumber(0);
if (i % 2)
{
flag = 1;
}
else
{
flag = 2;
}
}
}

void even(function<void(int)> printNumber)
{
for (int i = 2; i <= n; i += 2)
{
while (flag != 2)
{
this_thread::sleep_for(chrono::milliseconds(1));
}
printNumber(i);
flag = 0;
}
}

void odd(function<void(int)> printNumber)
{
for (int i = 1; i <= n; i += 2)
{
while (flag != 1)
{
this_thread::sleep_for(chrono::milliseconds(1));
}
printNumber(i);
flag = 0;
}
}
};
int main()
{
ZeroEvenOdd zeroEvenOdd(2);
thread t1(&ZeroEvenOdd::zero, &zeroEvenOdd, [&](int num)
{ cout << num << endl; });
thread t2(&ZeroEvenOdd::even, &zeroEvenOdd, [&](int num)
{ cout << num << endl; });
thread t3(&ZeroEvenOdd::odd, &zeroEvenOdd, [&](int num)
{ cout << num << endl; });
t1.join();
t2.join();
t3.join();
getchar();
return 0;
}

(2)信号量

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
#include <memory>
#include <iostream>
#include <semaphore.h>
#include <functional>
#include <thread>
#include <condition_variable>
#include <mutex>
#include <future>
#include <atomic>
using namespace std;

class ZeroEvenOdd
{
private:
int n;
sem_t zero1, even1, odd1;

public:
ZeroEvenOdd(int n)
{
this->n = n;
sem_init(&zero1, 0, 1);
sem_init(&even1, 0, 0);
sem_init(&odd1, 0, 0);
}

// printNumber(x) outputs "x", where x is an integer.
void zero(function<void(int)> printNumber)
{
for (int i = 1; i <= n; i++)
{
sem_wait(&zero1);
printNumber(0);
if (i % 2)
{
sem_post(&odd1);
}
else
{
sem_post(&even1);
}
}
}

void even(function<void(int)> printNumber)
{
for (int i = 2; i <= n; i += 2)
{

sem_wait(&even1);
printNumber(i);
sem_post(&zero1);
}
}

void odd(function<void(int)> printNumber)
{
for (int i = 1; i <= n; i += 2)
{

sem_wait(&odd1);
printNumber(i);
sem_post(&zero1);
}
}
};
int main()
{
ZeroEvenOdd zeroEvenOdd(2);
thread t1(&ZeroEvenOdd::zero, &zeroEvenOdd, [&](int num)
{ cout << num << endl; });
thread t2(&ZeroEvenOdd::even, &zeroEvenOdd, [&](int num)
{ cout << num << endl; });
thread t3(&ZeroEvenOdd::odd, &zeroEvenOdd, [&](int num)
{ cout << num << endl; });
t1.join();
t2.join();
t3.join();
getchar();
return 0;
}

(3)条件变量+互斥锁

记得要写notify_all()

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
class ZeroEvenOdd
{
private:
int n;
condition_variable cv;
mutex mtx, mtx1, mtx2;
int flag = 0;

public:
ZeroEvenOdd(int n) : flag(0)
{
this->n = n;
}

// printNumber(x) outputs "x", where x is an integer.
void zero(function<void(int)> printNumber)
{
for (int i = 1; i <= n; i++)
{
unique_lock<mutex> lock(mtx);
cv.wait(lock, [this]()
{ return flag == 0; });
printNumber(0);
if (i % 2)
flag = 1;
else
flag = 2;
cv.notify_all();
}
}

void even(function<void(int)> printNumber)
{
for (int i = 2; i <= n; i += 2)
{
unique_lock<mutex> lock(mtx2);
cv.wait(lock, [this]()
{ return flag == 2; });
printNumber(i);
flag = 0;
cv.notify_all();
}
}

void odd(function<void(int)> printNumber)
{
for (int i = 1; i <= n; i += 2)
{
unique_lock<mutex> lock(mtx1);
cv.wait(lock, [this]()
{ return flag == 1; });
printNumber(i);
flag = 0;
cv.notify_all();
}
}
};

(4)互斥锁

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
class ZeroEvenOdd
{
private:
int n;
condition_variable cv;
mutex mtx, mtx1, mtx2;
int flag = 0;

public:
ZeroEvenOdd(int n) : flag(0)
{
this->n = n;
mtx1.lock();
mtx2.lock();
}

// printNumber(x) outputs "x", where x is an integer.
void zero(function<void(int)> printNumber)
{
for (int i = 1; i <= n; i++)
{
mtx.lock();
printNumber(0);
if (i % 2)
{
mtx1.unlock();
}
else
{
mtx2.unlock();
}
}
}

void even(function<void(int)> printNumber)
{
for (int i = 2; i <= n; i += 2)
{
mtx2.lock();
printNumber(i);
mtx.unlock();
}
}

void odd(function<void(int)> printNumber)
{
for (int i = 1; i <= n; i += 2)
{
mtx1.lock();
printNumber(i);
mtx.unlock();
}
}
};

H2O生成

1117. H2O 生成

现在有两种线程,氧 oxygen 和氢 hydrogen,你的目标是组织这两种线程来产生水分子。

存在一个屏障(barrier)使得每个线程必须等候直到一个完整水分子能够被产生出来。

氢和氧线程会被分别给予 releaseHydrogenreleaseOxygen 方法来允许它们突破屏障。

这些线程应该三三成组突破屏障并能立即组合产生一个水分子。

你必须保证产生一个水分子所需线程的结合必须发生在下一个水分子产生之前。

换句话说:

  • 如果一个氧线程到达屏障时没有氢线程到达,它必须等候直到两个氢线程到达。
  • 如果一个氢线程到达屏障时没有其它线程到达,它必须等候直到一个氧线程和另一个氢线程到达。

书写满足这些限制条件的氢、氧线程同步代码。

示例 1:

1
2
3
输入: water = "HOH"
输出: "HHO"
解释: "HOH""OHH" 依然都是有效解。

示例 2:

1
2
3
输入: water = "OOHHHH"
输出: "HHOHHO"
解释: "HOHHHO", "OHHHHO", "HHOHOH", "HOHHOH", "OHHHOH", "HHOOHH", "HOHOHH""OHHOHH" 依然都是有效解。

提示:

  • 3 * n == water.length
  • 1 <= n <= 20
  • water[i] == 'O' or 'H'
  • 输入字符串 water 中的 ‘H’ 总数将会是 2 * n
  • 输入字符串 water 中的 ‘O’ 总数将会是 n

思路:

一道典型的阅读理解题。

题目的意思是,要一个水分子,一个水分子的生成。一个水分子是两个H一个O,也就是要运行两次hydrogen和一次oxygen。要想保证水分子正常生成,那么当有了2个H时,就不能再,执行hydrogen了,它就要等。同理,当有了1个O时,就不能再执行oxygen,就要等。

同时,当任何时候凑够了2个H,1个O时,也即能生成水分子时,就要把计数重置,以生成下一个水分子。

一个水分子内部,是不用管顺序的,也即HHO,OHH,HOH都是合法的。

互斥锁

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
class H2O {
public:
mutex mtx1,mtx2;
int cnt=2;
H2O() {

}

void hydrogen(function<void()> releaseHydrogen) {
mtx1.lock();
// releaseHydrogen() outputs "H". Do not change or remove this line.
releaseHydrogen();
cnt--;
if(cnt>0) mtx1.unlock();
if(cnt==0) mtx2.unlock();
}

void oxygen(function<void()> releaseOxygen) {
mtx2.lock();
// releaseOxygen() outputs "O". Do not change or remove this line.
releaseOxygen();
cnt+=2;
mtx1.unlock();
}
};

交替打印字符串

交替打印字符串

编写一个可以从 1 到 n 输出代表这个数字的字符串的程序,但是:

  • 如果这个数字可以被 3 整除,输出 “fizz”。
  • 如果这个数字可以被 5 整除,输出 “buzz”。
  • 如果这个数字可以同时被 3 和 5 整除,输出 “fizzbuzz”。

例如,当 n = 15,输出: 1, 2, fizz, 4, buzz, fizz, 7, 8, fizz, buzz, 11, fizz, 13, 14, fizzbuzz

假设有这么一个类:

1
2
3
4
5
6
7
class FizzBuzz {
public FizzBuzz(int n) { ... } // constructor
public void fizz(printFizz) { ... } // only output "fizz"
public void buzz(printBuzz) { ... } // only output "buzz"
public void fizzbuzz(printFizzBuzz) { ... } // only output "fizzbuzz"
public void number(printNumber) { ... } // only output the numbers
}

请你实现一个有四个线程的多线程版 FizzBuzz, 同一个 FizzBuzz 实例会被如下四个线程使用:

  1. 线程A将调用 fizz() 来判断是否能被 3 整除,如果可以,则输出 fizz
  2. 线程B将调用 buzz() 来判断是否能被 5 整除,如果可以,则输出 buzz
  3. 线程C将调用 fizzbuzz() 来判断是否同时能被 3 和 5 整除,如果可以,则输出 fizzbuzz
  4. 线程D将调用 number() 来实现输出既不能被 3 整除也不能被 5 整除的数字。

提示:

  • 本题已经提供了打印字符串的相关方法,如 printFizz() 等,具体方法名请参考答题模板中的注释部分。

1.互斥锁+条件变量

一波暴力出奇迹~

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#include <memory>
#include <iostream>
#include <semaphore.h>
#include <functional>
#include <thread>
#include <condition_variable>
#include <mutex>
#include <future>
#include <atomic>
using namespace std;

class FizzBuzz
{
private:
int n;
condition_variable cv;
int cnt = 1;
mutex mtx;

public:
FizzBuzz(int n)
{
this->n = n;
}

// printFizz() outputs "fizz".
void fizz(function<void()> printFizz)
{
while (cnt <= n)
{
unique_lock<mutex> lock(mtx);
cv.wait(lock, [this]()
{
if(cnt>n){
return true;
}
if(cnt%3==0&&cnt%5!=0){
return true;
}
return false; });
if (cnt > n)
break;
;
printFizz();
cnt++;
cv.notify_all();
}
}

// printBuzz() outputs "buzz".
void buzz(function<void()> printBuzz)
{
while (cnt <= n)
{
unique_lock<mutex> lock(mtx);
cv.wait(lock, [this]()
{
if(cnt>n){
return true;
}
if(cnt%3!=0&&cnt%5==0){
return true;
}
return false; });
if (cnt > n)
break;
;
printBuzz();
cnt++;
cv.notify_all();
}
}

// printFizzBuzz() outputs "fizzbuzz".
void fizzbuzz(function<void()> printFizzBuzz)
{
while (cnt <= n)
{
unique_lock<mutex> lock(mtx);
cv.wait(lock, [this]()
{
if(cnt>n){
return true;
}
if(cnt%5==0&&cnt%3==0){
return true;
}
return false; });
if (cnt > n)
break;
;
printFizzBuzz();
cnt++;
cv.notify_all();
}
}

// printNumber(x) outputs "x", where x is an integer.
void number(function<void(int)> printNumber)
{
while (cnt <= n)
{
unique_lock<mutex> lock(mtx);
cv.wait(lock, [this]()
{
if(cnt>n){
return true;
}
if(cnt%5==0||cnt%3==0){
return false;
}
return true; });
if (cnt > n)
break;
;
printNumber(cnt);
cnt++;
cv.notify_all();
}
}
};
int main()
{
FizzBuzz tmp(15);
thread t1(&FizzBuzz::fizz, &tmp, []()
{ cout << "fizz" << endl; });
thread t2(&FizzBuzz::buzz, &tmp, []()
{ cout << "buzz" << endl; });
thread t3(&FizzBuzz::fizzbuzz, &tmp, []()
{ cout << "fizzbuzz" << endl; });
thread t4(&FizzBuzz::number, &tmp, [](int num)
{ cout << num << endl; });
t1.join();
t2.join();
t3.join();
t4.join();
// getchar();
return 0;
}

哲学家进餐

哲学家进餐

5 个沉默寡言的哲学家围坐在圆桌前,每人面前一盘意面。叉子放在哲学家之间的桌面上。(5 个哲学家,5 根叉子)

所有的哲学家都只会在思考和进餐两种行为间交替。哲学家只有同时拿到左边和右边的叉子才能吃到面,而同一根叉子在同一时间只能被一个哲学家使用。每个哲学家吃完面后都需要把叉子放回桌面以供其他哲学家吃面。只要条件允许,哲学家可以拿起左边或者右边的叉子,但在没有同时拿到左右叉子时不能进食。

假设面的数量没有限制,哲学家也能随便吃,不需要考虑吃不吃得下。

设计一个进餐规则(并行算法)使得每个哲学家都不会挨饿;也就是说,在没有人知道别人什么时候想吃东西或思考的情况下,每个哲学家都可以在吃饭和思考之间一直交替下去。

image-20240517094400802

哲学家从 04顺时针 编号。请实现函数 void wantsToEat(philosopher, pickLeftFork, pickRightFork, eat, putLeftFork, putRightFork)

  • philosopher 哲学家的编号。
  • pickLeftForkpickRightFork 表示拿起左边或右边的叉子。
  • eat 表示吃面。
  • putLeftForkputRightFork 表示放下左边或右边的叉子。
  • 由于哲学家不是在吃面就是在想着啥时候吃面,所以思考这个方法没有对应的回调。

给你 5 个线程,每个都代表一个哲学家,请你使用类的同一个对象来模拟这个过程。在最后一次调用结束之前,可能会为同一个哲学家多次调用该函数。

示例:

1
2
3
4
5
6
7
8
9
10
输入:n = 1
输出:[[4,2,1],[4,1,1],[0,1,1],[2,2,1],[2,1,1],[2,0,3],[2,1,2],[2,2,2],[4,0,3],[4,1,2],[0,2,1],[4,2,2],[3,2,1],[3,1,1],[0,0,3],[0,1,2],[0,2,2],[1,2,1],[1,1,1],[3,0,3],[3,1,2],[3,2,2],[1,0,3],[1,1,2],[1,2,2]]
解释:
n 表示每个哲学家需要进餐的次数。
输出数组描述了叉子的控制和进餐的调用,它的格式如下:
output[i] = [a, b, c] (3个整数)
- a 哲学家编号。
- b 指定叉子:{1 : 左边, 2 : 右边}.
- c 指定行为:{1 : 拿起, 2 : 放下, 3 : 吃面}。
如 [4,2,1] 表示 4 号哲学家拿起了右边的叉子。

提示:

  • 1 <= n <= 60

学过操作系统的童鞋们都比较好懂。

开始这个问题之前,想个最暴力的做法,就是每次只能让一个人吃。一把大锁即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class DiningPhilosophers
{
public:
mutex mtx;
DiningPhilosophers()
{
}

void wantsToEat(int philosopher,
function<void()> pickLeftFork,
function<void()> pickRightFork,
function<void()> eat,
function<void()> putLeftFork,
function<void()> putRightFork)
{
mtx.lock();
pickLeftFork();
pickRightFork();
eat();
putLeftFork();
putRightFork();
mtx.unlock();
}
};

PV操作经典例题——哲学家进餐问题

分析:放在桌子上的筷子是临界资源,在一段时间内只允许一位哲学家使用,为了实现对筷子的互斥访问,可以用一个信号量表示筷子,由这五个信号量构成信号量数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
semaphore chopstick[5] = {1,1,1,1,1};
while(true)
{
/*当哲学家饥饿时,总是先拿左边的筷子,再拿右边的筷子*/
wait(chopstick[i]);
wait(chopstick[(i+1)%5]);

// 吃饭

/*当哲学家进餐完成后,总是先放下左边的筷子,再放下右边的筷子*/
signal(chopstick[i]);
signal(chopstick[(i+1)%5]);
}
  • 上述的代码可以保证不会有两个相邻的哲学家同时进餐,但却可能引起死锁的情况。假如五位哲学家同时饥饿而都拿起的左边的筷子,就会使五个信号量chopstick都为0,当他们试图去拿右手边的筷子时,都将无筷子而陷入无限期的等待。

为避免死锁,可以使用以下三种策略

策略一至多只允许四个哲学家同时进餐,以保证至少有一个哲学家能够进餐,最终总会释放出他所使用过的两支筷子,从而可使更多的哲学家进餐。定义信号量count,只允许4个哲学家同时进餐,这样就能保证至少有一个哲学家可以就餐。

错误代码

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
#include <memory>
#include <iostream>
#include <semaphore.h>
#include <functional>
#include <thread>
#include <condition_variable>
#include <mutex>
#include <future>
#include <vector>
#include <atomic>
using namespace std;
class DiningPhilosophers
{
public:
sem_t sem;
DiningPhilosophers()
{
sem_init(&sem, 0, 4);
}

void wantsToEat(int philosopher,
function<void()> pickLeftFork,
function<void()> pickRightFork,
function<void()> eat,
function<void()> putLeftFork,
function<void()> putRightFork)
{
sem_wait(&sem);
pickLeftFork();
pickRightFork();
eat();
putLeftFork();
putRightFork();
sem_post(&sem);
}
};
signed main()
{
DiningPhilosophers diningPhilosophers;
int nPhilosophers = 5;
vector<thread> philosophers(nPhilosophers);
for (int i = 0; i < nPhilosophers; i++)
{
philosophers[i] = thread(&DiningPhilosophers::wantsToEat, &diningPhilosophers, i, [i]()
{ cout << "Philosopher " << i << " picks up left fork\n"; }, [i]()
{ cout << "Philosopher " << i << " picks up right fork\n"; }, [i]()
{ cout << "Philosopher " << i << " eats\n"; }, [i]()
{ cout << "Philosopher " << i << " puts down left fork\n"; }, [i]()
{ cout << "Philosopher " << i << " puts down right fork\n"; });
}
for (int i = 0; i < nPhilosophers; i++)
{
philosophers[i].join();
}
return 0;
}

分析:本代码使用了一个信号量来限制同时吃饭的哲学家的数量。这是一个常见的解决方案,可以避免死锁。但是,没有考虑到叉子的使用情况。所有的哲学家都试图先拿起左边的叉子,然后拿起右边的叉子。这可能会导致一个问题:如果所有的哲学家都拿起了他们左边的叉子,那么他们的右边的叉子就都被他们右边的哲学家拿走了,因此每个哲学家都无法吃饭,导致所有哲学家都在等待,从而产生死锁。

简单来说,我们需要考虑哲学家人数问题,也要考虑到叉子的资源使用。

正确代码

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
class DiningPhilosophers
{
public:
sem_t sem;
mutex mtx[5];
DiningPhilosophers()
{
sem_init(&sem, 0, 4);
}

void wantsToEat(int philosopher,
function<void()> pickLeftFork,
function<void()> pickRightFork,
function<void()> eat,
function<void()> putLeftFork,
function<void()> putRightFork)
{
sem_wait(&sem);
int l = philosopher;
int r = (philosopher + 1) % 5;
mtx[l].lock();
mtx[r].lock();
pickLeftFork();
pickRightFork();
eat();
putLeftFork();
putRightFork();
mtx[l].unlock();
mtx[r].unlock();
sem_post(&sem);
}
};

策略二仅当哲学家的左右两支筷子都可用时,才允许他拿起筷子进餐。可以利用AND 型信号量机制实现,也可以利用信号量的保护机制实现。利用信号量的保护机制实现的思想是通过记录型信号量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
25
26
27
28
29
30
31
32
class DiningPhilosophers
{
public:
sem_t sem;
mutex mtx[5];
DiningPhilosophers()
{
sem_init(&sem, 0, 1);
}

void wantsToEat(int philosopher,
function<void()> pickLeftFork,
function<void()> pickRightFork,
function<void()> eat,
function<void()> putLeftFork,
function<void()> putRightFork)
{
sem_wait(&sem);
int l = philosopher;
int r = (philosopher + 1) % 5;
mtx[l].lock();
mtx[r].lock();
sem_post(&sem);
pickLeftFork();
pickRightFork();
eat();
putLeftFork();
putRightFork();
mtx[l].unlock();
mtx[r].unlock();
}
};

策略三:规定奇数号的哲学家先拿起他左边的筷子,然后再去拿他右边的筷子;而偶数号的哲学家则先拿起他右边的筷子,然后再去拿他左边的筷子。按此规定,将是1、2号哲学家竞争1号筷子,3、4号哲学家竞争3号筷子。即五个哲学家都竞争奇数号筷子,获得后,再去竞争偶数号筷子,最后总会有一个哲学家能获得两支筷子而进餐。

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
#include <memory>
#include <iostream>
#include <semaphore.h>
#include <functional>
#include <thread>
#include <condition_variable>
#include <mutex>
#include <future>
#include <vector>
#include <atomic>
using namespace std;
class DiningPhilosophers
{
public:
mutex mtx[5];
DiningPhilosophers()
{
}

void wantsToEat(int philosopher,
function<void()> pickLeftFork,
function<void()> pickRightFork,
function<void()> eat,
function<void()> putLeftFork,
function<void()> putRightFork)
{
int l = philosopher;
int r = (philosopher + 1) % 5;
if (philosopher % 2 == 0)
{
mtx[l].lock();
mtx[r].lock();
pickLeftFork();
pickRightFork();
eat();
putLeftFork();
putRightFork();
mtx[l].unlock();
mtx[r].unlock();
}
else
{
mtx[r].lock();
mtx[l].lock();
pickLeftFork();
pickRightFork();
eat();
putLeftFork();
putRightFork();
mtx[l].unlock();
mtx[r].unlock();
}
}
};
signed main()
{
DiningPhilosophers diningPhilosophers;
int nPhilosophers = 5;
vector<thread> philosophers(nPhilosophers);
for (int i = 0; i < nPhilosophers; i++)
{
philosophers[i] = thread(&DiningPhilosophers::wantsToEat, &diningPhilosophers, i, [i]()
{ cout << "Philosopher " << i << " picks up left fork\n"; }, [i]()
{ cout << "Philosopher " << i << " picks up right fork\n"; }, [i]()
{ cout << "Philosopher " << i << " eats\n"; }, [i]()
{ cout << "Philosopher " << i << " puts down left fork\n"; }, [i]()
{ cout << "Philosopher " << i << " puts down right fork\n"; });
}
for (int i = 0; i < nPhilosophers; i++)
{
philosophers[i].join();
}
return 0;
}

调试

在写并发程序的时候很容易写着写着就有bug了。这时候,如何定位bug就显得尤为关键。

问题代码

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
#include <iostream>           // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable
#include <vector>

// 锁资源1
std::mutex mtx1;
// 锁资源2
std::mutex mtx2;

// 线程A的函数
void taskA()
{
// 保证线程A先获取锁1
std::lock_guard<std::mutex> lockA(mtx1);
std::cout << "线程A获取锁1" << std::endl;

// 线程A睡眠2s再获取锁2,保证锁2先被线程B获取,模拟死锁问题的发生
std::this_thread::sleep_for(std::chrono::seconds(2));

// 线程A先获取锁2
std::lock_guard<std::mutex> lockB(mtx2);
std::cout << "线程A获取锁2" << std::endl;
std::cout << "线程A释放所有锁资源,结束运行!" << std::endl;
}

// 线程B的函数
void taskB()
{
// 线程B先睡眠1s保证线程A先获取锁1
std::this_thread::sleep_for(std::chrono::seconds(1));
std::lock_guard<std::mutex> lockB(mtx2);
std::cout << "线程B获取锁2" << std::endl;
// 线程B尝试获取锁1
std::lock_guard<std::mutex> lockA(mtx1);
std::cout << "线程B获取锁1" << std::endl;
std::cout << "线程B释放所有锁资源,结束运行!" << std::endl;
}
int main()
{
// 创建生产者和消费者线程
std::thread t1(taskA);
std::thread t2(taskB);

// main主线程等待所有子线程执行完
t1.join();
t2.join();

return 0;
}

运行可执行程序

1
2
3
4
 ✘ ⚙ penge@penge-virtual-machine  ~/Desktop/MordenCpp  ./main
线程A获取锁1
线程B获取锁2
...

查看进程号ps -aux | grep main

通过top命令再查看一下进程内每个线程具体的运行情况:top -Hp pid

1
2
3
80342 penge     20   0   88564   1676   1508 S   0.0   0.0   0:00.00 main                                                                                  
80343 penge 20 0 88564 1676 1508 S 0.0 0.0 0:00.00 main
80344 penge 20 0 88564 1676 1508 t 0.0 0.0 0:00.00 main

查看状态为S【Linux进程状态解析 之 R、S、D、T、Z、X (主要有三个状态)

接着使用 gdb attach pid调试一个已经在运行的进程.

查看所有线程的调用堆栈。

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
(gdb) thread apply all bt

Thread 3 (Thread 0x7f0846b3c700 (LWP 1407291)):
#0 __lll_lock_wait (futex=futex@entry=0x5574527f2160 <mtx1>, private=0) at lowlevellock.c:52
#1 0x00007f084768f0a3 in __GI___pthread_mutex_lock (mutex=0x5574527f2160 <mtx1>) at ../nptl/pthread_mutex_lock.c:80
#2 0x00005574527ed74f in __gthread_mutex_lock (__mutex=0x5574527f2160 <mtx1>) at /usr/include/x86_64-linux-gnu/c++/9/bits/gthr-default.h:749
#3 0x00005574527ed8a4 in std::mutex::lock (this=0x5574527f2160 <mtx1>) at /usr/include/c++/9/bits/std_mutex.h:100
#4 0x00005574527ed940 in std::lock_guard<std::mutex>::lock_guard (this=0x7f0846b3bdf0, __m=...) at /usr/include/c++/9/bits/std_mutex.h:159
#5 0x00005574527ed541 in taskB () at main.cpp:36
#6 0x00005574527ee526 in std::__invoke_impl<void, void (*)()> (__f=@0x557453d97008: 0x5574527ed4b1 <taskB()>) at /usr/include/c++/9/bits/invoke.h:60
#7 0x00005574527ee4be in std::__invoke<void (*)()> (__fn=@0x557453d97008: 0x5574527ed4b1 <taskB()>) at /usr/include/c++/9/bits/invoke.h:95
#8 0x00005574527ee450 in std::thread::_Invoker<std::tuple<void (*)()> >::_M_invoke<0ul> (this=0x557453d97008) at /usr/include/c++/9/thread:244
#9 0x00005574527ee40d in std::thread::_Invoker<std::tuple<void (*)()> >::operator() (this=0x557453d97008) at /usr/include/c++/9/thread:251
#10 0x00005574527ee3de in std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (*)()> > >::_M_run (this=0x557453d97000) at /usr/include/c++/9/thread:195
#11 0x00007f0847798df4 in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#12 0x00007f084768c609 in start_thread (arg=<optimized out>) at pthread_create.c:477
#13 0x00007f08475b1353 in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95

Thread 2 (Thread 0x7f084733d700 (LWP 1407290)):
#0 __lll_lock_wait (futex=futex@entry=0x5574527f21a0 <mtx2>, private=0) at lowlevellock.c:52
#1 0x00007f084768f0a3 in __GI___pthread_mutex_lock (mutex=0x5574527f21a0 <mtx2>) at ../nptl/pthread_mutex_lock.c:80
#2 0x00005574527ed74f in __gthread_mutex_lock (__mutex=0x5574527f21a0 <mtx2>) at /usr/include/x86_64-linux-gnu/c++/9/bits/gthr-default.h:749
#3 0x00005574527ed8a4 in std::mutex::lock (this=0x5574527f21a0 <mtx2>) at /usr/include/c++/9/bits/std_mutex.h:100
#4 0x00005574527ed940 in std::lock_guard<std::mutex>::lock_guard (this=0x7f084733cdf0, __m=...) at /usr/include/c++/9/bits/std_mutex.h:159
#5 0x00005574527ed3f8 in taskA () at main.cpp:23
#6 0x00005574527ee526 in std::__invoke_impl<void, void (*)()> (__f=@0x557453d96eb8: 0x5574527ed368 <taskA()>) at /usr/include/c++/9/bits/invoke.h:60
#7 0x00005574527ee4be in std::__invoke<void (*)()> (__fn=@0x557453d96eb8: 0x5574527ed368 <taskA()>) at /usr/include/c++/9/bits/invoke.h:95
#8 0x00005574527ee450 in std::thread::_Invoker<std::tuple<void (*)()> >::_M_invoke<0ul> (this=0x557453d96eb8) at /usr/include/c++/9/thread:244
#9 0x00005574527ee40d in std::thread::_Invoker<std::tuple<void (*)()> >::operator() (this=0x557453d96eb8) at /usr/include/c++/9/thread:251
#10 0x00005574527ee3de in std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (*)()> > >::_M_run (this=0x557453d96eb0) at /usr/include/c++/9/thread:195
#11 0x00007f0847798df4 in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#12 0x00007f084768c609 in start_thread (arg=<optimized out>) at pthread_create.c:477
#13 0x00007f08475b1353 in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95

Thread 1 (Thread 0x7f084733e740 (LWP 1407289)):
#0 __pthread_clockjoin_ex (threadid=139673531045632, thread_return=0x0, clockid=<optimized out>, abstime=<optimized out>, block=<optimized out>) at pthread_join_common.c:145
#1 0x00007f0847799057 in std::thread::join() () from /lib/x86_64-linux-gnu/libstdc++.so.6
#2 0x00005574527ed648 in main () at main.cpp:47

定位到taskA和taskB线程阻塞的原因,都是因为锁获取不到,然后再结合源码进行分析定位,最终发现taskA之所以获取不到mtx2,是因为mtx2早被taskB线程获取了;同样taskB之所以获取不到mtx1,是因为mtx1早被taskA线程获取了,导致所有线程进入阻塞状态,等待锁资源的获取,但是又因为没有线程释放锁,最终导致死锁问题。

知道是死锁问题,那么如何定位具体哪一行呢?

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
(base) sv@sv-NF5280M5:/home/sv$ sudo gdb attach 1406920
[sudo] sv 的密码:
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04.1) 9.2
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
attach: 没有那个文件或目录.
Attaching to process 1406920
[New LWP 1406921]
[New LWP 1406922]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
__pthread_clockjoin_ex (threadid=139933978740480, thread_return=0x0, clockid=<optimized out>, abstime=<optimized out>, block=<optimized out>)
at pthread_join_common.c:145
145 pthread_join_common.c: 没有那个文件或目录.
(gdb) info threads
Id Target Id Frame
* 1 Thread 0x7f44eb185740 (LWP 1406920) "main" __pthread_clockjoin_ex (threadid=139933978740480, thread_return=0x0, clockid=<optimized out>,
abstime=<optimized out>, block=<optimized out>) at pthread_join_common.c:145
2 Thread 0x7f44eb184700 (LWP 1406921) "main" __lll_lock_wait (futex=futex@entry=0x55d52319e1a0 <mtx2>, private=0) at lowlevellock.c:52
3 Thread 0x7f44ea983700 (LWP 1406922) "main" __lll_lock_wait (futex=futex@entry=0x55d52319e160 <mtx1>, private=0) at lowlevellock.c:52
(gdb) thread 2
[Switching to thread 2 (Thread 0x7f44eb184700 (LWP 1406921))]
#0 __lll_lock_wait (futex=futex@entry=0x55d52319e1a0 <mtx2>, private=0) at lowlevellock.c:52
52 lowlevellock.c: 没有那个文件或目录.
(gdb) bt # 函数调用栈
#0 __lll_lock_wait (futex=futex@entry=0x55d52319e1a0 <mtx2>, private=0) at lowlevellock.c:52
#1 0x00007f44eb4d60a3 in __GI___pthread_mutex_lock (mutex=0x55d52319e1a0 <mtx2>) at ../nptl/pthread_mutex_lock.c:80
#2 0x000055d52319974f in __gthread_mutex_lock (__mutex=0x55d52319e1a0 <mtx2>) at /usr/include/x86_64-linux-gnu/c++/9/bits/gthr-default.h:749
#3 0x000055d5231998a4 in std::mutex::lock (this=0x55d52319e1a0 <mtx2>) at /usr/include/c++/9/bits/std_mutex.h:100
#4 0x000055d523199940 in std::lock_guard<std::mutex>::lock_guard (this=0x7f44eb183df0, __m=...) at /usr/include/c++/9/bits/std_mutex.h:159
#5 0x000055d5231993f8 in taskA () at main.cpp:23
#6 0x000055d52319a526 in std::__invoke_impl<void, void (*)()> (__f=@0x55d523aeeeb8: 0x55d523199368 <taskA()>) at /usr/include/c++/9/bits/invoke.h:60
#7 0x000055d52319a4be in std::__invoke<void (*)()> (__fn=@0x55d523aeeeb8: 0x55d523199368 <taskA()>) at /usr/include/c++/9/bits/invoke.h:95
#8 0x000055d52319a450 in std::thread::_Invoker<std::tuple<void (*)()> >::_M_invoke<0ul> (this=0x55d523aeeeb8) at /usr/include/c++/9/thread:244
#9 0x000055d52319a40d in std::thread::_Invoker<std::tuple<void (*)()> >::operator() (this=0x55d523aeeeb8) at /usr/include/c++/9/thread:251
#10 0x000055d52319a3de in std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (*)()> > >::_M_run (this=0x55d523aeeeb0)
at /usr/include/c++/9/thread:195
#11 0x00007f44eb5dfdf4 in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#12 0x00007f44eb4d3609 in start_thread (arg=<optimized out>) at pthread_create.c:477
#13 0x00007f44eb3f8353 in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95
(gdb) f 5 # 线程2的第5帧信息
#5 0x000055d5231993f8 in taskA () at main.cpp:23
23 std::lock_guard<std::mutex> lockB(mtx2)

main.cpp:23即可知道问题行所在定位。