严正声明:本文系作者davidhopper原创,未经许可,不得转载。
下述问题来源于按序打印:
一、问题描述
提供一个类(原题描述以Java语言提供,本文使用C++实现)
public class Foo {
public void one() { print("one"); }
public void two() { print("two"); }
public void three() { print("three"); }
}
三个不同的线程将会共用一个Foo
实例。
线程A
将会调用one()
方法
线程B
将会调用two()
方法
线程C
将会调用three()
方法
请设计修改程序,以确保two()
方法在one()
方法之后被执行,three()
方法在 two()
方法之后被执行,亦即,无论操作系统内部按照什么样的顺序调用线程A
、B
、C
,执行的顺序永远是one()
->two()
->three()
,输出的结果永远是onetwothree
。
示例 1:
输入: [1,2,3]
输出: "onetwothree"
解释:
有三个线程会被异步启动。
输入 [1,2,3] 表示线程 A 将会调用 one() 方法,线程 B 将会调用 two() 方法,线程 C 将会调用 three() 方法。
正确的输出是 "onetwothree"。
示例 2:
输入: [1,3,2]
输出: "onetwothree"
解释:
输入 [1,3,2] 表示线程 A 将会调用 one() 方法,线程 B 将会调用 three() 方法,线程 C 将会调用 two() 方法。
正确的输出是 "onetwothree"。
注意:
尽管输入中的数字似乎暗示了顺序,但是我们并不保证线程在操作系统中的调度顺序。你看到的输入格式主要是为了确保测试的全面性。
二、问题分析
一般的解决方案是利用std::mutex
锁定临界区以指定线程的执行顺序,该思路没有问题,但一不小心就容易造成死锁。本文使用C++ 11中自带的std::promise
来实现,逻辑更为简洁。首先看std::promise
的介绍:
std::promise
是一个模板类template<typename R> class promise
,其泛型参数R
为std::promise
对象保存的值的类型,R
可以是void
类型。std::promise
保存的值可被与之关联的std::future
对象读取,读取操作可以发生在其它线程。std::promise
允许move
语义(右值构造,右值赋值),但不允许拷贝(拷贝构造、赋值),std::future
亦然。std::promise
和std::future
合作共同实现了多线程间通信。
2.1 线程随机调度的程序
下面给出线程随机调度的程序:
#include <functional>
#include <iostream>
#include <thread>
void printFirst() { std::cout << "first"; }
void printSecond() { std::cout << "second"; }
void printThird() { std::cout << "third"; }
class Foo {
public:
void first(const std::function<void()>& printFirst) {
printFirst();
}
void second(const std::function<void()>& printSecond) {
printSecond();
}
void third(const std::function<void()>& printThird) {
printThird();
}
};
void invokeFooFirst(Foo& foo) { foo.first(printFirst); }
void invokeFooSecond(Foo& foo) { foo.second(printSecond); }
void invokeFooThird(Foo& foo) { foo.third(printThird); }
int main() {
Foo foo;
std::thread t2(invokeFooSecond, std::ref(foo));
std::thread t1(invokeFooFirst, std::ref(foo));
std::thread t3(invokeFooThird, std::ref(foo));
t3.join();
t1.join();
t2.join();
std::cout << std::endl;
return 0;
}
在Linux系统中的编译指令为(注意使用C++11以上版本):
g++ -g thread_order.cpp -o thread_order -lpthread
在我的机器上执行结果为:
secondfirstthird
2.2 线程按序调度的程序
下面给出线程按序调度的程序:
不管三个线程如何被操作系统调度,永远是先执行first()
并设置second()
触发的信号;second()
一直挂起直到等到first()
设置的触发信号,third()
一直挂起直到等到second()
设置的触发信号。通过上述简单的信号传递机制,确保了first()
、second()
、third()
的按序执行。
#include <functional>
#include <future>
#include <iostream>
#include <thread>
void printFirst() { std::cout << "first"; }
void printSecond() { std::cout << "second"; }
void printThird() { std::cout << "third"; }
class Foo {
public:
void first(const std::function<void()>& printFirst) {
printFirst();
p1_.set_value();
}
void second(const std::function<void()>& printSecond) {
p1_.get_future().wait();
printSecond();
p2_.set_value();
}
void third(const std::function<void()>& printThird) {
p2_.get_future().wait();
printThird();
}
private:
std::promise<void> p1_;
std::promise<void> p2_;
};
void invokeFooFirst(Foo& foo) { foo.first(printFirst); }
void invokeFooSecond(Foo& foo) { foo.second(printSecond); }
void invokeFooThird(Foo& foo) { foo.third(printThird); }
int main() {
Foo foo;
std::thread t2(invokeFooSecond, std::ref(foo));
std::thread t1(invokeFooFirst, std::ref(foo));
std::thread t3(invokeFooThird, std::ref(foo));
t3.join();
t1.join();
t2.join();
std::cout << std::endl;
return 0;
}