What to call those things that Tokio schedules asynchronously?

I'm used to the usual names we have for units of work that get run at the same time (actually or apparently/parallel or concurrent):

"process" - Those heavy weight jobs our operating systems juggle. Often with each in its own protected memory space. You know "programs".

"thread" - Those bits of work that can be run at the same time within a process. Much lighter weight no memory isolation.

"green thread" - No idea really, some kind of thread created by a language or its run time.

Then we come to the world of async Rust and the likes of Tokio. Here I got used to calling these things that get ".await"ed as "tasks". So as to distinguish between "process" and "thread".

So I end up with functions in my code that contain loops that are supposed to run for the forever (the life of the program at least). With names like "writer_task". Where the "task" part is supposed to at least remind me of their longevity.

But today I find this in the Tokio docs:

The tokio::spawn function takes an asynchronous operation and spawns a new task to run it. A task is the object that the Tokio runtime schedules. Two different tasks are scheduled independently by Tokio. They may run simultaneously on different operating system threads.
...
The select! macro runs all branches concurrently on the same task . Because all branches of the select! macro are executed on the same task, they will never run simultaneously . The select! macro multiplexes asynchronous operations on a single task.

Hmm...It seems not all ".await"ed things are the same. Some can be run with true parallelism by having their another thread to run on and hence possibly a CPU core. Some are only concurrent all running on a single thread.

So now my "writer_task" could be concurrent or parallel depending on if I select! it or spawn it. If I select! it it is said to run on the same task, so it is not really a "task" at all.

In short, what is good naming for things like my "writer_task"? Or should I just forget the "_task" naming convention I have? Or do I worry too much?

1 Like

These are tasks too in Tokio's lingo.

I would side-step the problem entirely by just calling the function writer().

To me, appending _task to the end is kinda like adding _func to the end of a variable name because it happens to contain something callable or adding something like Abstract or Interface to a trait's name because they define an abstract interface. You can normally tell something is callable by the context and all traits are abstract interfaces, so that sort of naming convention feels unnecessary to me.

Consumer doesn't normally care that your function may spawn tasks internally that may execute in parallel or sequentially, instead they will care about what it achieves.

To answer the original question though, when you pass a future to tokio::spawn() it'll create a "task" (async analogue of an OS thread) that gets polled to completion on the tokio threadpool. The select!() macro just lets you create a future that can poll multiple sub-futures concurrently, but it won't create a task until you use tokio::spawn() to run it (or maybe a future which calls select!() internally, or whatever).

1 Like

If the question is:

What to call those things that Tokio schedules asynchronously?

Then those are tasks.

But in your post you mention:

What you .await is not what's scheduled by tokio. You can .await a future inside another future, and similarly you can select!/join! futures inside another future, but their effect is just to "embed" the those futures inside the current future and instruct it on how/when to poll them. So in the end it's all inside a single future, which you can then execute by spawning a task for it, but tokio will know nothing about what you .awaited/select!ed/join!ed inside it, just that it is a future.

So if your writer_task doesn't tokio::spawn task internally, then it isn't a task but just a future. And given that every async function is a future I would avoid putting the "future" in the name.

3 Likes

Perhaps you are right. I have always loathed that idea of appending types to names. For example calling an integer `my_var_u32". Was that "Hungarian Notation" that was all the rage years back? So I'm not sure why I feel the urge to do it here.

Except... The functions I'm talking about are intended never to return, unless there is a serious error. They look like this trivial example:

async fn line_reader_task(reader: ReadHalf<SerialStream>) -> Result<()> {
    let buf_reader = BufReader::new(reader);

    let mut lines = buf_reader.lines();
    loop {
        match lines.next_line().await.context("Error on reading line")? {
            Some(line) => {
                info!("Serial got: {:?}", line);
            }
            None => error!("Serial got: empty line."),
        }
    }
}

So it is not really about the type of the function. More about the kind of thing it is supposed to do.

If that's the case, I would say line_reader_task isn't the best either.

The line_reader bit is saying what it uses to do something, but what it is actually doing is recieving data from a serial connection and printing it to the console as it arrives. I would probably call it monitor_serial() or print_uart_messages() because when I read the code again 6 months for now and want to know the high-level flow of my program (e.g. while troubleshooting), that's the aspect I would care about most.

I think you are right. I'm jumping ahead of myself here.

You see, at the moment this code is just used to test my serial hardware and cable connections, printing the bytes or lines coming in is all it needs to do and you are right about the naming.

But eventually I want to evolve it to read those bytes or lines and send them out over a channel. The printing becomes redundant apart from debug!ing. But then of course the function would be doing a different thing and deserve a different name. But what?

Clearly the "_task" thing has to go. It may or may not be a task depending on how it is run.

1 Like

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.