|
8 | 8 | // option. This file may not be copied, modified, or distributed
|
9 | 9 | // except according to those terms.
|
10 | 10 |
|
11 |
| -//! Task death: asynchronous killing, linked failure, exit code propagation. |
| 11 | +/*! |
| 12 | +
|
| 13 | +Task death: asynchronous killing, linked failure, exit code propagation. |
| 14 | +
|
| 15 | +This file implements two orthogonal building-blocks for communicating failure |
| 16 | +between tasks. One is 'linked failure' or 'task killing', that is, a failing |
| 17 | +task causing other tasks to fail promptly (even those that are blocked on |
| 18 | +pipes or I/O). The other is 'exit code propagation', which affects the result |
| 19 | +observed by the parent of a task::try task that itself spawns child tasks |
| 20 | +(such as any #[test] function). In both cases the data structures live in |
| 21 | +KillHandle. |
| 22 | +
|
| 23 | +I. Task killing. |
| 24 | +
|
| 25 | +The model for killing involves two atomic flags, the "kill flag" and the |
| 26 | +"unkillable flag". Operations on the kill flag include: |
| 27 | +
|
| 28 | +- In the taskgroup code (task/spawn.rs), tasks store a clone of their |
| 29 | + KillHandle in their shared taskgroup. Another task in the group that fails |
| 30 | + will use that handle to call kill(). |
| 31 | +- When a task blocks, it turns its ~Task into a BlockedTask by storing a |
| 32 | + the transmuted ~Task pointer inside the KillHandle's kill flag. A task |
| 33 | + trying to block and a task trying to kill it can simultaneously access the |
| 34 | + kill flag, after which the task will get scheduled and fail (no matter who |
| 35 | + wins the race). Likewise, a task trying to wake a blocked task normally and |
| 36 | + a task trying to kill it can simultaneously access the flag; only one will |
| 37 | + get the task to reschedule it. |
| 38 | +
|
| 39 | +Operations on the unkillable flag include: |
| 40 | +
|
| 41 | +- When a task becomes unkillable, it swaps on the flag to forbid any killer |
| 42 | + from waking it up while it's blocked inside the unkillable section. If a |
| 43 | + kill was already pending, the task fails instead of becoming unkillable. |
| 44 | +- When a task is done being unkillable, it restores the flag to the normal |
| 45 | + running state. If a kill was received-but-blocked during the unkillable |
| 46 | + section, the task fails at this later point. |
| 47 | +- When a task tries to kill another task, before swapping on the kill flag, it |
| 48 | + first swaps on the unkillable flag, to see if it's "allowed" to wake up the |
| 49 | + task. If it isn't, the killed task will receive the signal when it becomes |
| 50 | + killable again. (Of course, a task trying to wake the task normally (e.g. |
| 51 | + sending on a channel) does not access the unkillable flag at all.) |
| 52 | +
|
| 53 | +Why do we not need acquire/release barriers on any of the kill flag swaps? |
| 54 | +This is because barriers establish orderings between accesses on different |
| 55 | +memory locations, but each kill-related operation is only a swap on a single |
| 56 | +location, so atomicity is all that matters. The exception is kill(), which |
| 57 | +does a swap on both flags in sequence. kill() needs no barriers because it |
| 58 | +does not matter if its two accesses are seen reordered on another CPU: if a |
| 59 | +killer does perform both writes, it means it saw a KILL_RUNNING in the |
| 60 | +unkillable flag, which means an unkillable task will see KILL_KILLED and fail |
| 61 | +immediately (rendering the subsequent write to the kill flag unnecessary). |
| 62 | +
|
| 63 | +II. Exit code propagation. |
| 64 | +
|
| 65 | +FIXME(#7544): Decide on the ultimate model for this and document it. |
| 66 | +
|
| 67 | +*/ |
12 | 68 |
|
13 | 69 | use cast;
|
14 | 70 | use cell::Cell;
|
|
0 commit comments