[Pitch] Task Cancellation Shields

This post’s primary goal is to be thought-provoking:

——

This is likely not an actionable line of thinking because it differs too much from the status quo, but it makes too much sense to me not to write it down and share it. I share this here because the problem being addressed in this thread is precisely the problem that my proposed line of thinking aims to architect out of existence.

The Three Laws of Functions

I posit that functions are guided by three laws, similar in structure to Asimov's Three Laws of Robotics:

  1. A function must never do anything "incorrect".
  2. A function must move directly towards its particular goal/finish line.
  3. A function must minimize its usage of the available resources.

And, like Asimov's laws (and really like any set of laws should be), these laws have a relative order which governs which one to obey if two of them come into conflict. For example, the First Law trumps the Second Law, meaning that a function must stop moving towards its goal if doing so would require doing something "incorrect" - in Swift we generally throw an error in such case.

Swift Defaults to Safety

In a recently resurrected thread I read this comment from @xwu:

It seems to me that Swift has generally continued to stick to this philosophy since the time of Xiaodi's comment. It also seems to me like the right default in the context of my proposed laws - safety is fundamentally more important than performance (not to diminish the importance of performance too much - it's important enough to make it into the three laws after all).

What is it to cancel a function?

I propose that to "cancel a function" is to set its "particular goal" (see the Second Law) to nil. It no longer has any finish line to aim at. Thus, the Second Law becomes inert, and all that is left are the First and Third laws, meaning that it is still of utmost importance that the function not do anything "incorrect", and then within that constraint the function should use resources minimally. For many functions, simply stopping in the middle is not "incorrect", and is also the minimal usage of resources. For these functions, cancellation should trigger an early exit. For other functions, there may be some cleanup that is necessary to stay within the bounds of "correctness", and even though this violates the Third Law we still do the cleanup, because the First Law is more important.

What is it to cancel a task?

A task is essentially a handle for an in-progress function. Under Swift Concurrency, to cancel a task is to cancel the top-level function that it represents (where cancellation has the semantics I described in the previous paragraph) AND to individually cancel all in-progress subcomponents of the top-level function. Using a cooking example like in one of the classic WWDC videos: if our task is to "make salad", then to cancel is to say that we no longer need to get to the finish line of having a prepared salad, and additionally to tell the "chop carrots" child task that we no longer need to get to the finish line of chopping carrots.

How does this relate to the pitch at hand?

The issue at hand in this thread is that sometimes it is very much incorrect for the system to tell a subcomponent of a function that it no longer has to reach its finish line, just because the parent no longer has to reach its finish line. In the general case, this is not a sound logical leap. One would have to leverage specific knowledge of the entire call tree (including descendants) of a function to determine that the function losing its finish line is equivalent to every single subcomponent losing its finish line. In our cooking example, the problem is telling the "move knife to a safer location" function that it no longer needs to finish, just because we no longer need a salad.

Conclusion

Swift Concurrency's automatic task cancellation propagation seems to me to break with the guiding principle of safety-first that Swift seems follow pretty much everywhere else. It is a behavior that is always performant, but sometimes incorrect, which is a reversal of the First and Third laws. If we were still debating how Swift Concurrency should be implemented I would be arguing that we should seriously consider having task cancellation not propagate automatically, and then find ways to make opting into performant code easy in proportion to how difficult it is to introduce incorrect behavior (like we do everywhere else).

I understand that it might be impossible at this point to switch to not propagating cancellation by default, and I also haven't thought very far in terms of what those opt-in mechanisms would ideally look like. One thing that seems clear to me is that turning on propagation for an entire portion of the task tree should either be impossible or be a very salted tool since, again, one would need to have full knowledge and correct analysis of the implementation of every sub-function in order to use this tool correctly. The much more easily reached tool should be for creating one or many child tasks to which cancellation is propagated (but not propagating to the children of those children), since that is something that the given function can reason about locally.

The only thing that makes me wonder if such a change could conceivably be made is something along the lines of this comment by @allevato from the same ancient thread:

If I'm not mistaken that automatic cancellation propagation is strictly a performance improvement (albeit an incredibly important one), then it seems conceivable to me that under a new language mode (e.g. Swift 7) and managing to come up with a low-effort migration story, we could change this behavior, since no code would become incorrect, just slower.

Please feel free to correct or criticize both the content and sheer size of this post. I'm not proposing in full earnest that we make such a change, I'm just curious what others take on my line of thinking is, and maybe it will stir up some new ideas or consensus in relation to this pitch.

1 Like