unifex:C++现代异步模型先导


成为C++23的标准 std::execution将带给C++一个modern asynchronous model。本文将翻译 libunifex文档并梳理代码结构。
译注:下文的英文单词Concept/concept用于指代C++20的std::concept。

定制点机制

Unifex库运用一种tag_invoke()机制,这种机制允许类型通过定义一个tag_invoke()的重载函数来定制操作,ADL(参数依赖查找,argument-dependent lookup) 可以找到这个重载函数,重载函数把定制点对象(CPO,customisation-points object) 作为第一个参数,并上其余参数一同传给CPO::operator()

更多关于tag_invoke详情可以查阅 P1895R0

unifex::tag_invoke()本身也是个定制点对象,所以它可以被ADL给找到并转发到它的重载函数。这个机制的要点就是需要构造一个Niebloid,因此你不必对每个CPO都进行重载。用重载tag_invoke()函数的形式去自定义CPO的方式是把这些重载函数要声明为友元函数,这样在解析tag_invoke()的调用时可以缩小编译器需要检索的重载函数集。

例子1:定义一个新的CPO

inline struct example_cpo {
  // An optional default implementation
  template<typename T>
  friend bool tag_invoke(example_cpo, const T& x) noexcept {
    return false;
  }

  template<typename T>
  bool operator()(const T& x) const
    noexcept(is_nothrow_tag_invocable_v<example_cpo, const T&>)
    -> tag_invoke_result_t<example_cpo, const T&> {
    // Dispatch to the call to tag_invoke() passing the CPO as the
    // first parameter.
    return tag_invoke(example_cpo{}, x);
  }
} example;

例子2:自定义CPO

struct my_type {
  friend bool tag_invoke(tag_t<example>, const my_type& t) noexcept {
    return t.isExample;
  }

  bool isExample;
}

例子3:调用CPO

struct other_type {};

void usage() {
  // Customised for this type so will dispatch to custom implementation.
  my_type t{true};
  assert(example(t) == true);

  // Not customised so falls back to default implementation.
  other_type o;
  assert(example(t) == false);
}

封装类型也可以通过CPO的子集去转发到封装的对象。

template<typename T, typename Allocator>
struct allocator_wrapper {
  // Customise one CPO.
  friend void tag_invoke(tag_t<get_allocator>, const allocator_wrapper& a) {
    return a.alloc_;
  }

  // Pass through the rest.
  template<typename CPO, typename... Args>
  friend auto tag_invoke(CPO cpo, const allocator_wrapper& x, Args&&... args)
    noexcept(std::is_nothrow_invocable_v<CPO, const T&, Args...>)
    -> std::invoke_result_t<CPO, const T&, Args...> {
    return std::move(cpo)(x.inner_, (Args&&)...);
  }

  Allocator alloc_;
  T inner_;
};

Concept

unifex由以下9个关键concept构成异步语义:

  • Receiver - 一个接收异步操作结果的回调泛化
  • Sender - 一个传递结果给Receiver的操作
  • TypedSender - 一个描述发送类型的Sender
  • OperationState - 一个持有异步操作状态的对象
  • ManySender - 一个可以发送多个值传给ReceiverSender
  • AsyncStream - 类似一个input range,序列的每一个值只有当请求的时候才会异步地惰性求值。
  • Scheduler - 一个用于调度work到context的对象
  • TimeScheduler - 一个用于在特定时间点调度work到context的对象
  • StopToken - 各种类stop-token的concept,被用于标识一个请求到停止一个操作。

Sender/Receiver

Sender指一个使用三个定制点set_value, set_doneset_error 中某一个向Receiver对象交付结果的操作。Sender是一个异步操作,而传统的函数对象或者lambda是一个同步操作。将异步操作具体化并使用标准接口来启动它们并提供continuation,我们允许让Sender延迟启动,和使用泛型算法与其他操作相互组合。

启动一个异步操作

Sender可以是一个延迟执行的操作,也可以是已经在执行的操作。不过,从concept上看,我们应该认为Sender是一个惰性操作,并且需要显式执行它。
为了初始化一个异步操作,首先你要使用 connect()函数传入SenderReceiver,返回一个OperationState对象。这个对象持有操作状态和封装了执行异步操作的必要逻辑。一个异步操作过程中可能包含多个需要执行的步骤并产生了一些中间结果,OperationState对象则是这个异步操作过程的状态机。
我们调用start()函数并用左值引用传递OperationState对象,那么这从OperationState上看,异步操作/状态机就算是“启动”了。一旦启动,异步操作将一直执行到它最终完成为止。
connect()start()的操作解耦,允许调用者控制OperationState对象的存放布局和生命周期。OperationState对象还可以被视作一个类型,调用者在编译期便获取OperationState对象的大小,因此能把它放入stack管理,或者放入协程管理,又或者存储为某个类的成员。

operation-state的生命周期

调用者要保证,一旦start()被调用,operation-state在异步操作完成之前要一直存活。一旦receiver调用一个异步完成信号函数,调用者要让receiver确保operation-state被析构。意思是,从operation-state角度来看,一旦receiver执行了操作完成信号函数后,调用者不能假定operation-state仍在存活,因为receiver可能会销毁这个对象。operation-state不能被移动或者拷贝。你必须就地构造对象,例如像connect()一样使用copy-elision就地返回一个对象。

异步操作完成

当给set_value), set_done()set_error()定制点传入以receiver为首的实参成功调用后便记为操作完成

  • set_value调用指示“成功操作”。
  • set_error调用指示“失败操作”。
  • set_done调用指示“完成操作”。(操作无结果)

出现无结果的原因是上层抽象已经完成目的并想要提早结束操作。在这种情况下,操作有可能满足后置条件,但是由于上层抽象的原因中止了是否满足后置条件的判断。

详情可以查阅文档: Cancellation is serendipitous-success

这里要注意到没有结果的“操作成功”无结果的“操作完成” 的区别。它们形式上都没有结果,因此我们要根据后置条件去判断应该调用哪一个:如果满足后置条件则调用set_value;没有满足后置条件则调用 set_done

Receiver

Receiver是一个利用三个定制点接收异步操作结果回调的泛化。它也可以被认为是一个异步操作的continuation
不要将Receiver理解成一个个单一的concept,而是理解成对接收特定完成信号后做相应处理的concept

  • value_receiver<Values...>表明一个receiver接受一个带Values...类型实参的set_value()完成信号。
  • error_receiver<Error>表明一个receiver接受一个带Error类型值的set_error()完成信号。
  • done_receiver表明一个receiver接受一个不带值的set_done()完成信号。
    Receiver可以被移动构造和析构。

以下伪代码可以描述Receiver:

namespace unifex
{
  // CPOs
  inline constexpr unspecified set_value = unspecified;
  inline constexpr unspecified set_error = unspecified;
  inline constexpr unspecified set_done = unspecified;

  template<typename R>
  concept __receiver_common =
    std::move_constructible<R> &&
    std::destructible<R>;

  template<typename R>
  concept done_receiver =
    __receiver_common<R> &&
    requires(R&& r) {
        set_done((R&&)r);
    };

  template<typename R, typename... Values>
  concept value_receiver =
    __receiver_common<R> &&
    requires(R&& r, Values&&... values) {
      set_value((R&&)r, (Values&&)values...);
    };

  template<typename R, typename Error>
  concept error_receiver =
    __receiver_common<R> &&
    requires(R&& r, Error&& error) {
      set_error((R&&)r, (Error&&)error);
    };
}

不同的sender有着不同的完成信号集,它们可以潜在地完成这些信号,因此对于传递给它们的connect()的receiver会有不同的要求。上面concept可以组合一起去约束 connect() 操作以支持sender支持的一组完成信号。

上下文信息

调用者也可以使用receiver传递上下文信息给被调用者。receiver可能自定义额外的getter CPOs,所以允许sender查询调用上下文信息。例如,为封闭上下文去检索StopToken,AllocatorScheduler。又例如,在operatio中,get_stop_token()CPO调用一个receiver获取reciever的stop_token,这样receiver可以通过这个stop-token对停止操作的请求进行通信。

按语:可以通过接收者作为隐含上下文从调用方传递给被调用方的一组内容是开放的。应用程序可以使用额外的特定于应用程序的上下文信息来扩展这个集合,这些信息可以通过接收器传递。The set of things that could be passed down as implicit context from caller
to callee via the receiver is an open-set. Applications can extend this set with
additional application-specific contextual information that can be passed through
via the receiver.

Sender

Sender表示一个求值的异步操作,即通过调用三个定制点其中一个向receiver发送完成信号。目前没有一个通用的Sender concept。一般来说,不可能确定一个对象是一个sender
In general it’s not possible to determine whether an object is a sender in isolation
of a receiver. Once you have both a sender and a receiver you can check if a sender
can send its results to a receiver of that type by checking the sender_to concept.

最简单就是利用connect()测试一个类型Ssender 到一个类型Rreceiver

namespace unifex
{
  // Sender CPOs
  inline constexpr unspecified connect = unspecified;

  // Test whether a given sender and receiver can been connected.
  template<typename S, typename R>
  concept sender_to =
    requires(S&& sender, R&& receiver) {
      connect((S&&)sender, (R&&)receiver);
    };
}

待办:Consider adding some kind of sender_traits class or an is_sender<T> CPO
that can be specialised to allow a type to opt-in to being classified as a sender
independently of a concrete receiver type.

TypedSender

TypedSender扩展了Sender接口以支持两个以上的嵌套模板类型别名,可用于查询set_value() and set_error()的重载函数。
当我们定义一个嵌套模板类型别名value_types时,会有两个双重模板参数:VariantTupleVariant的实例会变成类型别名产生的类型,模板参数对每一个被调用的set_value重载函数中的模板参数是Tuple<...>的实例,为每个形参提供一个模板实参,该实参将在receiver形参之后传递给’ set_value '。

A nested template type alias value_types is defined, which takes two template
template parameters, a Variant and a Tuple, from which the type-alias produces
a type that is an instantiation of Variant, with a template argument for each
overload of set_value that may be called, with each template argument being an
instantiation of Tuple<...> with a template argument for each parameter that
will be passed to set_value after the receiver parameter.

当我们定义一个嵌套模板类型别名error_types时,它接受一个双重模板参数:Variant
类型别名会对这个参数生成一个Variant实例的类型,每一个要被调用set_error重载函数都有一个模板参数,当调用set_error时,这个模板参数会是error实参类型。

A nested template type alias error_types is defined, which takes a single
template template parameter, a Variant, from which the type-alias produces
a type that is an instantiation of Variant, with a template argument for each
overload of set_error that may be called, with each template argument being
the type of the error argument for the call to set_error.

定义一个嵌套的 static constexpr bool sends_done , 使用set_done表示sender无论如何都会完成。例如:

struct some_typed_sender {
 template<template<typename...> class Variant,
          template<typename...> class Tuple>
 using value_types = Variant<Tuple<int>,
                             Tuple<std::string, int>,
                             Tuple<>>;

 template<template<typename...> class Variant>
 using error_types = Variant<std::exception_ptr>;

 static constexpr bool sends_done = true;
 ...
};

这个TypedSender 表明它将会调用以下的重载函数:

  • set_value(R&&, int)
  • set_value(R&&, std::string, int)
  • set_value(R&&)
  • set_error(R&&, std::exception_ptr)
  • set_done(R&&)

当要检索sendvalue_types/error_types/sends_done属性时,你应该在 sender_traits<Sender>类里查找它们而不是在sender type定义上。

如:typename unifex::sender_traits<Sender>::template value_types<std::variant, std::tuple>

OperationState

OperationState对象包含一个独立异步操作的所有状态。operation-state是调用 connect()函数的返回值,它不可拷贝也不可移动。你只能对operation-state做两件事:start()或者销毁它。当且仅当operation-state没有被start()调用或者被调用后操作已经完成,operation-state销毁才有效。

namespace unifex
{
  // CPO for starting an async operation
  inline constexpr unspecified start = unspecified;

  // CPO for an operation-state object.
  template<typename T>
  concept operation_state =
    std::destructible<T> &&
    requires(T& operation) {
      start(operation);
    };
}

ManySender/ManyReceiver

ManySender表示会求零个或多个值的异步操作。它通过调用set_next()来获取每一个求值,调用三个定制点来结束操作。
ManySender封装了值序列(即对set_next()的调用是非重叠的)和并行/批量操作(即再不同的线程/SIMD通道上可能存在对set_next()的并发/重叠)。
ManySender没有反压机制。一旦它启动了,完全由sender驱动向receiver传值,由receiver请求sender停止发送值。例,
A ManySender does not have a back-pressure mechanism. Once started, the delivery of values to the receiver is entirely driven by the sender. The receiver can request the sender to stop sending values, e.g. by causing the StopToken to enter the
stop_requested() state, but the sender may or may not respond in a timely manner.
Stream(下文提及)对比,只有消费者请求,ManySender才会惰性求下一个值,提供一个天然反压机制。

Sender vs. ManySender

sender只能产生一个结果
Whereas Sender produces a single result. ie. a single call to one of either
set_value(), set_done() or set_error(), a ManySender produces multiple values
via zero or more calls to set_next() followed by a call to either set_value(),
set_done() or set_error() to terminate the sequence.

A Sender is a kind of ManySender, just a degenerate ManySender that never
sends any elements via set_next().

Also, a ManyReceiver is a kind of Receiver. You can pass a ManyReceiver
to a Sender, it will just never have its set_next() method called on it.

Note that terminal calls to a receiver (i.e. set_value(), set_done() or set_error())
must be passed an rvalue-reference to the receiver, while non-terminal calls to a receiver
(i.e. set_next()) must be passed an lvalue-reference to the receiver.

The sender is responsible for ensuring that the return from any call to set_next()
strongly happens before the call to deliver a terminal signal is made.
ie. that any effects of calls to set_next() are visible within the terminal signal call.

A terminal call to set_value() indicates that the full-set of set_next() calls were
successfully delivered and that the operation as a whole completed successfully.

Note that the set_value() can be considered as the sentinel value of the parallel
tasks. Often this will be invoked with an empty pack of values, but it is also valid
to pass values to this set_value() call.
e.g. This can be used to produce the result of the reduce operation.

A terminal call to set_done() or set_error() indicates that the operation may have
completed early, either because the operation was asked to stop early (as in set_done)
or because the operation was unable to satisfy its post-conditions due to some failure
(as in set_error). In this case it is not guaranteed that the full set of values were
delivered via set_next() calls.

As with a Sender and ManySender you must call connect() to connect a sender
to it. This returns an OperationState that holds state for the many-sender operation.

The ManySender will not make any calls to set_next(), set_value(), set_done()
or set_error() before calling start() on the operation-state returned from
connect().

因此,Sender通常要添加它的 connect()操作。如下:

struct some_sender_of_int {
  template<typename Receiver>
  struct operation { ... };

  template<typename Receiver>
    requires
      value_receiver<std::decay_t<Receiver>, int> &&
      done_receiver<std::decay_t<Receiver>
  friend operation<std::decay_t<Receiver>> tag_invoke(
    tag_t<connect>, some_many_sender&& s, Receiver&& r);
};

ManySender的添加 connect()操作方式如下:

struct some_many_sender_of_ints {
  template<typename Receiver>
  struct operation { ... };

  template<typename Receiver>
    requires
      next_receiver<std::decay_t<Receiver>, int> &&
      value_receiver<std::decay_t<Receiver>> &&
      done_receiver<std::decay_t<Receiver>>
  friend operation<std::decay_t<Receiver>> tag_invoke(
    tag_t<connect>, some_many_sender&& s, Receiver&& r);
};

顺序执行 vs. 并发执行

从上层抽象看,ManySende会向receiver发送多个值。一些用例,我们想
For some use-cases we want to process these values one at a time and in
a particular order. ie. process them sequentially. This is largely the
pattern that the Reactive Extensions (Rx) community has built their
concepts around.

For other use-cases we want to process these values in parallel, allowing
multiple threads, SIMD lanes, or GPU cores to process the values more
quickly than would be possible normally.

In both cases, we have a number of calls to set_next, followed by a
call to set_value, set_error or set_done.
So what is the difference between these cases?

Firstly, the ManySender implementation needs to be capable of making
overlapping calls to set_next() - it needs to have the necessary
execution resources available to be able to do this.
Some senders may only have access to a single execution agent and so
are only able to send a single value at a time.

Secondly, the receiver needs to be prepared to handle overlapping calls
to set_next(). Some receiver implementations may update shared state
with the each value without synchronisation and so it would be undefined
behaviour to make concurrent calls to set_next(). While other
receivers may have either implemented the required synchronisation or
just not require synchronisation e.g. because they do not modify
any shared state.

The set of possible execution patterns is thus constrained to the
intersection of the capabilities of the sender and the constraints
placed on the call pattern by the receiver.

Note that the constraints that the receiver places on the valid
execution patterns are analagous to the “execution policy” parameter
of the standard library parallel algorithms.

With existing parallel algorithms in the standard library, when you
pass an execution policy, such as std::execution::par, you are telling
the implementation of that algorithm the constraints of how it is
allowed to call the callback you passed to it.

例如:

std::vector<int> v = ...;

int max = std::reduce(std::execution::par_unseq,
                      v.begin(), v.end(),
                      std::numeric_limits<int>::min(),
                      [](int a, int b) { return std::max(a, b); });

Passing std::execution::par is not saying that the algorithm
implementation must call the lambda concurrently, only that it may
do so. It is always valid for the algorithm to call the lambda sequentially.

We want to take the same approach with the ManySender / ManyReceiver
contract to allow a ManySender to query from the ManyReceiver
what the execution constraints for calling its set_next() method
are. Then the sender can make a decision about the best strategy to
use when calling set_next().

To do this, we define a get_execution_policy() CPO that can be invoked,
passing the receiver as the argument, and have it return the execution
policy that specifies how the receiver’s set_next() method is allowed
to be called.

For example, a receiver that supports concurrent calls to set_next()
would customise get_execution_policy() for its type to return
either unifex::par or unifex::par_unseq.

A sender that has multiple threads available can then call
get_execution_policy(receiver), see that it allows concurrent execution
and distribute the calls to set_next() across available threads.

TypedManySender

利用TypedSender,类型暴露了类型别名,这样允许sender的消费者查询将使用何种类型调用receiverset_value()set_error() 方法。
With the TypedSender concept, the type exposes type-aliases that allow
the consumer of the sender to query what types it is going to invoke a
receiver’s set_value() and set_error() methods with.

A TypedManySender concept similarly extends the ManySender
concept, requiring the sender to describe the types it will invoke set_next(),
via a next_types type-alias, in addition to the value_types and error_types
type-aliases required by TypedSender.

Note that this requirement for a TypedManySender to provide the next_types
type-alias means that the TypedSender concept, which only need to provide the
value_types and error_types type-aliases, does not subsume the TypedManySender
concept, even though Sender logically subsumes the ManySender concept.

Streams

Stream是另一个惰性求值的异步序列,只有当消费者调用next()方法请求下一个值的时候才会按需返回一个求值的sender。消费者一次可能只请求一个值,同时必须等待上一个值被求出后才能进行下一个值的请求。
Stream包含两个方法:

  • next(stream) - Returns a Sender that produces the next value.
    The sender delivers one of the following signals to the receiver
    passed to it:
    • set_value() if there is another value in the stream,
    • set_done() if the end of the stream is reached
    • set_error() if the operation failed
  • cleanup(stream) - Returns a Sender that performs async-cleanup
    operations needed to unsubscribe from the stream.
    • Calls set_done() once the cleanup is complete.
    • Calls set_error() if the cleanup operation failed.

Note that if next() is called then it is not permitted to call
next() again until that sender is either destroyed or has been
started and produced a result.

If the next() operation completes with set_value() then the
consumer may either call next() to ask for the next value, or
may call cleanup() to cancel the rest of the stream and wait
for any resources to be released.

If a next() operation has ever been started then the consumer
must ensure that the cleanup() operation is started and runs
to completion before destroying the stream object.

If the next() operation was never started then the consumer
is free to destroy the stream object at any time.

对比ManySender

以下列举和ManySender对比的一些不同点:

  • The consumer of a stream may process the result asynchronously and can
    defer asking for the next value until it has finished processing the
    previous value.
    • A ManySender can continue calling set_next() as soon as the
      previous call to set_next() returns.
    • A ManySender has no mechanism for flow-control. The ManyReceiver
      must be prepared to accept as many values as the ManySender sends
      to it.
  • The consumer of a stream may pass a different receiver to handle
    each value of the stream.
    • ManySender sends many values to a single receiver.
    • Streams sends a single value to many receivers.
  • A ManySender has a single cancellation-scope for the entire operation.
    The sender can subscribe to the stop-token from the receiver once at the
    start of the operation.
    • As a stream can have a different receiver that will receiver each element
      it can potentially have a different stop-token for each element and so
      may need to subscribe/unsubscribe stop-callbacks for each element.

兼容协程

When a coroutine consumes an async range, the producer is unable to send
the next value until the coroutine has suspended waiting for it. So an
async range must wait until a consumer asks for the next value before
starting to compute it.

A ManySender type that continuously sends the next value as soon as
the previous call to set_value() returns would be incompatible with
a coroutine consumer, as it is not guaranteed that the coroutine consumer
would necessarily have suspended, awaiting the next value.

A stream is compatible with the coroutine model of producing a stream
of values. For example the cppcoro::async_generator type allows the
producer to suspend execution when it yields a value. It will not resume
execution to produce the next value until the consumer finishes processing
the previous value and increments the iterator.

设计权衡

Stream设计设计需要为请求流中的每个值构造一个新的操作状态对象。如果我们对每一个值都这么做,那么相比于ManySender可以对一个单一receriver调用多次,状态机的setup/teardown可能会耗很多资源。
然而这个方式更适配协程模型。
It separates the operations of cancelling the stream in-between requests
for the next element (ie. by calling cleanup() instead of next())
from the operaiton of interrupting an outstanding request to next() using
the stop-token passed to that next() operation.

消费者大概率不会在上一次next()完成之前再调用next()cleanup()。这意味着 cleanup()的实现不需要线程同步,因为这些调用天然是顺序执行。

Scheduler

Scheduler是一个轻量的handle,表示那个在调度work上的执行上下文。
A scheduler is a lightweight handle that represents an execution context
on which work can be scheduled.
scheduler提供一个单一操作schedule()且是一个异步操作(如返回一个sender)。当操作开始的时候,把一个work放进队列;当操作完成的时候,把work从执行上下文中退出队列。
如果scheduler操作成功完成(即完成时调用set_value()),执行上下文中的scheduler保证这个操作完成并且调用一个只带receiver参数的set_value()方法。
If the schedule operation completes successfully (ie. completion is signalled
by a call to set_value()) then the operation is guaranteed to complete on
the scheduler’s associated execution context and the set_value() method
is called on the receiver with no value arguments.
ie. the schedule operation is a “sender of void”.

If the schedule operation completes with set_done() or set_error() then
it is implementation defined which execution context the call is performed
on.
schedule()操作能
The schedule() operation can therefore be used to execute work on the
scheduler’s associated execution context by performing the work you want to
do on that context inside the set_value() call.

Scheduler定义如下:

namespace unifex
{
  // The schedule() CPO
  inline constexpr unspecified schedule = {};

  // The scheduler concept.
  template<typename T>
  concept scheduler =
    std::is_nothrow_copy_constructible_v<T> &&
    std::is_nothrow_move_constructible_v<T> &&
    std::destructible<T> &&
    std::equality_comparable<T> &&
    requires(const T cs, T s) {
      schedule(cs); // TODO: Constraint this returns a sender of void.
      schedule(s);
    };
}

Sub-schedulers

If you want to schedule work back on the same execution context then you
can use the schedule_with_subscheduler() function instead of schedule()
and this will call set_value() with a Scheduler that represents the current
execution context.

e.g. on a thread-pool the sub-scheduler might represent a scheduler that lets
you directly schedule work onto a particular thread rather than to the thread
pool as a whole.

This allows the receiver to schedule additional work onto the same execution
context/thread if desired.

The default implementation of schedule_with_subscheduler() just produces
a copy of the input scheduler as its value.

TimeScheduler

TimeScheduler扩展了Scheduler,能够调度work在某个时刻或之后进行,而无需立即尽快进行。
这将增加以下功能:

  • typename TimeScheduler::time_point
  • now(ts) -> time_point
  • schedule_at(ts, time_point) -> sender_of<void>
  • schedule_after(ts, duration) -> sender_of<void>

Instead, the current time is obtained from the scheduler itself by calling the now()
customisation point, passing the scheduler as the only argument.

This allows tighter integration between scheduling by time and the progression of time
within a scheduler. e.g. a time scheduler only needs to deal with a single time source
that it has control over. It doesn’t need to be able to handle different clock sources
which may progress at different rates.

now()操作作为TimeScheduler操作一部分,这样可以实现一些包含状态时钟的scheduler,例如virtual time scheduler,可以手动提前时间来跳过空闲时间段
Having the now() operation as an operation on the TimeScheduler allows implementations of schedulers that contain stateful clocks such as virtual
time schedulers which can manually advance time to skip idle periods. e.g. in unit-tests.

namespace unifex
{
  // TimeScheduler CPOs
  inline constexpr unspecified now = unspecified;
  inline constexpr unspecified schedule_at = unspecified;
  inline constexpr unspecified schedule_after = unspecified;

  template<typename T>
  concept time_scheduler =
    scheduler<T> &&
    requires(const T scheduler) {
      now(scheduler);
      schedule_at(scheduler, now(scheduler));
      schedule_after(scheduler, now(scheduler) - now(scheduler));
    };
}

TimePoint

我们用TimePoint对象表示在一个TimeScheduler对象时间线上的某一时刻。这里的time_point可以是一个std::chrono::time_pointTimePoint提供一个兼容std::chrono::time_point的子集。实际上,它不必提供clock类型因此也不必提供一个静态的clock::now()方法获取当前时间。如果需要当前时间,则通过TimeScheduler对象中now()CPO获取。
你能够计算两个time-point之间的差去生成一个std::chrono::duration,也能够对一个time-point加减一个std::chrono::duration去生成一个新的time-point

namespace unifex
{
  template<typename T>
  concept time_point =
    std::regular<T> &&
    std::totally_ordered<T> &&
    requires(T tp, const T ctp, typename T::duration d) {
      { ctp + d } -> std::same_as<T>;
      { ctp - d } -> std::same_as<T>;
      { ctp - ctp } -> std::same_as<typename T::duration>;
      { tp += d } -> std::same_as<T&>;
      { tp -= d } -> std::same_as<T&>;
    };

按语:This concept is only checking that you can add/subtract the same duration
type returned from operator-(T, T). Ideally we’d be able to check that this
type supports addition/subtraction of any std::chrono::duration instantiation.

StopToken

Unifex库使用了StopToken机制来支持异步的cancellation操作,这样cancellation就可以并发执行。stop-token是一个token,
A stop-token is a token that can be passed to an operation and that can be later
used to communicate a request for that operation to stop executing, typically
because the result of the operation is no longer needed.

在C++20, std::stop_token类型已经加入标准库。而在Unifex库中,我们还提供了一些其他类型的stop-token,在某些情况下允许有更高效的实现。
In C++20 a new std::stop_token type has been added to the standard library.
However, in Unifex we also wanted to support other kinds of stop-token that
permit more efficient implementations in some cases. For example, to avoid the
need for reference-counting and heap-allocation of the shared-state in cases
where structured concurrency is being used, or to avoid any overhead altogether
in cases where cancellation is not required.

To this end, Unifex operations are generally written against a generic
StopToken concept rather than against a concrete type, such as std::stop_token.

The StopToken concept defines the end of a stop-token passed to an async
operation. It does not define the other end of the stop-token that is used
to request the operation to stop.

namespace unifex
{
  struct __stop_token_callback_archetype {
    // These have no definitions.
    __stop_token_callback_archetype() noexcept;
    __stop_token_callback_archetype(__stop_token_callback_archetype&&) noexcept;
    __stop_token_callback_archetype(const __stop_token_callback_archetype&) noexcept;
    ~__stop_token_callback_archetype();
    void operator()() noexcept;
  };

  template<typename T>
  concept stop_token_concept =
    std::copyable<T> &&
    std::is_nothrow_copy_constructible_v<T> &&
    std::is_nothrow_move_constructible_v<T> &&
    requires(const T token) {
      typename T::template callback_type<__stop_token_callback_archetype>;
      { token.stop_requested() ? (void)0 : (void)0 } noexcept;
      { token.stop_possible() ? (void)0 : (void)0 } noexcept;
    } &&
    std::destructible<
      typename T::template callback_type<__stop_token_callback_archetype>> &&
    std::is_nothrow_constructible_v<
      typename T::template callback_type<__stop_token_callback_archetype>,
      T, __stop_token_callback_archetype> &&
    std::is_nothrow_constructible_v<
      typename T::template callback_type<__stop_token_callback_archetype>,
      const T&, __stop_token_callback_archetype>;
}

按语:C++20的std::stop_token 类型实际上并不是完整的concept,因此它不能嵌套callback_type模板类型别名。我们可以定义一些函数去构造stop-callback对象代替使用一个嵌套类型别名。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
好的,编写异步并发接口可以使用C++11标准中的`std::async`,它可以异步地执行一个函数,并且返回一个`std::future`对象,可以在未来的某个时刻获取异步操作的结果。 下面是一个示例,演示如何使用`std::async`实现异步并发接口: ```c++ #include <iostream> #include <future> #include <chrono> // 异步执行的函数 int asyncFunction(int x) { std::cout << "asyncFunction started" << std::endl; std::this_thread::sleep_for(std::chrono::seconds(5)); std::cout << "asyncFunction return " << x * 2 << std::endl; return x * 2; } int main() { // 使用std::async异步执行函数 std::future<int> result = std::async(std::launch::async, asyncFunction, 10); // 在主线程中执行其他操作 std::cout << "Main thread do something else" << std::endl; // 等待异步操作完成 int res = result.get(); std::cout << "Async function return " << res << std::endl; return 0; } ``` 在上面的示例中,`std::async`函数异步执行了`asyncFunction`函数,并且使用`std::future`对象`result`保存了异步操作的结果。在主线程中,我们可以执行其他操作,然后通过调用`result.get()`函数获取异步操作的结果。 需要注意的是,`std::async`函数有三种启动方式: - `std::launch::async`:异步启动任务,并且立即返回。 - `std::launch::deferred`:延迟启动任务,只有在调用`std::future`对象的`get`函数时才会执行任务。 - `std::launch::async | std::launch::deferred`:让系统自行选择启动方式。 在使用`std::async`时,需要根据具体的情况选择合适的启动方式,以达到最好的性能和效果。 此外,C++11标准库中还提供了一些其他的并发库,例如`std::thread`、`std::mutex`、`std::condition_variable`等,可以实现更为复杂和灵活的并发操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值