Name for "cooperative future" pattern?

I'm writing a program that needs to do some long running disk I/O while being responsive to user input. I am thinking about making heavy use of a type like

class Pending[T] {
    def get(): Option[T]
}

to represent the result of a long running computation. get() returns Some[T] if the operation is done, None otherwise. But each time None is returned, it makes a little progress in the computation. Thus a thread can alternately call get() and check if there is any user input, so it can quickly respond to new user input.

(Apologies for Scala style pseudocode. Still a Rust learner. I do intend to write the actual program in Rust)

It is almost like futures Future in std::future - Rust, except that the operation is not async.

Likewise, the type

class PendingIterator[T] {
   def next(): Option[T]
}

represents a sequence of long running computations. next() returns Some[T] if the next object is available, None if not, and the iterator advances each time Some is returned.

Is there a name for this pattern? I ask because I haven't really seen it before, and don't know what search terms to use either. Are there any libraries for it? (Composing Pending types and PendingIterators etc)
Is this a good way to implement a "concurrent" program anyway?

This is pretty much the same as a Future: you are polling for the result, and it returns it when ready, and thus it is asynchronous.

The distinction is just that you don't have a way for it to notify that now would be a good time to wake it up like futures have.

If you don't want to go through the effort of implementing the Future API though, a common name is Promise or Task.

1 Like

It is almost like futures Future in std::future - Rust, except that the operation is not async.

You basically have made a different version of Future. There is no more intrinsic “asyncness” to Future than there is to your trait. The only difference is that Future has a means to efficiently do nothing if there is currently nothing for it to do, by way of the Waker, but that's in a sense an optimization and not a fundamental difference. It's valid for the owner of a Future to poll() it any time it likes, just usually inefficient.

Is there a name for this pattern?

Ignoring comparisons to Future in particular, I would keep it simple and say that you have a form of cooperative multitasking here. Your long-running operation explicitly chooses to pause its work and relinquish the CPU to other concurrent tasks — that's exactly what cooperative multitasking is.

If you mean the pattern of returning Option for "nothing available yet", I don't know of a name for it, but one place it comes up a lot is channels, which often have a try_recv() method that returns an Option (or Result) containing a message only if there is a message, and that's exactly like your PendingIterator.

Personally, I would suggest having your function return Poll instead of Option — there is no functional difference, but the naming makes it clearer what your return means and how to use it. As I said, your Pending is pretty much exactly a Future minus the Waker mechanism, so bringing it closer, as far as that doesn't overcomplicate things, is good for familiarity.

5 Likes

Thanks for the replies. After thinking about it a bit more, I think that both Futures and this Pending interface are doing the same thing, they are both doing cooperative multitasking, and they essentially result in the same program. The choice between them is mostly up to taste. (Feel free to correct me if I am missing something.) With futures, calls to Future.await are the points where the current computation can give up control, while with Pending, it’s Pending.get. With Future, the code looks imperative, but it needs to be run in an async runtime. With Pending, no runtime is needed, but to do anything with a Pending value you’d probably use map or flatMap

class Pending[T] {
   def map[U](T => U): Pending[U]
   def flatMap[U](T => Pending[U]): Pending[U]
}

Probably because of my Scala background, I thought of the functional approach first. But it seems like the Rust community has generally settled on Futures/await/async, and all the support is in that direction, so that’s probably the way to go for Rust programs.

As a way to further think about the relationship between these two approaches, one language that seems to have them both is JavaScript. The “functional” way is Promise How to use promises - Learn web development | MDN
The imperative way is async/await How to use promises - Learn web development | MDN. Async/await is essentially syntactic sugar around Promise.

That's pretty much the same in Rust, where async/ .await is syntactic sugar over Futures. You can hand-implement Futures [1], but it's often painful (if the state has self-references, you need to deal with Pinning, which is hard to wrap your head around).


  1. the difference to JS is that it provides a built-in Promise class, while in Rust you create your own -- in that sense the Future trait is closer to the concept of thenables in JS ↩︎

These sort of functional combinators are available in Rust as well. They are provided by the FutureExt extension trait in the futures crate (flatMap is called then there). They aren't in the standard library because things get added there slowly and only as needed, due to the strong stability guarantees the standard library must uphold (Future only got put in the standard library when it was needed to support async/await).

2 Likes

Okay, here's some detailed corrections!

.await is not part of Future, it is part of async/await syntax. The method on Future is Future::poll, and as I already said, it's nearly identical to yours. You could in fact replace your Pending trait with Future and get identical results.

Again, this confuses Future with async. “The code looks imperative” is a property of async blocks, not of Future. You can write Futures that are not made from async, but are just plain trait implementations. You can use map() and then() if you want to build futures functionally.

You need an async runtime to run arbitrary futures efficiently, but for futures which work like your Pendings that never want to sleep, it is sufficient to just call them the way you already are, providing a noop Waker in the context parameter. I'm not saying you should do that (though I would in your place), but that the difference with what you are doing is nearly zero.

Also, an async runtime can be very small, and 99% of the remaining complexity comes from supporting letting the future sleep and then be woken from an arbitrary thread.

5 Likes

Thanks for the additional details (and the pointer to FutureExt). Now to slowly digest it...

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.