目录

  • 定制点机制
  • Concept
  • Sender/Receiver
    • 启动一个异步操作
    • operation-state的生命周期
    • 异步操作完成
    • Receiver
      • 上下文信息
    • Sender
    • TypedSender
    • OperationState
  • ManySender/ManyReceiver
    • Sender vs. ManySender
    • 顺序执行 vs. 并发执行
    • TypedManySender
  • Streams
    • 对比ManySender
    • 兼容协程
    • 设计权衡
  • Scheduler
    • Sub-schedulers
  • TimeScheduler
  • TimePoint
  • StopToken

成为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 implementationtemplate<typename T>friend bool tag_invoke(example_cpo, const T& x) noexcept {return false;}template<typename T>bool operator()(const T& x) constnoexcept(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
{// CPOsinline 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 CPOsinline 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 operationinline 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>requiresvalue_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>requiresnext_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() CPOinline 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 CPOsinline 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对象代替使用一个嵌套类型别名。

unifex:C++现代异步模型先导相关推荐

  1. 【NIO】异步模型之Callback -- 封装NIO

    在[NIO]IO模型,这节课中,我们提到了5种IO模型.第四种,SIGIO一般都是在进程间使用信号通讯的时候的手段,在Java中不是很适用,我就不深入去讲了.第五种,linux 服务器上的典型代表是 ...

  2. 【网络编程】之五、异步模型

    注:本文部分转载 一:select模型 二:WSAAsyncSelect模型 三:WSAEventSelect模型 四:Overlapped I/O 事件通知模型 五:Overlapped I/O 完 ...

  3. 面试必会系列 - 5.1 网络BIO、NIO、epoll,同步/异步模型、阻塞/非阻塞模型,你能分清吗?

    本文已收录至 Github(MD-Notes),若博客中图片模糊或打不开,可以来我的 Github 仓库,包含了完整图文:https://github.com/HanquanHq/MD-Notes,涵 ...

  4. 以两种异步模型应用案例,深度解析Future接口

    摘要:本文以实际案例的形式分析了两种异步模型,并从源码角度深度解析Future接口和FutureTask类. 本文分享自华为云社区<[精通高并发系列]两种异步模型与深度解析Future接口(一) ...

  5. java 异步模型_Java IO编程全解(三)——伪异步IO编程

    为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化,后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远 ...

  6. 微服务升级_SpringCloud Alibaba工作笔记0005---spring gateway非阻塞异步模型

    技术交流QQ群[JAVA,C++,Python,.NET,BigData,AI]:170933152

  7. c# 三种异步编程模型EAP(*)、 APM(*)和 TPL

    为什么80%的码农都做不了架构师?>>>    EAP 是 Event-based Asynchronous Pattern(基于事件的异步模型)的简写 优点是简单,缺点是当实现复杂 ...

  8. 简单地使用线程之一:使用异步编程模型

    .NetFramework的异步编程模型从本质上来说是使用线程池来完成异步的任务,异步委托.HttpWebRequest等都使用了异步模型. 这里我们使用异步委托来说明异步编程模型. 首先,我们来明确 ...

  9. 并行开发 —— 第六篇 异步编程模型

    在.net里面异步编程模型由来已久,相信大家也知道Begin/End异步模式和事件异步模式,在task出现以后,这些东西都可以被task包装 起来,可能有人会问,这样做有什么好处,下面一一道来. 一: ...

  10. 领导者/追随者(Leader/Followers)模型和半同步/半异步(half-sync/half-async)模型

    领导者/追随者(Leader/Followers)模型和半同步/半异步(half-sync/half-async)模型都是常用的客户-服务器编程模型.这几天翻了些文章,发现对领导者/追随者模型说的比较 ...

最新文章

  1. 找到一款牛B的vim插件
  2. ThinkPHP5 清除runtime缓存文件
  3. HTML 5 样式指南和代码约定
  4. 蓝奏云文件上传php源码_蓝奏云客户端 v0.3.1,第三方蓝奏网盘电脑版
  5. 如何使用ABAP把数字转换成单词
  6. QT学习——Tcp客户端通信(本地回环)
  7. android mina分析,Android与Mina整合
  8. highgui java opencv_OpenCV在C Qt应用程序中的highgui
  9. TMS320F28335项目开发记录1_CCS的使用介绍
  10. 小程序中的多表联合查询
  11. VMware安装CentOS 7.0 Fail to start media check on /dev/sr0
  12. EDGE浏览器打开网页缓慢解决
  13. 报错:信息:INFO: Error parsing HTTP request header
  14. MacOS修改Hosts文件
  15. 五猴分桃问题的数学解
  16. 5.Abp vNext 地磅无人值守 微信小程序
  17. scikit-learn学习系列 - 广义线性模型
  18. 建材企业ERP 重在规划(转)
  19. 图片怎么转PDF文件?免费图片转PDF方法推荐
  20. 爱因斯坦经典逻辑推理题

热门文章

  1. linux libodbc.so.1,关于C#:Testprintenv:加载共享库时出错:libodbc.so.1:无法打开共享对象文件...
  2. 手机android系统锁了怎么解,安卓手机解锁图案忘了怎么办?手机解锁密码忘了的解决办法...
  3. Vue子组件与父组件(看了就会)
  4. 取消参考文献自动编号_取消参考文献引用 - 卡饭网
  5. 操作系统µC/OS的故事,最终Micrium被Silicon Labs收购
  6. 分布式任务调度相关介绍
  7. 共享计算机怎么连,电脑如何连接共享文件
  8. android 剪贴板增强工具,ClipX - 超级实用的小巧剪贴板增强工具
  9. 飞机大战,坦克大战源码、简单仿记事本、错题本源码及笔记
  10. excel字符串和单元格拼接_单元格引用问题(引用的单元格与字符串变量连接)...