forked from HowardHinnant/papers
-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathdisentangling-schedulers-and-executors.html
480 lines (434 loc) · 19.6 KB
/
disentangling-schedulers-and-executors.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Disentangling schedulers and executors</title>
<style>
p {text-align:justify}
li {text-align:justify}
blockquote.note
{
background-color:#E0E0E0;
padding-left: 15px;
padding-right: 15px;
padding-top: 1px;
padding-bottom: 1px;
}
ins {color:#00A000}
del {color:#A00000}
</style>
</head>
<body>
<address align=right>
Document number: P2235R0
<br/>
Audience: LEWG, SG1
<br/>
<br/>
<a href="mailto:[email protected]">Ville Voutilainen</a><br/>
On behalf of SFS (Finland)<br/>
2020-10-15<br/>
</address>
<hr/>
<h1 align=center>Disentangling schedulers and executors</h1>
<h2>Credits</h2>
<p>
Many many thanks to Tomasz Kamiński for a very high-quality
technical review of this paper.
</p>
<h2>Abstract</h2>
<p>
This proposal proposes to simplify the design of schedulers and executors,
providing a single conversion facility from an <code>executor</code> to
a <code>scheduler</code>,
removing the non-scheduler/sender/receiver facilities from
<code>schedule</code> and <code>connect</code>.
This proposal *also* removes the ability to
<code>execute</code> on a sender.
</p>
<h3>Why?</h3>
<p>
Schedulers and executors are apples and oranges. Schedulers, senders,
and receivers establish a generic protocol that is knit together
so as to facilitate generic programming and algorithms that can
operate within the framework of that protocol, transforming in various
ways what senders and receivers do. Executors do not work with this
protocol, because they do not provide two thirds of it; namely,
they do not provide any means to ever invoke set_error or set_done.
</p>
<p>
Therefore treating an executor as a scheduler is akin to a lossy
conversion. The conversion is sufficiently lossy that it would be unwise
if it Just Happens without any indication in source code. That's why
multiple reviewers have suggested that that conversion should be
explicit. This proposal makes it explicit.
</p>
<p>
Once we accept that an executor shouldn't just be treated as a scheduler,
we also realize that there's no point in providing support for treating
a sender-of-void as an executor in the execution::execute() CPO.
Such an operation is every bit as lossy as treating an executor as
a scheduler. It should be likewise explicit, and there's no reason
to provide it in the fundamental building blocks; a separate algorithm
can be provided that connects a plain invocable to a sender.
</p>
<p>
In a slightly different vein, a scheduler or a sender should not be treated
as an executor either. That's not like a lossy conversion, but it's
like invoking a function with a wide contract, and then having it be
overridden by a function with a narrow contract, and thus the caller
expectations of a wide contract aren't met. The semantics of execute()
are intentionally rather free-form. There is no generic protocol that
executors conform to, but senders and receivers do have such a protocol,
and schedulers *will* conform to it, causing naive users of execute
on senders to shoot themselves in the foot. Furthermore, that protocol
will intercept attempts to implement custom protocols solely in the operator()
of the user-provided runnable; the handling of scheduling errors
will happen in schedulers before that runnable is invoked,
thwarting the attempts to implement something custom and breaking
expectations for a custom protocol.
</p>
<p>
These apples and oranges don't mix. In either direction. Cross-pollination
attempts should be explicit, visible, greppable, and done with utmost care.
</p>
<h3>How?</h3>
<ol>
<li><code>schedule()</code> operates on schedulers only.</li>
<li><code>execute()</code> is a customization point that operates on an executor only.</li>
<li>All senders&receivers-related operations, like <code>connect()</code>,
operate on senders&receivers only.</li>
<li>There is a conversion function that takes an executor, and converts it
to a scheduler. The working name for this function
is <code>make_scheduler_from_executor()</code>, because that's what it does.</li>
<li>Separate algorithms, most likely and preferrably with names other
than 'execute', can be provided in e.g. <a href="https://wg21.link/p1897">P1897</a> to allow straightforward fire-and-forget on schedulers
or senders.
</ol>
<h2>Rumination</h2>
<h3>The current design is complex, confusing, and error-prone</h3>
<p>
The current design allows execute() on anything, schedule() on anything,
connect() on anything. But that makes no sense; execute() is okay
as a one-way fire-and-forget mechanism when no particular error-handling
semantics are expected, but schedule() on an executor
tosses in the wind the <code>set_error/set_done</code> parts. Those
parts are absolutely necessary for some senders&receivers use
cases to work. Padding them in with operations that e.g.
<code>terminate</code> shouldn't just happen willy-nilly. Otherwise
it becomes impossible to reason about generic code, and to trust
that senders are actual senders, receivers are actual receivers,
and that the senders&receivers protocol actually works.
As far as execute() goes, the story is similar. What we have
right now will terminate() a program if there is a scheduling
error. This becomes ever more likely when programs and applications
using these facilities grow more complex; simple schedulers&senders&receivers combine into more complicated ones, increasing the chance
that scheduling errors will not be just allocation failures, but
something much more likely, such as i/o errors. Thus, simply executing
on a sender that has terminate() called from its receivers' set_done/set_error
is a massive footgun. It's also a massive footgun if we make executing
on a sender actually work properly with the sender&receiver protocol,
since transitioning to the executor world will again drop that protocol.
</p>
<p>
This proposal makes the design clearer. In order to make the jump
from executors (which don't conform to any protocol) to senders&receivers,
there is exactly one conversion operation that crosses that bridge
(or rather jumps over the river when there is no bridge)
Otherwise, the separate worlds are kept separate. Similarly,
the jump from senders&receivers to executors doesn't introduce
surprises when an execute function would do drastically different
things depending on what it's invoked on. This avoids a vector<bool>
problem that we currently have in the design of P0443. Any jumps
back and forth between the worlds don't introduce surprises either.
</p>
<p>
Once in a senders&receivers world, all operations related to them
work as expected. The cross-river jump mentioned before is easy
to find, and is greppable. One doesn't need to look at all operations,
including <code>schedule</code> and <code>connect</code> and wonder
whether they're operating on a scheduler and a sender or perhaps
an executor and an invocable.
</p>
<p>
Similarily, once in an executor world, the operations related to an executor
work as expected. In order to have a protocol, or to deal with
executor-specific means of handling scheduling errors, that needs to be
programmed explicitly, instead of having a sender introduce a protocol
where it's perhaps not expected or even desired.
</p>
<p>
It's still perfectly plausible to provide a function that queues
a plain invocable to run as an error-ignoring receiver of a sender.
But that's a separate named algorithm, not an overload of <code>connect</code>. It's still possible to indirectly call <code>schedule</code> on an executor,
but that's <code>schedule(make_scheduler_from_executor(foo))</code>,
not <code>schedule(foo)</code>.
</p>
<h3>The cross-concept bridging the current design attempts is not
useful for programmers</h3>
<p>
We are going to see a fair amount of generic algorithms that
operate on senders. These algorithms will provide decorators
that change (sometimes extend, sometimes transform) what
senders, receivers, and operation_states do. They will be lego-like
building blocks that introduce a particular form of aspect-oriented
programming into the world of senders and receivers.
</p>
<p>
Programmers, even library programmers, are expected to be mostly using
such algorithms, rather than using things like <code>connect()</code>
directly. These algorithms will be constrained to accept
<code>schedulers, </code><code>senders</code>
and <code>receivers</code>. They will not be constrained to accept
both <code>schedulers</code> and <code>executors</code> even if the
cross-concept bridging that's currently in P0443 might make using
them on <code>executors</code> well-formed.
</p>
<p>
And, again, code that uses executors will probably do so with the expectation
that there is a particular custom protocol in play, or no protocol at all.
Implicitly introducing the senders&receivers protocol into code that has
the aforementioned expectation(s) can be a massively breaking change.
</p>
<p>
We need a clear, simple, and understandable story of what our concepts
are and what they do. That means we shouldn't encourage every programmer
to deal with a <code>scheduler_or_executor</code> concept that's basically
a disjunction of the two concepts. Executors aren't schedulers, so let's
not pretend that they are. For those who insist on treating an executor
as a scheduler, we provide them with a very explicit conversion function.
Executors are a perfectly reasonable family of types in and of themselves,
types that provide a less strict protocol than schedulers do; that's fine,
those types can be used by audiences who have no use for the
senders-and-receivers protocol. But as long as those audiences don't
have a use for the senders-and-receivers protocol, we make the conversion
from an executor to a scheduler _deliberately_ ugly. And as long as those
audiences don't have a use for the senders-and-receivers protocol, we
don't inflict it on those audiences implicitly, either.
</p>
<h3>All that considered, is make_scheduler_from_executor fundamental for P0443?</h3>
<p>
That would be no. We could just as well do something like it separately in P1897. It's proposed here to provide a better overall view of the cross-concept
picture, but doesn't strictly need to be in P0443.
</p>
<h3>What about bridging to the other direction? Why is there no make_executor_from_sender proposed here?</h3>
<p>
Such a facility is fraught with peril. A sender will report scheduling errors.
For an executor, those errors have nowhere to go. Thus they would
be unhandled errors, and would need to be intercepted and most likely
call terminate(). What makes it worse is that you can take a sender,
and apply an error-handling algorithm on top of it, which would otherwise
intercept the set_error calls and do what that algorithm defines.
But a hypothetical make_executor_from_sender wouldn't know that, so it would
need to intercept set_error again, on top, and those intercepts would cause termination
even if an algorithm that would make those intercepts unnecessary
has already been applied.
</p>
<h3>All that considered, do we still need both concepts, executor and scheduler?</h3>
<p>
Yes, we do. Schedulers, senders, and receivers establish a scalable
protocol that can deal with errors between task submission and
callback of the invokable, and provide proper cleanup. However,
if you need something completely different from that protocol,
an executor may be a better fit.
And to be able to write multiple different executors, they should still
have a common API.
</p>
<h2>Changes to P0443</h2>
<p>
In 2.2.3.4 execution::execute, modify the second paragraph and remove
the third bullet:
<p>
<p>
For some subexpressions e and f, let E be decltype((e)) and let F be decltype((f)). The expression execution::execute(e, f) is ill-formed if F does not model invocable, or if E does not model<del> either</del> executor <del>or sender</del>. Otherwise, it is expression-equivalent to:
</p>
<pre><ul>
<li>e.execute(f), if that expression is valid. If the function selected does not execute the function object f on the executor e, the program is ill-formed with no diagnostic required.</li>
<li>Otherwise, execute(e, f), if that expression is valid, with overload resolution performed in a context that includes the declaration
void execute();
and that does not include a declaration of execution::execute. If the function selected by overload resolution does not execute the function object f on the executor e, the program is ill-formed with no diagnostic required.</li>
<li><del>Otherwise, execution::submit(e, as-receiver<remove_cvref_t<F>, E>{forward<F>(f)}) if</del>
<ul>
<li><del>F is not an instance of as-invocable<R,E'> for some type R where E and E' name the same type ignoring cv and reference qualifiers, and</del></li>
<li><del>invocable<remove_cvref_t<F>&> && sender_to<E, as-receiver<remove_cvref_t<F>, E>> is true</del></li></ul>
<del>where as-receiver is some implementation-defined class template equivalent to:</del>
<del>template<class F, class&gT;</del>
<del>struct as-receiver {</del>
<del>F f_;</del>
<del>void set_value() noexcept(is_nothrow_invocable_v<F&>) {</del>
<del>invoke(f_);</del>
<del>}</del>
<del>template<class E></del>
<del>[[noreturn]] void set_error(E&&) noexcept {</del>
<del>terminate();</del>
<del>}</del>
<del>void set_done() noexcept {}</del>
<del>};</del>
</li>
</ul>
</pre>
In 2.2.3.5 execution::connect, remove the third bullet:
</p>
<ul>
<del><li>Otherwise, as-operation{s, r}, if
<ul>
<li>r is not an instance of as-receiver<F, S'> for some type F where S and S' name the same type ignoring cv and reference qualifiers, and</li>
<li>receiver_of<R> && executor-of-impl<remove_cvref_t<S>, as-invocable<remove_cvref_t<R>, S>> is true,</li>
</ul>
<p>where as-operation is an implementation-defined class equivalent to</p>
<pre>
struct as-operation {
remove_cvref_t<S> e_;
remove_cvref_t<R> r_;
void start() noexcept try {
execution::execute(std::move(e_), as-invocable<remove_cvref_t<R>, S>{r_});
} catch(...) {
execution::set_error(std::move(r_), current_exception());
}
};
</pre>
<p>and as-invocable is a class template equivalent to the following:</p>
<pre> template<class R, class>
struct as-invocable {
R* r_;
explicit as-invocable(R& r) noexcept
: r_(std::addressof(r)) {}
as-invocable(as-invocable && other) noexcept
: r_(std::exchange(other.r_, nullptr)) {}
~as-invocable() {
if(r_)
execution::set_done(std::move(*r_));
}
void operator()() & noexcept try {
execution::set_value(std::move(*r_));
r_ = nullptr;
} catch(...) {
execution::set_error(std::move(*r_), current_exception());
r_ = nullptr;
}
};</pre></li></del>
<li>Otherwise, execution::connect(s, r) is ill-formed.</li>
</ul>
<p>
In 2.2.3.8 execution::schedule, remove the third bullet:
</p>
<ul>
<del><li>Otherwise, as-sender<remove_cvref_t<S>>{s} if S satisfies executor, where as-sender is an implementation-defined class template equivalent to
<pre> template<class E>
struct as-sender {
private:
E ex_;
public:
template<template<class...> class Tuple, template<class...> class Variant>
using value_types = Variant<Tuple<>>;
template<template<class...> class Variant>
using error_types = Variant<std::exception_ptr>;
static constexpr bool sends_done = true;
explicit as-sender(E e) noexcept
: ex_((E&&) e) {}
template<class R>
requires receiver_of<R>
connect_result_t<E, R> connect(R&& r) && {
return execution::connect((E&&) ex_, (R&&) r);
}
template<class R>
requires receiver_of<R>
connect_result_t<const E &, R> connect(R&& r) const & {
return execution::connect(ex_, (R&&) r);
}
};
</pre></li></del>
<li>Otherwise, execution::schedule(s) is ill-formed.</li>
</ul>
<p>
After 2.2.3, add a new section:
<blockquote>
<p>
<ins>2.x execution::make_scheduler_from_executor
</ins>
</p>
<p><ins>The behavior of a program that adds specializations for <code>make_scheduler_from_executor</code> is undefined.</ins>
</p>
<p><ins><code>template <class E> scheduler auto make_scheduler_from_executor(E&& executor);</code></ins></p>
<p><ins>Constraints: remove_cvref_t<E> satisfies execution::executor.</ins></p>
<p><ins>Preconditions: remove_cvref_t<E> models execution::executor.</ins></p>
<p><ins>Returns:</ins></p>
<p>
<ins>a <code>scheduler</code>, so that calling <code>execution::schedule(s)</code> on that
<code>scheduler </code>s returned from <code>make_scheduler_from_executor</code>
is expression-equivalent to</ins>
</p>
<p>
<ins>as-sender<remove_cvref_t<S>>{executor}, where as-sender is an implementation-defined class template equivalent to</ins></p>
<ins><pre> template<class E>
struct as-sender {
private:
E ex_;
public:
template<template<class...> class Tuple, template<class...> class Variant>
using value_types = Variant<Tuple<>>;
template<template<class...> class Variant>
using error_types = Variant<std::exception_ptr>;
static constexpr bool sends_done = true;
explicit as-sender(E e) noexcept
: ex_((E&&) e) {}
template<class R>
requires receiver_of<R>
auto connect(R&& r) && {
return as-operation<E, remove_cvref_t<R>>{(E&&)ex_, (R&&) r};
}
template<class R>
requires receiver_of<R>
auto connect(R&& r) const & {
return as-operation<E, remove_cvref_t<R>>{ex_, (R&&) r};
}
};
</pre></ins>
<p><ins>where as-operation is an implementation-defined class template equivalent to</p>
<ins><pre>
template <class E, class R>
struct as-operation {
E e_;
R r_;
void start() noexcept try {
execution::execute(std::move(e_), as-invocable<R, E>{r_});
} catch(...) {
execution::set_error(std::move(r_), current_exception());
}
};
</pre></ins>
<p><ins>and as-invocable is an implementation-defined class template equivalent to the following:</p></ins>
<ins><pre> template<class R, class>
struct as-invocable {
R* r_;
explicit as-invocable(R& r) noexcept
: r_(std::addressof(r)) {}
as-invocable(as-invocable && other) noexcept
: r_(std::exchange(other.r_, nullptr)) {}
~as-invocable() {
if(r_)
execution::set_done(std::move(*r_));
}
void operator()() & noexcept try {
execution::set_value(std::move(*r_));
r_ = nullptr;
} catch(...) {
execution::set_error(std::move(*r_), current_exception());
r_ = nullptr;
}
};</pre></ins>
</p>
</blockquote>
<h2>What This Buys</h2>
<p>
The user can no longer pass an executor to <code>schedule()</code>
or <code>connect()</code>, and have the code be well-formed, but with broken
runtime semantics. Such code is extremely likely to be a mistake.
</p>
<p>
The user can no longer pass a sender to <code>execute()</code>,
and have the code be well-formed, but with broken
runtime semantics. Such code is extremely likely to be a mistake.
</p>
</body>
</html>