等待一个事件或其他条件
第一,它可以持续的检查共享数据标志(用于做保护工作的互斥量),直到另一线程完成工作时对这个标志进行重设。
第二个选择是在等待线程在检查间隙,使用 std::this_thread::sleep_for() 进行周期性的间歇:
#include <iostream>
#include <thread>
#include <mutex>
class wait_test {
bool flag;
std::mutex m;
public:
wait_test(bool _flag):flag(_flag){}
void setFlag(bool _flag)
{
std::unique_lock<std::mutex> lk(m);
flag = _flag;
}
bool getFlag()
{
std::unique_lock<std::mutex> lk(m);
return flag;
}
void wait_for_flag()
{
std::unique_lock<std::mutex> lk(m);
while (!flag)
{
lk.unlock(); // 1 解锁互斥量
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 2 休眠100ms
lk.lock(); // 3 再锁互斥量
}
}
};
void funA(wait_test &wt, int &i)
{
while (!wt.getFlag())
{
++i;
}
}
void funB(wait_test &wt, int &i)
{
std::cout << "begin\t" << i << std::endl;
wt.wait_for_flag();//等待主线程set
std::cout << "end\t" << i << std::endl;
}
int main()
{
wait_test wt{ false };
int i{ 0 };
std::thread t1{ funA,std::ref(wt),std::ref(i) }, t2{ funB,std::ref(wt),std::ref(i) };
t1.detach();
t2.detach();
wt.setFlag(true);
system("pause");
return 0;
}
第三个选择(也是优先的选择)是,使用C++标准库提供的工具去等待事件的发生。通过另一线程触发等待事件的机制是最基本的唤醒方式(例如:流水线上存在额外的任务时),这种机制就称为“
条件变量”(condition variable)。从概念上来说,
一个条件变量会与多个事件或其他条件相关,并且一个或多个线程会等待条件的达成。当某些线程被终止时,为了唤醒等待线程(允许等待线程继续执行)终止的线程将会向等待着的线程广播“条件达成”的信息。
等待条件达成
C++标准库对条件变量有两套实现: std::condition_variable 和 std::condition_variable_any 。这两个实现都包含在 <condition_variable> 头文件的声明中。两者都需要与一个互斥量一起才能工作(互斥量是为了同步);前者仅限于与 std::mutex 一起工作,而后者可以和任何满足最低标准的互斥量一起工作,从而加上了_any的后缀。因为 std::condition_variable_any 更加通用,这就可能从体积、性能,以及系统资源的使用方面产生额外的开销,所以 std::condition_variable 一般作为首选的类型,当对灵活性有硬性要求时,我们才会去考虑 std::condition_variable_any 。
使用 std::condition_variable 处理数据等待
#include <iostream>
#include <thread>
#include <mutex>
#include <queue>
struct data_chunk {
int m;
};
struct A {
std::mutex mut;
std::queue<data_chunk> data_queue; // 1
std::condition_variable data_cond;
bool more_data_to_prepare()
{
return data_queue.size() < 10;//可以加大这里的数据,达到观察效果,如500
}
bool is_last_chunk()
{
return data_queue.size() == 3;//对应450
}
};
int i = 0;
data_chunk prepare_data()
{
data_chunk r;
r.m = ++i;
return r;
}
void data_preparation_thread(A &a)
{
std::cout << "preparation begin"<< std::endl;
while (a.more_data_to_prepare())
{
const data_chunk data = prepare_data();
std::lock_guard<std::mutex> lk(a.mut);
a.data_queue.push(data); // 2
std::cout << "preparation notify" << std::endl;
a.data_cond.notify_one(); // 3
}
std::cout << "preparation end" << std::endl;
}
void process(const data_chunk &d)
{
std::cout << d.m << std::endl;
}
void data_processing_thread(A &a)
{
while (true)
{
std::unique_lock<std::mutex> lk(a.mut); // 4
a.data_cond.wait(
lk, [&a] {return !a.data_queue.empty();}); // 5
//std::cout << "process wait end" << std::endl;
data_chunk data = a.data_queue.front();
a.data_queue.pop();
lk.unlock(); // 6
process(data);
if (a.is_last_chunk())
break;
}
}
int main()
{
A a;
std::thread t1{ data_preparation_thread,std::ref(a) },
t2{ data_processing_thread,std::ref(a) };
t1.join();
t2.join();
system("pause");
return 0;
}
当等待线程重新获取互斥量并检查条件时,如果它并非直接响应另一个线程的通知,这就是所谓的“伪唤醒”(spurious wakeup)。因为任何伪唤醒的数量和频率都是不确定的,这里不建议使用一个有副作用的函数做条件检查。当你这样做了,就必须做好多次产生副作用的心理准备。
用 std::unique_lock 而不使用 std::lock_guard ——等待中的线程必须在等待期间解锁互斥量,并在这这之后对互斥量再次上锁,而 std::lock_guard 没有这么灵活。
解锁 std::unique_lock 的灵活性,不仅适用于对wait()的调用;它还可以用于有待处理但还未处理的数据。
使用条件变量构建线程安全队列
使用条件变量的线程安全队列(完整版),并且融入上一段程序:
#include <queue>
#include <memory>
#include <mutex>
#include <condition_variable>//头文件
template<typename T>
class threadsafe_queue
{
private:
mutable std::mutex mut; // 1 互斥量必须是可变的
std::queue<T> data_queue;
std::condition_variable data_cond;
public:
threadsafe_queue()
{}
threadsafe_queue(threadsafe_queue const& other)
{
std::lock_guard<std::mutex> lk(other.mut);
data_queue = other.data_queue;
}
void push(T new_value)
{
std::lock_guard<std::mutex> lk(mut);
data_queue.push(new_value);
data_cond.notify_one();
}
void wait_and_pop(T& value)
{
std::unique_lock<std::mutex> lk(mut);
data_cond.wait(lk, [this] {return !data_queue.empty();});
value = data_queue.front();
data_queue.pop();
}
std::shared_ptr<T> wait_and_pop()
{
std::unique_lock<std::mutex> lk(mut);
data_cond.wait(lk, [this] {return !data_queue.empty();});
std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));
data_queue.pop();
return res;
}
bool try_pop(T& value)
{
std::lock_guard<std::mutex> lk(mut);
if (data_queue.empty())
return false;
value = data_queue.front();
data_queue.pop();
return true;
}
std::shared_ptr<T> try_pop()
{
std::lock_guard<std::mutex> lk(mut);
if (data_queue.empty())
return std::shared_ptr<T>();
std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));
data_queue.pop();
return res;
}
bool empty() const
{
std::lock_guard<std::mutex> lk(mut);
return data_queue.empty();
}
};
threadsafe_queue<data_chunk> data_queue; // 1
void data_preparation_thread()
{
while (more_data_to_prepare())
{
data_chunk const data = prepare_data();
data_queue.push(data); // 2
}
}
void data_processing_thread()
{
while (true)
{
data_chunk data;
data_queue.wait_and_pop(data); // 3
process(data);
if (is_last_chunk(data))
break;
}
}
使用期望等待一次性事件
C++标准库模型将这种一次性事件称为“期望” (future)。
当事件发生时(并且期望状态为就绪),这个“期望”就不能被重置。
在C++标准库中,有两种“期望”,使用两种类型模板实现,声明在头文件中: 唯一期望(uniquefutures)( std::future<> )和共享期望(shared futures)( std::shared_future<> )。后者的实现中,所有实例会在同时变为就绪状态,并且他们可以访问与事件相关的任何数据。这种数据关联与模板有关,比如 std::unique_ptr 和 std::shared_ptr 的模板参数就是相关联的数据类型。在与数据无关的
地方,可以使用 std::future<void> 与 std::shared_future<void> 的特化模板。
带返回值的后台任务
当任务的结果你不着急要时,你可以使用 std::async 启动一个异步任务。与 std::thread 对象等待运行方式的不同, std::async 会返回一个 std::future 对象,这个对象持有最终计算出来的结果。当你需要这个值时,你只需要调用这个对象的get()成员函数;并且直到“期望”状态为就绪的情况下,线程才会阻塞;之后,返回计算结果。
使用 std::future 从异步任务中获取返回值:
#include <future>
#include <iostream>
int find_the_answer_to_ltuae()
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
return 10;
}
void do_other_stuff()
{
std::this_thread::sleep_for(std::chrono::milliseconds(120));
}
int main()
{
std::future<int> the_answer = std::async(find_the_answer_to_ltuae);
do_other_stuff();
std::cout << "The answer is " << the_answer.get() << std::endl;
system("pause");
return 0;
}
std::async 允许你通过添加额外的调用参数,向函数传递额外的参数。当第一个参数是一个指向成员函数的指针,第二个参数提供有这个函数成员类的具体对象(不是直接的,就是通过指针,还可以包装在 std::ref 中),剩余的参数可作为成员函数的参数传入。否则,第二个和随后的参数将作为函数的参数,或作为指定可调用对象的第一个参数。
使用 std::async 向函数传递参数
#include <string>
#include <future>
#include <iostream>
struct X
{
int m;
void foo(int i, std::string const& s)
{
std::cout << s << "\t" << i << std::endl;
}
std::string bar(std::string const &s)
{
return "bar("+s+")";
}
};
struct Y
{
double operator()(double d)
{
return d + 1.1;
}
};
X baz(X& _x)
{
++_x.m;
return _x;
}
class move_only
{
public:
move_only() = default;
move_only(move_only&&) = default;
move_only(move_only const&) = delete;
move_only& operator=(move_only&&) = default;
move_only& operator=(move_only const&) = delete;
void operator()()
{
std::cout << "move_only()" << std::endl;
}
};
void fun()
{
X x;
auto f1 = std::async(&X::foo, &x, 42, "hello"); // 调用p->foo(42, "hello"),p是指向x的指针,指针
auto f2 = std::async(&X::bar, x, "goodbye"); // 调用tmpx.bar("goodbye"), tmpx是x的拷贝副本,具体对象
std::cout << f2.get() << std::endl;
Y y;
auto f3 = std::async(Y(), 3.141); // 调用tmpy(3.141),tmpy通过Y的移动构造函数得到
std::cout << f3.get() << std::endl;
auto f4 = std::async(std::ref(y), 2.718); // 调用y(2.718)
std::cout << f4.get() << std::endl;
x.m = 1;
auto f5 = std::async(baz, std::ref(x)); // 调用baz(x)
std::cout << f5.get().m << std::endl;
auto f6 = std::async(move_only()); // 调用tmp(),tmp是通过std::move(move_only())构造得到
}
int main()
{
fun();
system("pause");
return 0;
}
在默认情况下,这取决于 std::async 是否启动一个线程,或是否在期望等待时同步任务。在大多数情况下(估计这就是你想要的结果),但是你也可以在函数调用之前,向 std::async 传递一个额外参数。这个参数的类型是 std::launch ,还可以是std::launch::defered ,用来表明函数调用被延迟到wait()或get()函数调用时才执行, std::launch::async 表明函数必须在其所在的独立线程上执行, std::launch::deferred | std::launch::async 表明实现可以选择这两种方式的一种。最后一个选项是默认的。当函数调用被延迟,它可能不会在运行了。
baz函数更改,方便观察效果:
X baz(X& _x,int i)
{
_x.m=i;
std::cout << _x.m<<"调用" << std::endl;
return _x;
}
调用:
auto f7 = std::async(std::launch::async, Y(), 1.2); // 在新线程上执行
std::cout <<"f7\t"<< f7.get() << std::endl;
auto f8 = std::async(std::launch::deferred, baz, std::ref(x),2); // 在wait()或get()调用时执行
auto f9 = std::async(
std::launch::deferred | std::launch::async,
baz, std::ref(x),3); // 实现选择执行方式
std::cout << "f9\t"<<f9.get().m << std::endl;
auto f10 = std::async(baz, std::ref(x),4);
f8.wait(); // 调用延迟函数,后台运行,此时如果有结果就前行,否则阻塞
std::cout << "f8\t" << f8.get().m << std::endl;
std::cout <<"f10\t"<< f10.get().m << std::endl;
这不是让一个 std::future 与一个任务实例相关联的唯一方式;你也可以将任务包装入一个 std::packaged_task<> 实例中,或通过编写代码的方式,使用 std::promise<> 类型模板显示设置值。与 std::promise<> 对比, std::packaged_task<> 具有更高层的抽象,所以我们从“高抽象”的模板说起。
任务与期望
std::packaged_task<> 对一个函数或可调用对象,绑定一个期望。当 std::packaged_task<>对象被调用,它就会调用相关函数或可调用对象,将期望状态置为就绪,返回值也会被存储为相关数据。
std::packaged_task<> 的模板参数是一个函数签名,当你构造出一个 std::packaged_task<> 实例时,你必须传入一个函数或可调用对象,这个函数或可调用的对象需要能接收指定的参数和返回可转换为指定返回类型的值。类型可以不完全匹配;你可以用一个int类型的参数和返回一个float类型的函数,来构建 std::packaged_task<double(double)> 的实例,因为在这里,类型可以隐
式转换。
指定函数签名的返回类型可以用来标识,从get_future()返回的 std::future<> 的类型,不过函数签名的参数列表,可用来指定“打包任务”的函数调用操作符。
std::packaged_task<> 的特化——局部类定义:
template<>
class std::packaged_task<std::string(std::vector<char>*, int)>
{
public:
template<typename Callable>
explicit std::packaged_task(Callable&& f);
std::future<std::string> get_future();
void operator()(std::vector<char>*, int);
};
这里的 std::packaged_task 对象是一个可调用对象,并且它可以包含在一个 std::function 对象中,传递到 std::thread 对象中,就可作为线程函数;传递另一个函数中,就作为可调用对象,或可以直接进行调用。当 std::packaged_task 作为一个函数调用时,可为函数调用操作符提供所需的参数,并且返回值作为异步结果存储在 std::future ,可通过get_future()获取。
你可以把一个任务包含入 std::packaged_task ,并且在检索期望之前,需要将 std::packaged_task 对象传入,以便调用时能及时的找到。
当你需要异步任务的返回值时,你可以等待期望的状态变为“就绪”。下面的代码就是这么个情况。
线程间传递任务(使用 std::packaged_task 执行一个图形界面线程):
#include <string>
#include <iostream>
#include <deque>
#include <mutex>
#include <future>
#include <thread>
//#include <utility>
typedef void (Fun)();
struct data {
int id;
};
struct pack_test {
std::mutex m;
std::deque<std::packaged_task<void()> > tasks;
};
bool gui_shutdown_message_received(data &d)
{
return d.id> 10;
}
void get_and_process_gui_message(data &d)
{
++d.id;
std::cout << d.id << std::endl;
}
void gui_thread(pack_test &pt, data &d) // 1
{
while (!gui_shutdown_message_received(d)) // 2
{
get_and_process_gui_message(d); // 3
std::packaged_task<void()> task;
std::lock_guard<std::mutex> lk(pt.m);
if (pt.tasks.empty()) // 4
continue;
task = std::move(pt.tasks.front()); // 5
pt.tasks.pop_front();
std::cout << "task" << std::endl;
task(); // 6,执行任务
}
}
void fun()
{
std::cout << "fun()"<<std::endl;
}
template<typename Func>
std::future<void> post_task_for_gui_thread(Func f, pack_test &pt)//这个函数返回用来干什么?
{
std::packaged_task<void()> task(f); // 7
std::future<void> res = task.get_future(); // 8
std::lock_guard<std::mutex> lk(pt.m); // 9
pt.tasks.push_back(std::move(task)); // 10
return res;
}
int main()
{
data d;
d.id = 0;
pack_test pt;
std::thread post_t{ post_task_for_gui_thread<Fun>,fun,std::ref(pt) }, gui_bg_thread{ gui_thread,std::ref(pt),std::ref(d) };
post_t.detach();
gui_bg_thread.detach();
system("pause");
return 0;
}
std::packaged_task用法示例:点击打开链接
#include <iostream>
#include <cmath>
#include <thread>
#include <future>//std::packaged_task
#include <functional>//std::bind
// unique function to avoid disambiguating the std::pow overload set
int f(int x, int y) { return std::pow(x, y); }
void task_lambda()
{
std::packaged_task<int(int, int)> task(
[](int a, int b) {
return std::pow(a, b);
});
std::future<int> result = task.get_future();
task(2, 9);
std::cout << "task_lambda:\t" << result.get() << '\n';
}
void task_bind()
{
std::packaged_task<int()> task(std::bind(f, 2, 11));
std::future<int> result = task.get_future();
task();
std::cout << "task_bind:\t" << result.get() << '\n';
}
void task_thread()
{
std::packaged_task<int(int, int)> task(f);
std::future<int> result = task.get_future();
std::thread task_td(std::move(task), 2, 10);
task_td.join();
std::cout << "task_thread:\t" << result.get() << '\n';
}
int main()
{
task_lambda();
task_bind();
task_thread();
system("pause");
return 0;
}
这些任务能作为一个简单的函数调用来表达吗?还有,这些任务的结果能从很多地方得到吗?这些情况可以使用第三种方法创建“期望”来解决:使用 std::promise 对值进行显示设置。
使用std::promises
在不同的应用程序中,存在着大量的网络连接,因此不同应用都会拥有一定数量的线程(可能只有一个)来处理网络连接,每个线程处理可同时处理多个连接事件。
一对 std::promise/std::future 会为这种方式提供一个可行的机制;在期望上可以阻塞等待线程,同时,提供数据的线程可以使用组合中的“承诺”来对相关值进行设置,以及将“期望”的状态置为“就绪”。
可以通过get_future()成员函数来获取与一个给定的 std::promise 相关的 std::future 对象,就像是与 std::packaged_task 相关。当“承诺”的值已经设置完毕(使用set_value()成员函数),对应“期望”的状态变为“就绪”,并且可用于检索已存储的值。当你在设置值之前销毁 std::promise ,将会存储一个异常。
使用“承诺”解决单线程多连接问题:
#include <future>
void process_connections(connection_set& connections)
{
while (!done(connections)) // 1
{
for (connection_iterator // 2,依次的检查每一个连接
connection = connections.begin(), end = connections.end();
connection != end;
++connection)
{
if (connection->has_incoming_data()) // 3,检索是否有数据
{
data_packet data = connection->incoming();
std::promise<payload_type>& p =
connection->get_promise(data.id); // 4,一个ID映射到一个 std::promise (可能是在相关容器中进行的依次查找)
p.set_value(data.payload);//值设置在包的有效负载中?
}
if (connection->has_outgoing_data()) // 5,正在发送已入队的传出数据
{
outgoing_packet data =
connection->top_of_outgoing_queue();
connection->send(data.payload);
data.promise.set_value(true); // 6,当发送完成,与传出数据相关的“承诺”将置为true,来表明传输成功
}
}
}
}
std::promise使用方法:点击打开链接
#include <vector>
#include <thread>
#include <future>
#include <numeric>
#include <iostream>
//#include <chrono>
void accumulate(std::vector<int>::iterator first,
std::vector<int>::iterator last,
std::promise<int> accumulate_promise)
{
int sum = std::accumulate(first, last, 0);
accumulate_promise.set_value(sum); // Notify future
}
void do_work(std::promise<void> barrier)
{
std::this_thread::sleep_for(std::chrono::seconds(1));
barrier.set_value();
}
int main()
{
// Demonstrate using promise<int> to transmit a result between threads.
std::vector<int> numbers = { 1, 2, 3, 4, 5, 6 };
std::promise<int> accumulate_promise;
std::future<int> accumulate_future = accumulate_promise.get_future();
std::thread work_thread(accumulate, numbers.begin(), numbers.end(),
std::move(accumulate_promise));
accumulate_future.wait(); // wait for result
std::cout << "result=" << accumulate_future.get() << '\n';
work_thread.join(); // wait for thread completion
// Demonstrate using promise<void> to signal state between threads.
std::promise<void> barrier;
std::future<void> barrier_future = barrier.get_future();
std::thread new_work_thread(do_work, std::move(barrier));
barrier_future.wait();
new_work_thread.join();
return 0;
}
为“期望”存储“异常”
#include <stdexcept>//定义了常用异常类
#include <cmath><span style="font-size:14px;"><strong>
</strong></span>#include <thread>
#include <future>
#include <iostream>
double square_root(double x)
{
if (x<0)
{
throw std::out_of_range("x<0");
}
return sqrt(x);
}
void fun(double x, std::promise<double> some_promise)
{
try
{
some_promise.set_value(square_root(x));
}
catch (...)
{
/*copy_exception was misleading ,
*the function was renamed to std::make_exception_ptr
*for the final C++11 standard.
*/
//当异常类型是已知的,它就应该优先被使用
some_promise.set_exception(std::make_exception_ptr(std::logic_error("foo ")));
//some_promise.set_exception(std::current_exception());//catch now exception
}
}
int main()
{
//std::thread t1{ square_root,-1 };
//t1.join();
//std::future<double> f = std::async(square_root, -1);
//double y = f.get();//此时得到异常
//std::packaged_task<double(double)> task(square_root);
//std::future<double> result= task.get_future();
//std::thread task_td(std::move(task), -1);
//task_td.join();
//auto r = result.get();//此时得到异常,std::out_of_range
std::promise<double> some_promise;
std::future<double> future = some_promise.get_future();
std::thread promise_td(fun,-1, std::move(some_promise));
future.wait();
auto rOfFuture = future.get();//此时得到异常,std::logic_error
std::cout << rOfFuture << std::endl;
system("pause");
return 0;
}
另一种向“期望”中存储异常的方式是,在没有调用“承诺”上的任何设置函数前,或正在调用包装好的任务时,销毁与 std::promise 或 std::packaged_task 相关的“期望”对象。在这任何情况下,当“期望”的状态还不是“就绪”时,调用 std::promise 或 std::packaged_task 的析构函数,将会存储一个与 std::future_errc::broken_promise 错误状态相关的 std::future_error 异常;通过创建一个“期望”,你可以构造一个“承诺”为其提供值或异常;你可以通过销毁值和异常源,去违背“承诺”。在这种情况下,编译器没有在“期望”中存储任何东西,等待线程可能会永远的等下去。
关于copy_exception:点击打开链接
task.~packaged_task();
some_promise.~promise();
更改上述程序:
std::promise<double> some_promise;
std::future<double> future = some_promise.get_future();
some_promise.~promise();
std::thread promise_td(fun,-1, std::move(some_promise));
future.wait();
auto rOfFuture = future.get();//此时得到异常,std::future_error
std::cout << rOfFuture << std::endl;
std::future 也有局限性,在很多线程在等待的时候,只有一个线程能获取等待结果。当多个线程需要等待相同的事件的结果,你就需要使用 std::shared_future 来替代 std::future 了。
多个线程的等待
因为 std::future 是只移动的,所以其所有权可以在不同的实例中互相传递,但是只有一个实例可以获得特定的同步结果;而std::shared_future 实例是可拷贝的,所以多个对象可以引用同一关联“期望”的结果。
在每一个 std::shared_future 的独立对象上成员函数调用返回的结果还是不同步的,所以为了在多个线程访问一个独立对象时,避免数据竞争,必须使用锁来对访问进行保护。优先使用的办法:为了替代只有一个拷贝对象的情况,可以让每个线程都拥有自己对应的拷贝对象。这样,当每个线程都通过自己拥有的 std::shared_future 对象获取结果,那么多个线程访问共享同步结果就是安全的。
std::shared_future 的实例同步 std::future 实例的状态。当 std::future 对象没有与其他对象共享同步状态所有权,那么所有权必须使用 std::move 将所有权传递到 std::shared_future ,其默认构造函数如下:
std::promise<int> p;
std::future<int> f(p.get_future());
assert(f.valid()); // 1 "期望" f 是合法的
std::shared_future<int> sf(std::move(f));
assert(!f.valid()); // 2 "期望" f 现在是不合法的
assert(sf.valid()); // 3 sf 现在是合法的
如其他可移动对象一样,转移所有权是对右值的隐式操作,所以你可以通过 std::promise 对象的成员函数get_future()的返回值,直接构造一个 std::shared_future 对象,例如1:
std::promise<std::string> p;
std::shared_future<std::string> sf(p.get_future()); // 1 隐式转移所有权
std::promise< std::map< SomeIndexType, SomeDataType, SomeComparator,
SomeAllocator>::iterator> p;//2
auto sf = p.get_future().share();
2:std::future 有一个share()成员函数,可用来创建新的 std::shared_future ,并且可以直接转移“期望”的所有权。类型开以很复杂,也可以方便修改。
限定等待时间
介绍两种可能是你希望指定的超时方式:一种是“时延”的超时方式,另一种是“绝对”超时方式。第一种方式,需要指定一段时间(例如,30毫秒);第二种方式,就是指定一个时间点(例如,协调世界时[UTC]17:30:15.045987023,2011年11月30日)。多数等待函数提供变量,对两种超时方式进行处理。处理持续时间的变量以“_for”作为后缀,处理绝对时间的变量以"_until"作为后缀。
所以,当 std::condition_variable 的两个成员函数wait_for()和wait_until()成员函数分别有两个负载,这两个负载都与wait()成员函数的负载相关——其中一个负载只是等待信号触发,或时间超期,亦或是一个虚假的唤醒,并且醒来时,会检查锁提供的谓词,并且只有在检查为true时才会返回(这时条件变量的条件达成),或直接而超时。
时钟
时钟就是时间信息源。特别是,时钟是一个类,提供了四种不同的信息:现在时间、时间类型、时钟节拍、通过时钟节拍的分布,判断时钟是否稳定。
时钟的当前时间可以通过调用静态成员函数now()从时钟类中获取;例如, std::chrono::system_clock::now() 是将返回系统时钟的当前时间。
特定的时间点类型可以通过time_point的数据typedef成员来指定,所以some_clock::now()的类型就是some_clock::time_point。
时钟节拍被指定为1/x(x在不同硬件上有不同的值)秒,这是由时间周期所决定——一个时钟一秒有25个节拍,因此一个周期为 std::ratio<1, 25> ,当一个时钟的时钟节拍每2.5秒一次,周期就可以表示为 std::ratio<5, 2> 。
当时钟节拍均匀分布(无论是否与周期匹配),并且不可调整,这种时钟就称为稳定时钟。当is_steady静态数据成员为true时,表明这个时钟就是稳定的,否则,就是不稳定的。
C++标准库提供一个稳定时钟 std::chrono::steady_clock 。C++标准库提供的其他时钟可表示为 std::chrono::system_clock (在上面已经提到过),它代表了系统时钟的“实际时间”,并且提供了函数可将时间点转化为time_t类型的值; std::chrono::high_resolution_clock 可能是标准库中提供的具有最小节拍周期(因此具有最高的精度[分辨率])的时钟。
时延
时延是时间部分最简单的; std::chrono::duration<> 函数模板能够对时延进行处理(线程库使用到的所有C++时间处理工具,都在 std::chrono 命名空间内)。
std::chrono::milliseconds ms(54802);
std::chrono::seconds s =
std::chrono::duration_cast<std::chrono::seconds>(ms);//54s,截断
延迟支持计算,所以你能够对两个时延变量进行加减,或者是对一个时延变量乘除一个常数(模板的第一个参数)来获得一个新延迟变量。例如,5*seconds(1)与seconds(5)或minutes(1)-seconds(55)一样。
你等待一个“期望”状态变为就绪已经35毫秒:
std::future<int> f = std::async(some_task);
if (f.wait_for(std::chrono::milliseconds(35)) == std::future_status::ready)
do_something_with(f.get());
你可以等待一个“期望”,所以当函数等待超时时,会返回 std::future_status::timeout ;当“期望”状态改变,函数会返回 std::future_status::ready ;当“期望”的任务延迟了,函数会返回 std::future_status::deferred 。基于时延的等待是使用内部库提供的稳定时钟,来进行计时的;所以,即使系统时钟在等待时被调整(向前或向后),35毫秒的时延在这里意味着,的确耗时35毫秒。
时间点
时钟的时间点可以用 std::chrono::time_point<> 的类型模板实例来表示,实例的第一个参数用来指定所要使用的时钟,第二个函数参数用来表示时间的计量单位(特化的 std::chrono::duration<> )。
代码块的计时:
#include <thread>
#include <iostream>
#include <chrono>
void do_something()
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));//100ms
}
int main()
{
auto start = std::chrono::high_resolution_clock::now();//获取现在时间
do_something();
auto stop = std::chrono::high_resolution_clock::now();
std::cout << "do_something() took "
<< std::chrono::duration_cast<std::chrono::milliseconds>(stop - start).count()
<< " seconds"<< std::endl;//100ms
system("pause");
return 0;
}
等待一个条件变量——有超时功能:
#include <condition_variable>
#include <mutex>
#include <chrono>
std::condition_variable cv;
bool done;
std::mutex m;
bool wait_loop()
{
auto const timeout = std::chrono::steady_clock::now() +
std::chrono::milliseconds(500);
std::unique_lock<std::mutex> lk(m);
while (!done)
{
if (cv.wait_until(lk, timeout) == std::cv_status::timeout)
break;
}
return done;
}
具有超时功能的函数
休眠只是超时处理的一种形式;你已经看到了,超时可以配合条件变量和“期望”一起使用。超时甚至可以在尝试获取一个互斥锁时(当互斥量支持超时时)使用。 std::mutex 和 std::recursive_mutex 都不支持超时锁,但是 std::timed_mutex 和 std::recursive_timed_mutex 支持。这两种类型也有try_lock_for()和try_lock_until()成员函数,可以在一段时期内尝试,或在指定时间点前获取互斥锁。表4.1(译文P98)展示了C++标准库中支持超时的函数。参数列表为“延时”(duration)必须是 std::duration<> 的实例,并且列出为“时间点”(time_point)必须是 std::time_point<> 的实例。
使用同步操作简化代码
同步工具的使用在本章称为构建块。你可以关注那些需要同步的操作,而非具体使用的机制。当需要为程序的并发时,这是一种可以帮助你简化你的代码的方式,提供更多的函数化(是函数化编程的意思(functional programming))方法。比起在多个线程间直接共享数据,每个任务拥有自己的数据会应该会更好,并且结果可以对其他线程进行广播,这就需要使用“期望”来完成了。
使用“期望”的函数化编程
“函数化编程”(functional programming ( FP )):函数结果只依赖于传入函数的参数,并不依赖外部状态。
C++是一个多范型的语言,其也可以写出FP类型的程序。在C++11中这种方式要比C++98简单许多,因为C++11支持lambda表达式,还加入了Boost和TR1中的 std::bind ,以及自动可以自行推断类型的自动变量。“期望”作为拼图的最后一块,它使得函数化
编程模式并发化(FP-style concurrency)在C++中成为可能;一个“期望”对象可以在线程间互相传递,并允许其中一个计算结果依赖于另外一个的结果,而非对共享数据的显式访问。
快速排序 FP模式版
快速排序——顺序实现版:
#include <list>
#include <iostream>
#include <algorithm>
template<typename T>
std::list<T> sequential_quick_sort(std::list<T> input)
{
if (input.empty())
{
return input;
}
std::list<T> result;
result.splice(result.begin(), input, input.begin());
// 1,选择第一个数为“中间”值,使用splice()将输入的首个元素(中间值)放入结果列表中,inout中再无此元素。
T const & pivot = *result.begin(); // 2,中间值的引用
auto divide_point = std::partition(input.begin(), input.end(),
[&](T const& t) {return t<pivot;}); // 3,使用已获取的引用避免对“中间”值的拷贝
std::list<T> lower_part;
lower_part.splice(lower_part.end(), input, input.begin(),divide_point); // 4,移动小于divided_point的值
auto new_lower(
sequential_quick_sort(std::move(lower_part))); // 5,进入迭代
auto new_higher(
sequential_quick_sort(std::move(input))); // 6
result.splice(result.end(), new_higher); // 7,末尾插入
result.splice(result.begin(), new_lower); // 8,头部插入
return result;
}
int main()
{
std::list<int> l{ 3,6,9,1,0,2 };
auto r = sequential_quick_sort<int>(l);
for (auto iter = r.cbegin();iter != r.cend();++iter)
{
std::cout << *iter << "\t";
}
std::cout << std::endl;
system("pause");
return 0;
}
快速排序 FP模式线程强化版
快速排序——“期望”并行版:
#include <list>
#include <iostream>
#include <algorithm>
#include <future>
#include <thread>
template<typename T>
std::list<T> parallel_quick_sort(std::list<T> input)
{
if (input.empty())
{
return input;
}
std::list<T> result;
result.splice(result.begin(), input, input.begin());
T const& pivot = *result.begin();
auto divide_point = std::partition(input.begin(), input.end(),
[&](T const& t) {return t<pivot;});
std::list<T> lower_part;
lower_part.splice(lower_part.end(), input, input.begin(),
divide_point);
std::future<std::list<T> > new_lower( // 1,开新线程,其中&开以省略
std::async(parallel_quick_sort<T>, std::move(lower_part)));
auto new_higher(
parallel_quick_sort(std::move(input))); // 2,递归
result.splice(result.end(), new_higher); // 3
result.splice(result.begin(), new_lower.get()); // 4,取出值,并插入头部
return result;
}
int main()
{
std::list<int> l{ 3,6,9,1,0,2 };
auto r = parallel_quick_sort<int>(l);
for (auto iter = r.cbegin();iter != r.cend();++iter)
{
std::cout << *iter << "\t";
}
std::cout << std::endl;
system("pause");
return 0;
}
当任务过多时(已影响性能),这些任务应该在使用get()函数获取的线程上运行,而不是在新线程上运行,这样就能避免任务向线程传递的开销。
比起使用 std::async() ,你可以写一个spawn_task()函数对 std::packaged_task 和 std::thread 做简单的包装,你需要为函数结果创建一个 std::packaged_task 对象, 可以从这个对象中获取“期望”,或在线程中执行它,返回“期望”。其本身并不提供太多的好处(并且事实上会造成大规模的超额任务),但是它会为转型成一个更复杂的实现铺平道路,将会实现向一个队列添加任务,而后使用线程池的方式来运行它们。我们将在第9章再讨论线程池。使用 std::async 更适合于当你知道你在干什么,并且要完全控制在线程池中构建或执行过任务的线程。
spawn_task()函数:
template<typename F, typename A>//写法很魔性,一定要在<后+typename
std::future<typename std::result_of<F(A &&)>::type>
spawn_task(F&& f, A&& a)
{
typedef std::result_of<F(A&&)>::type result_type;
std::packaged_task<result_type(A&&)> task(std::move(f));
std::future<result_type> res(task.get_future());
std::thread t(std::move(task), std::move(a));
t.detach();
return res;
}
关于typename std::result_of<F(A &&>::type问题:在Effective C++(Items 42)提到,请使用关键字typename标识嵌套从属类型名称(该类型相依于template中出现的类型);但不得在基类列或成员初值列内以它作为Base class修饰符。
更改调用:
std::future<std::list<T> > new_lower(
spawn_task(parallel_quick_sort<T>, std::move(lower_part)));
因为避开了共享易变数据,函数化编程可算作是并发编程的范型;并且也是CSP(Communicating Sequential Processer[3],通讯顺序进程)的范型,这里线程理论上是完全分开的,也就是没有共享数据,但是有通讯通道允许信息在不同线程间进行传递。
使用消息传递的同步操作
CSP:当没有共享数据,每个线程就可以进行独立思考,其行为纯粹基于其所接收到的信息。
ATM逻辑类的简单实现(后续加入完整实现的分析):
struct card_inserted
{
std::string account;
};
class atm
{
messaging::receiver incoming;
messaging::sender bank;
messaging::sender interface_hardware;
void (atm::*state)();
std::string account;
std::string pin;
void waiting_for_card() // 1
{
interface_hardware.send(display_enter_card()); // 2,终端显示
incoming.wait(). // 3,等待消息传入处理
handle<card_inserted>(// 4,lambda函数
[&](card_inserted const& msg)
{
account = msg.account;
pin = "";
interface_hardware.send(display_enter_pin());
state = &atm::getting_pin;
}
);
}
void getting_pin();
public:
void run() // 5,运行开始处
{
state = &atm::waiting_for_card; // 6,初始化状态
try
{
for (;;)
{
(this->*state)(); // 7,反复执行当前状态的成员函数
}
}
catch (messaging::close_queue const&)
{
}
}
};
void atm::getting_pin()
{
//三种状态的转换
incoming.wait()
.handle<digit_pressed>( // 1,处理输入的pin
[&](digit_pressed const& msg)
{
unsigned const pin_length = 4;
pin += msg.digit;
if (pin.length() == pin_length)
{
bank.send(verify_pin(account, pin, incoming));
state = &atm::verifying_pin;
}
}
)
.handle<clear_last_pressed>( // 2,清除
[&](clear_last_pressed const& msg)
{
if (!pin.empty())
{
pin.resize(pin.length() - 1);
}
}
)
.handle<cancel_pressed>( // 3,退出
[&](cancel_pressed const& msg)
{
state = &atm::done_processing;
}
);
}