Should I use async, or use a separate thread?

Hi there, I have a lengthy function, calc_triangles, and I need to run that every few nanoseconds, but I don't want it to slow down the main thread, so I want to put it into a separate thread, hence my question.
As I understand it, async is basically some magical rust sugar which runs the task defined within it on a thread set aside for these kinds of tasks. And, as far as I understand, I should use a library like tokio to do this. Unfortunately I'm a bit lost as to how I should proceed, (I've never really understood async even in C# where they say it's so easy[?]) should I use tokio and tokio::run_async as described in this blog post, or should I use futures, which I don't grasp either?

To explain the circumstances I'm attempting to use this for:
I have a complicated algorithm (Not really complicated, just computationally slow) that I use to render the snake in my snake game, so I initially changed the algorithm to generate a cache of triangles every time I move the snake by one. But then I realized that moving the snake and calculating all the triangle is kind of dumb to do in one function call, and wanted to move it into a separate thread.
Sorry for the long question

4 Likes

I'm making a lot of assumptions about your context here, but here are my initial thoughts:

  • if you're not clear on futures, I'd avoid async at least just now. It's still evolving, and in particular beginner-level documentation and examples will take some time
  • I'm not sure that running your triangles computation in a separate thread will, by itself, give you any advantage. What will your main thread be doing at this point - just waiting for the result? You need some concurrency in terms of multiple bits of work that can converge at some later point in time.
  • what might be useful to you, if your calculation is computationally slow because it involves iteratively doing the same thing for (say) every cell in the arena, is to look at the parallel iterators in the rayon crate to help you get your computation done faster across multiple cores.
6 Likes

async has actually nothing to do with threads (at least directly). It is just syntactic sugar to create functions returning futures, and allowing usage of await! for easier futures continuation (instead of calling .and_then or .map on future, you just await! it, so it suspends current thread, until future is ready).
Obviously futures are in general intented to be run on separated thread (or some thread pool), but it's not "out of the box" futures future. There are libraries like tokio which gives you convinient functions to run future on its thread pool, and this is way to go if you want to use future based approach on multithreading environment.

The question is if you want to use future based approach at all. In my opinion this is very good approach if your solultion is divideable into many small (or at least not-very-big) tasks, which you could chain somehow. Async/await help with this very much and makes such solutions easy.

However if you have several computation heavy calculation, even if this calculation is countinously fed with data, running such on several thread may be both more clear, and more efficient (thread pool has also its overhead).

There is also third option - maybe you are doing simple calculation, but on really big dataset - in this case, rayon with its parallel iterators may be best solution. Also if you decide on rayon, and you don't need result of calculations on the spot, you still may run those iterators on separated thread, so it will not block until you need your result.

6 Likes

Spawn a thread. Communicate with it using mpsc channels.

Async is mainly for network I/O and to avoid having hundreds of short-lived threads.

11 Likes

Thanks a lot to all of you! My algorithm (function, whatever) is rather straightforward and not something I think can be split into a parallel task easily, so I don't think I'll be using rayon for this one. If async and futures are still changing and don't have good documentation for beginners, then I think I'll wait out on that one. For now, however, I'll use a thread like @kornel suggests. One of my first practice projects was actually a (rather crappy) background worker (Which shouldn't be used, it's a mess), so I have some experience with threads.

1 Like

Would it be more advisable to use a Sender<DataToBeWorkedOn>, where I have to clone it every time, or a Sender<Arc<Mutex<DataToBeWorkedOn>>> to just clone the reference? I presume it'd be easier to use the clone method, but suboptimal in efficiency in comparison with the Arc approach

Do you have any code snippets to share? This doesn't sound right.

Let me reiterate, would something like this:

pub fn start_thread_triangles(output: Arc<Mutex<TriangleCache>>) -> Sender<DataToBeWorkedOn>{
    let (mut sender, mut reciever) = channel::<DataToBeWorkedOn>();
    thread::spawn(move || {
        loop{
            let incoming = reciever.recv();
            let cache = TriangleCache::new();
            //do the calculation with the cache and incoming
            (*output.lock().unwrap().deref_mut()) = cache;
        }
    });
    sender
}

Be preferred over this:

pub fn start_thread_triangles(output: Arc<Mutex<TriangleCache>>) -> Sender<DataToBeWorkedOn>{
    let (mut sender, mut reciever) = channel::<Arc<Mutex<DataToBeWorkedOn>>>();
    thread::spawn(move || {
        loop{
            let incoming = reciever.recv();

            let data = incoming.lock().unwrap();

            let cache = TriangleCache::new();
            //do the calculation with the cache and data
            (*output.lock().unwrap().deref_mut()) = cache;
        }
    });
    sender
}

If you can avoid using the Arc<Mutex<T>> entirely, that would be preferred. Is there a reason why you would need Arc<Mutex<DataToBeWorkedOn>> over DataToBeWorkedOn? The only reason to use Arc<Mutex<T>> is if you're sharing this to multiple threads and want those threads to have write access to the shared data. Arc<T> shares with only read-access. T just transfers ownership and does away with the original copy. Note that Arc also requires a deep copy of the data that you wrap within it.

Also, some notes:

  • Does the triangle cache have to be recreated on each new input, or can you reset it?
  • You should use the Mutex from parking_lot. It is both significantly faster than the Mutex in the standard library, and more ergonomic to use.
  • The mpmc channels from crossbeam_channel are also significantly faster / ergonomic in comparison to the standard library.

Receivers implement IntoIterator, so you can do this:

for incoming in receiver {
    // do thing
    *output.lock() = cache;
}
1 Like

I thought that perhaps instead of cloneing it every time I wanted to use it, I could just clone the Arc<Mutex> which is probably going to be a lot faster.

But, I still need to be able to modify the original DataToBeWorkedOn, and so I would still need to have write access.

If I were to use the pre-existing one, it would kill the purpose of the new thread. It would lock the pre-existing one and make the main thread (Which draws stuff as well) wait until it has read-access.
Unless you mean something like so:

let cache = TriangleCache::new();
loop{
    //
}

Which would be just fine.
I will probably use the standard Mutex because the DataToBeWorkedOn is only changed every few ms, which basically defeats the purpose of using a faster library in that sense.
The channel alternative sound really ergonomic though, and I will probably switch.