Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

4.2 使用future

std::future

`头文件

std::future<> 可移动不可拷贝(构造函数和赋值重载操作符)

  • An asynchronous operation (created via std::async, std::packaged_task, or std::promise) can provide a std::future object to the creator of that asynchronous operation.

  • The creator of the asynchronous operation can then use a variety of methods to query, wait for, or extract a value from the std::future. These methods may block if the asynchronous operation has not yet provided a value.

  • When the asynchronous operation is ready to send a result to the creator, it can do so by modifying shared state (e.g. std::promise::set_value) that is linked to the creator’s std::future.

假设你要乘飞机去国外度假,当到达机场办理完各种登机手续后,还需要等待机场广播通知登机。这段时间内,你可能会在候机室里面找一些事情来打发时间,比如:读书,上网,或者来一杯咖啡。不过,你就在等待一件事情:机场广播通知登机。

C++标准库将这种事件称为future。当线程需要等待特定事件时,某种程度上来说就需要知道期望的结果。之后,线程会周期性(较短的周期)的等待或检查事件是否触发(检查信息板),检查期间也会执行其他任务(品尝昂贵的咖啡)。另外,等待任务期间也可以先执行另外的任务,直到对应的任务触发,而后等待future的状态会变为就绪状态。future可能是和数据相关(比如,登机口编号),也可能不是。当事件发生时(状态为就绪),这个future就不能重置了。

C++标准库中有两种future,声明在**<future>头文件**中: unique future(std::future<>)和shared futures(std::shared_future<>),与了std::unique_ptrstd::shared_ptr非常类似。std::future只能与指定事件相关联,而std::shared_future就能关联多个事件。后者的实现中,所有实例会在同时变为就绪状态,并且可以访问与事件相关的数据。这种关联与模板有关,比如std::unique_ptrstd::shared_ptr的模板参数就是相关的数据类型。与数据无关处的,可以使用std::future<void>std::shared_future<void>的特化模板。虽然,我倾向于线程通讯,但future对象本身并不提供同步访问。当多个线程需要访问一个独立future对象时,必须使用互斥量或类似同步机制进行保护。不过,当多个线程对一个std::shared_future<>副本进行访问,即使同一个异步结果,也不需要同步future。

并行技术规范将这两个模板类在std::experimental命名空间中进行了扩展:std::experimental::future<>std::experimental::shared_future<> 。这个命名空间是为了将其与std命名空间中的模板类进行区分,实验命名空间中为这两个模板类添加了更多的功能。尤其是std::experimental中的内容与代码质量无关(我希望这里也会有较高质量的实现),需要强调的是这个命名空间提供的都不是标准类和函数,这个命名空间中类和函数的语法和语义,很可能与纳入C++标准(也就是std命名空间)后有所不同。如果想要使用这两个试验性的模板类,需要包含<experimental/future>头文件。

最简单的事件,就是在后台运行的计算操作。第2章中已经清楚了std::thread 执行的任务不能有返回值,不过这个问题能使用future进行解决。

4.2.1 后台任务的返回值 std::async

std::async 模板函数

构造函数:(六个,两种)

  • async( Function&& f, Args&&… args );

  • async( std::launch policy, Function&& f, Args&&… args );

概述:runs the function f asynchronously (potentially in a separate thread which might be a part of a thread pool) and returns a std::future that will eventually hold the result of that function call.

policy:

  • std::launch::async:executes the callable object f on a new thread of execution (with all thread-locals initialized) as if spawned by std::thread(std::forward(f), std::forward(args)…), except that if the function f returns a value or throws an exception, it is stored in the shared state accessible through the std::future that async returns to the caller.

  • If the deferred flag is set (i.e. (policy & std::launch::deferred) != 0):converts f and args... the same way as by std::thread constructor, but does not spawn a new thread of execution. Instead, lazy evaluation is performed: the first call to a non-timed wait function on the std::future that async returned to the caller will cause

    the copy of f to be invoked (as an rvalue) with the copies of args... (also passed as rvalues) in the current thread(对这个std::future调用non-timed wait function的线程) (which does not have to be the thread that originally called std::async). The result or exception is placed in the shared state associated with the future and only then it is made ready. All further accesses to the same std::future will return the result immediately.

  • If more than one flag is set, it is implementation-defined which policy is selected. For the default (both the std::launch::async and std::launch::deferred flags are set in policy), standard recommends (but doesn’t require) utilizing available concurrency, and deferring any additional tasks.

关于其中std::future的析构:

If the std::future obtained from std::async is not moved from or bound to a reference, the destructor of the std::future will block at the end of the full expression until the asynchronous operation completes, essentially making code such as the following synchronous:

std::async(std::launch::async, []{ f(); }); // temporary's dtor waits for f()
std::async(std::launch::async, []{ g(); }); // does not start until f() completes

假设有一个需要长时间的运算,需要计算出一个有效值,但并不迫切需要这个值。你可以启动新线程来执行这个计算,你需要计算的结果,而std::thread并不提供直接接收返回值的机制。这里就需要**std::async函数模板(也是在头文件<future>**)。

当不着急让任务结果时,可以使用**std::async启动一个异步任务。与std::thread对象等待的方式不同,std::async会返回一个std::future对象,这个对象持有最终计算出来的结果。当需要这个值时,只需要调用这个对象的get()成员函数,就会阻塞(调用)线程直到future为就绪为止**,并返回计算结果。

代码4.6 std::future从异步任务中获取返回值

#include <future>
#include <iostream>

int find_the_answer_to_ltuae();
void do_other_stuff();
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;
}

std::thread方式一样,std::async允许通过添加额外的调用参数,向函数传递额外的参数。第一个参数是指向成员函数的指针第二个参数提供这个函数成员类的具体对象(是通过指针,也可以包装在std::ref中),剩余的参数可作为函数的参数传入。否则,第二个和随后的参数将作为函数的参数,或作为指定可调用对象的第一个参数。和std::thread一样,当参数为右值时,拷贝操作将使用移动的方式转移原始数据,就可以使用“只移动”类型作为函数对象和参数。

代码4.7 使用std::async向函数传递参数

#include <string>
#include <future>
struct X
{
  void foo(int,std::string const&);
  std::string bar(std::string const&);
};
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的拷贝副本
struct Y
{
  double operator()(double);
};
Y y;
auto f3=std::async(Y(),3.141);  // 调用tmpy(3.141),tmpy通过Y的移动构造函数得到
auto f4=std::async(std::ref(y),2.718);  // 调用y(2.718)
X baz(X&);
std::async(baz,std::ref(x));  // 调用baz(x)
class move_only
{
public:
  move_only();
  move_only(move_only&&)
  move_only(move_only const&) = delete;
  move_only& operator=(move_only&&);
  move_only& operator=(move_only const&) = delete;
  
  void operator()();
};
auto f5=std::async(move_only());  // 调用tmp(),tmp是通过std::move(move_only())构造得到

future的等待取决于std::async是否启动一个线程,或是否有任务在进行同步。大多数情况下,也可以在函数调用之前std::async传递一个额外参数,这个参数的类型是std::launch,还可以是std::launch::defered,表明函数调用延迟到wait()或get()函数调用时才执行,**std::launch::async表明函数必须在独立线程上执行std::launch::deferred | std::launch::async表明实现可以选择这两种方式的一种**(“ If more than one flag is set, it is implementation-defined which policy is selected.”)。最后一个选项(std::launch::deferred | std::launch::async)是默认的,当函数调用延迟,就可能不会再运行了。如下所示:

auto f6=std::async(std::launch::async,Y(),1.2);  // 在新线程上执行
auto f7=std::async(std::launch::deferred,baz,std::ref(x));  // 在wait()或get()调用时执行
auto f8=std::async(
              std::launch::deferred | std::launch::async,
              baz,std::ref(x));  // 实现选择执行方式
auto f9=std::async(baz,std::ref(x));
f7.wait();  //  调用延迟函数

本章的后续小节和第8章中,会再次看到这段程序,使用std::async会将算法分割到各个任务中,这样程序就能并发了。不过,这不是让std::future与任务实例相关联的唯一方式,也可以将任务包装入std::packaged_task<>中,或通过编写代码的方式,使用std::promise<>模板显式设置值。与std::promise<>相比,std::packaged_task<>具有更高的抽象,所以我们从“高抽象”模板说起。

4.2.2 future与任务关联 std::packaged_task

std::packaged_task<>  可移动不可拷贝

概述:The class template std::packaged_task wraps any Callable target (function, lambda expression, bind expression, or another function object) so that it can be invoked asynchronously. Its return value or exception thrown is stored in a shared state which can be accessed through std::future objects.

(args)重载函数:Calls the stored task with args as the arguments(在调用这个重载函数的线程中执行). The return value of the task or any exceptions thrown are stored in the shared state. The shared state is made ready and any threads waiting for this are unblocked.

std::packaged_task<>会将future与函数或可调用对象进行绑定。当调用std::packaged_task<>对象时,就会调用相关函数或可调用对象,(并且返回值作为异步结果存储在std::future)当future状态为就绪时,会存储返回值。这可以用在构建线程池(可见第9章)或其他任务的管理中,比如:在任务所在线程上运行其他任务,或将它们串行运行在一个特殊的后台线程上。当粒度较大的操作被分解为独立的子任务时,每个子任务都可以包含在std::packaged_task<>实例中,之后将实例传递到任务调度器或线程池中。对任务细节进行抽象,调度器仅处理std::packaged_task<>实例,而非处理单独的函数。

std::packaged_task<>模板参数是一个函数签名,比如void()就是一个没有参数也没有返回值的函数,或int(std::string&, double*)就是有一个非const引用的std::string参数和一个指向double类型的指针参数,并且返回类型是int。构造std::packaged_task<>实例时,就必须传入函数或可调用对象。这个函数或可调用的对象,需要能接收指定的参数和返回(可转换为指定返回类型的)值。类型可以不完全匹配,因为这里类型可以隐式转换,可以用int类型参数和返回float类型的函数,来构建std::packaged_task<double(double)>实例。

函数签名的返回类型可以用来标识从get_future()返回的std::future<>的类型,而函数签名的参数列表,可用来指定packaged_task的函数调用操作符。例如,模板偏特化std::packaged_task<std::string(std::vector<char>*,int)>会在下面的代码中使用到。

代码4.8 std::packaged_task<>的偏特化

template<>
class packaged_task<std::string(std::vector<char>*,int)>
{
public:
  template<typename Callable>
  explicit 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()获取(获取future对象)**。因此可以用std::packaged_task对任务进行打包,并适时的取回future。当异步任务需要返回值时,可以等待future状态变为“就绪”。

线程间传递任务

很多图形架构需要特定的线程去更新界面,所以当线程对界面更新时,需要发出一条信息给正确的线程,让相应的线程来做界面更新。std::packaged_task提供了这种功能,且不需要发送一条自定义信息给图形界面线程。

代码4.9 使用std::packaged_task执行一个图形界面线程

#include <deque>
#include <mutex>
#include <future>
#include <thread>
#include <utility>

std::mutex m;
std::deque<std::packaged_task<void()> > tasks;

bool gui_shutdown_message_received();
void get_and_process_gui_message();

void gui_thread()  // 1
{
  //没有关闭gui的情况下,不断尝试从队首取任务
  while(!gui_shutdown_message_received())  // 2
  {
    get_and_process_gui_message();  // 3
    std::packaged_task<void()> task;
    { //这里设置一个scope的目的是std::lock_guard<std::mutex>对象离开作用域时析构解锁
      std::lock_guard<std::mutex> lk(m);
      if(tasks.empty())  // 4
        continue; //对于while循环的continue
      task=std::move(tasks.front());  // 5 调用std::packaged_task<void()>的移动赋值重载函数
      tasks.pop_front();
    }
    task();  // 6
  }
}

std::thread gui_bg_thread(gui_thread);

//将任务传入队列
template<typename Func>
std::future<void> post_task_for_gui_thread(Func f)
{
  std::packaged_task<void()> task(f);  // 7
  std::future<void> res=task.get_future();  // 8
  std::lock_guard<std::mutex> lk(m);
  tasks.push_back(std::move(task));  // 9
  return res; // 10
}

代码十分简单:图形界面线程①循环直到收到一条关闭图形界面的信息后关闭界面②。关闭界面前,进行轮询界面消息处理③,例如:用户点击和执行在队列中的任务。当队列中没有任务④时,循环将继续。除非能在队列中提取出一个任务⑤,释放队列上的锁,并且执行任务⑥。这里future与任务相关,当任务执行完时,其状态会置为“就绪”。

将任务传入队列:提供的函数⑦可以提供一个打包好的任务,通过这个任务⑧调用get_future()成员函数获取future对象,并且在任务推入列表⑨之前,future将返回调用函数⑩。

例子中使用std::packaged_task<void()>创建任务,其中包含了一个无参数无返回值的函数或可调用对象(如果当这个调用有返回值时,返回值会被丢弃)。这可能是最简单的任务,std::packaged_task也可以用于复杂的情况——通过指定不同的函数签名作为模板参数,不仅可以改变其返回类型(因此该类型的数据会存在期望相关的状态中),也可以改变函数操作符的参数类型。这个例子可以简单的扩展成允许任务运行在图形界面线程上,并且接受传参,还可以通过std::future获取返回值。

这些任务能作为简单的函数调用来表达吗?还有,任务的结果能从很多地方得到吗?这些问题可以使用第三种方法创建future来解决:使用std::promise对值进行显示设置。

4.2.3 使用std::promise

std::promise 可移动不可拷贝

A promise may do three things with the shared state(那个与这个promise对象相关联的std::future):

  • make ready: the promise stores the result or the exception in the shared state. Marks the state ready and unblocks any thread waiting on a future associated with the shared state.
  • release: the promise gives up its reference to the shared state. If this was the last such reference, the shared state is destroyed. Unless this was a shared state created by std::async which is not yet ready, this operation does not block.
  • abandon: the promise stores the exception of type std::future_error with error code std::future_errc::broken_promise, makes the shared state ready, and then releases it.

The promise is the “push” end of the promise-future communication channel: the operation that stores a value in the shared state synchronizes-with (as defined in std::memory_order) the successful return from any function that is waiting on the shared state (such as std::future::get).

Concurrent access to the same shared state may conflict otherwise: for example multiple callers of std::shared_future::get must either all be read-only or provide external synchronization.

当需要处理很多网络连接时,会使用不同线程尝试连接每个接口,能使网络尽早联通。不幸的是,随着连接数量的增长,这种方式变的越来越不合适。因为大量的线程会消耗大量的系统资源,还有可能造成线程上下文频繁切换(当线程数量超出硬件可接受的并发数时),这都会对性能有影响。最极端的例子:线程会将系统资源消耗殆尽,系统连接网络的能力会变的极差。因此通过少数线程处理网络连接,每个线程同时处理多个连接,对需要处理大量网络连接的应用而言,这是一种比较普遍的做法。

当线程处理多个连接事件,来自不同的端口连接的数据包基本上以乱序方式进行处理。同样的,数据包也将以乱序的方式进入队列。很多情况下,一些应用不是等待数据成功的发送,就是等待(新的)指定网络接口数据的接收成功。

std::promise<T>提供设定值的方式(类型为T),这个类型会和后面看到的std::future<T>对象相关联。std::promise/std::future对提供一种机制:future可以阻塞等待线程,提供数据的线程可以使用promise对相关值进行设置,并将future的状态置为“就绪”。

可以通过给定的**std::promise的get_future()成员函数来获取与之相关的std::future对象,与std::packaged_task的用法类似。当promise设置完毕(使用set_value()成员函数)时,对应的future状态就变为“就绪”,并且可用于检索已存储的值。当设置值之前销毁std::promise,将会存储一个异常**。在4.2.4节中,会详细描述异常是如何传送到线程的。

代码4.10中是单线程处理多接口的实现,这个例子中,可以使用一对std::promise<bool>/std::future<bool>找出传出成功的数据块,与future相关的只是简单的“成功/失败”标识。对于传入包,与future相关的数据就是数据包的有效负载。

代码4.10 使用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 获取引用
        p.set_value(data.payload); //set_value
      }
      if(connection->has_outgoing_data())  // 5
      {
        outgoing_packet data=
            connection->top_of_outgoing_queue();
        connection->send(data.payload);
        data.promise.set_value(true);  // 6 表明传输成功 set_value
      }
    }
  }
}

process_connections()中(直到done()返回true①为止)每一次循环,都会依次的检查每个连接②,检索是否有数据③或正在发送已入队的传出数据⑤。假设输入数据包是具有ID和有效负载的(有实际的数在其中),一个ID映射到一个std::promise(可能是在相关容器中进行的依次查找)④,并且值是在包的有效负载中。传出包是在传出队列中检索,从接口直接发送出去。当发送完成,传出数据相关的promise将置为true,来表明传输成功⑥。是否能映射到实际网络协议上,取决于所用协议。

上面的代码不理会异常,一切工作都会很好的执行,但有悖常理。有时候磁盘满载,有时候会找不到东西,有时候网络会断,还有时候数据库会崩溃。当需要某个操作的结果时,就需要在对应的线程上执行这个操作,因为代码可以通过异常来报告错误。不过,这会对使用std::packaged_taskstd::promise带来一些不必要的限制。因此,C++标准库提供了一种在以上情况下清理异常的方法,并且允许将异常存储为相关结果的一部分。

4.2.4 将异常存与future中

看完下面的代码段,思考一下:当你传递-1到square_root()中时,它将抛出一个异常,并且你想让调用者看到这个异常:

double square_root(double x)
{
  if(x<0)
  {
    throw std::out_of_range("x<0");
  }
  return sqrt(x);
}

假设调用square_root()函数不是当前线程,

double y=square_root(-1);

将调用改为异步调用:

std::future<double> f=std::async(square_root,-1);
double y=f.get();

当y获得函数调用的结果,线程调用f.get()时,就能再看到异常了。

函数作为**std::async的一部分时,当调用抛出一个异常时,这个异常就会存储到future中之后future的状态置为“就绪”,之后调用get()会抛出已存储的异常**(注意:标准级别没有指定重新抛出的这个异常是原始的异常对象,还是一个拷贝。不同的编译器和库将会在这方面做出不同的选择)。将函数打包入std::packaged_task任务包后,当任务调用时,同样的事情也会发生。打包函数抛出一个异常,这个异常将存储在future中,在get()调用时会再次抛出。

当然,通过函数的显式调用,std::promise也能提供同样的功能。当存入的是异常而非数值时,就需要调用set_exception()成员函数,而非set_value()。这通常是用在一个catch块中,并作为算法的一部分。为了捕获异常,这里使用异常填充promise:

extern std::promise<double> some_promise;
try
{
  some_promise.set_value(calculate_value());
}
catch(...)
{
  some_promise.set_exception(std::current_exception()); //set_exception
}

这里使用std::current_exception()来检索抛出的异常,可用std::copy_exception()作为替代方案,std::copy_exception()直接存储新的异常而不抛出

some_promise.set_exception(std::copy_exception(std::logic_error("foo ")));

比使用try/catch块更加清晰,当异常类型已知,就应该优先使用。不是因为代码实现简单,而是给编译器提供了极大的优化空间。

另一种向future中存储异常的方式,在没有调用promise上的任何设置函数前,或正在调用包装好的任务时,销毁与std::promisestd::packaged_task相关的future对象。任何情况下,当future的状态还不是“就绪”时,调用std::promisestd::packaged_task的析构函数将会存储一个与std::future_errc::broken_promise错误状态相关的std::future_error异常。通过创建一个future,可以构造一个promise为其提供值或异常,也可以通过销毁值和异常源,去违背promise。这种情况下,编译器没有在future中存储任何东西,线程可能会永远的等下去。

现在,例子中都在用std::future,不过std::future也有局限性。很多线程在等待的时候,只有一个线程能获取结果。当多个线程等待相同事件的结果时,就需要使用std::shared_future来替代std::future了。

4.2.5 多个线程的等待 std::shared_future

std::shared_future 可拷贝

虽然std::future可以处理所有在线程间数据转移的同步,但是调用某一特殊 std::future对象的成员函数,就会让这个线程的数据和其他线程的数据不同步。多线程在没有额外同步的情况下,访问独立std::future对象时,就会有数据竞争和未定义行为。因为std::future独享同步结果,并且通过调用get()函数,一次性的获取数据,这就让并发访问变的毫无意义。

如果并行代码没办法让多个线程等待同一个事件,std::shared_future可以帮你解决这个问题。因为**std::future是只移动的,所以其所有权可以在不同的实例中互相传递,但只有一个实例可以获得特定的同步结果,而std::shared_future实例是可拷贝的**,所以多个对象可以引用同一关联期望值的结果。

每一个std::shared_future的独立对象上,成员函数调用返回的结果还是不同步的,所以为了在多个线程访问一个独立对象时避免数据竞争,必须使用锁来对访问进行保护。优先使用的办法:为了替代只有一个拷贝对象的情况,可以让每个线程都拥有自己对应的拷贝对象。这样,当每个线程都通过自己拥有的std::shared_future对象获取结果,那么多个线程访问共享同步结果就是安全的。可见图4.1。

图4.1 使用多个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()); //把p内部的future转移给f,即f现在是p内部的future
assert(f.valid());  // 1 期望值 f 是合法的
std::shared_future<int> sf(std::move(f)); //移动语义的std::shared_future<int>的构造函数
assert(!f.valid());  // 2 期望值 f 现在是不合法的
assert(sf.valid());  // 3 sf 现在是合法的

期望值f开始是合法的①,因为引用的是promise p的同步状态,但是在转移sf的状态后,f就不合法了②,而sf就是合法的了③。

如其他可移动对象一样,转移所有权是对右值的隐式操作,所以可以通过std::promise对象的成员函数get_future()的返回值,直接构造一个std::shared_future对象,例如:

std::promise<std::string> p;
std::shared_future<std::string> sf(p.get_future());  // 1 隐式转移所有权 调用std::shared_future<std::string>移动语义的构造函数

转移所有权是隐式的,用右值构造std::shared_future<>,得到std::future<std::string>类型的实例①。

std::future的这种特性,可促进std::shared_future的使用,容器可以自动的对类型进行推断,从而初始化该类型的变量(详见附录A,A.6节)。**std::future有一个share()成员函数,可用来创建新的std::shared_future ,并且可以直接转移future的所有权**。这样也就能保存很多类型,并且使得代码易于修改:

std::promise< std::map< SomeIndexType, SomeDataType, SomeComparator,
     SomeAllocator>::iterator> p;
auto sf=p.get_future().share();

这个例子中,sf的类型推导为std::shared_future<std::map<SomeIndexType, SomeDataType, SomeComparator, SomeAllocator>::iterator>,还真的长。当比较器或分配器有所改动,只需要对promise的类型进行修改即可。future的类型会自动与promise的修改进行匹配。

有时需要限定等待事件的时间,不论是因为时间上有硬性规定(一段指定的代码需要在某段时间内完成),还是因为在事件没有很快的触发,或是有工作需要特定线程来完成,为了处理这种情况,需要等待函数能对超时进行指定。

评论