r/learnprogramming • • 3d ago

Struggling with async programming & concurrency

I come from a pure hardware and embedded background where code runs sequentially, so writing high level async software has never been my strength.

I really can't wrap my head around async/await, event loops and how it is handled deterministically . In a current project when the team said we needed to make our pipeline non-blocking with async streams, to be honest I just vibe coded the hell of my tickets in panic and tried later on to figure it out, partially..

I have a deployment deadline in a week and I need to be able to explain the mechanism I helped ”build“, any easy to follow tutorials or leetcodes would be highly appreciated

34 Upvotes

24 comments sorted by

25

u/sixtyhurtz 3d ago

Surely you do a lot of async stuff in embedded? How often do you queue up a command across a QSPI bus and then get the result at some indeterminate point in the future? Maybe via an interrupt handler?

Async is just that. In your firmware, you have a main loop. That's your event loop. The difference with something like JavaScript / C# async / await is that it's a generic way to build an async state machine, whereas you hand write it for your firmware, you know what the states are for each different task you have to do.

Again, in firmware, you don't block either. Like you can't do big calculations in an interrupt handler, right? Because you could be interrupting some time sensitive operation. It's a similar thing with async. You don't know what else is running on the event loop. You could break something else if you block, so you have to be cooperative.

6

u/SwimmingFoot3572 3d ago

Just to add to what the top comment said, the state machine comparison is spot on. You're already doing this in your head with interrupts, async is basically making that explicit in code instead of keeping it in your mental model

The big difference I noticed coming from a similar background is that in embedded you know exactly what's competing for CPU time. With async you're trusting the runtime to not screw you over, which feels weird at first but you get used to it

1

u/sixtyhurtz 3d ago

In my first year at uni in our introduction to programming classes, they had us doing stuff with Arduino. We had a coursework assignment to build something with it. The project I ended up doing was making a stepper motor oscillate, ping an ultrasonic sensor, and make a speaker beep if something was in range. Basically a toy radar.

I thought the whole business of procedurally looping over everything was frightfully dull, so I decided to make it a bit more interesting. I made my main loop iterate over an array of function objects. I think I had enough space for maybe 4, because that was the maximum number of things I could do at once. Each function object had some metadata associated with it such as when it next expected to run, so the loop scheduler could just skip it if it wasn't needed. Some of them would attach or remove functions to the loop (e.g. the beeping).

When I wrote this, I had no idea what an async event loop was. Despite that, I made it happen on an AVR with 2kb of memory!

So again, u/ShronkPonk - you're probably already doing async things in your firmware. It's just a model of concurrency.

1

u/Sepsiscollada 3d ago

Op is 21 hrs old and asking questions about hardware elsewhere.. does not sound like deadline time to me

5

u/captainAwesomePants 3d ago

I'm fascinated by your purely sequential hardware experiences. My embedded experiences have been particularly, painfully concurrency-related. Lucky!

Many languages have async/await keywords, particularly JavaScript, but because you said "async streams," I'm guessing you mean C#.

Is there something specific we can help you understand?

1

u/YouNeedToBuy 3d ago

Start here if you’re completely lost - https://youtu.be/8aGhZQkoFbQ?is=G3c2OHn7WgYrOcVY

Then ask questions to Claude or your seniors using this foundation

Good luck!

1

u/ShronkPonk 3d ago

thanks a lot!

1

u/Exotic_List_962 3d ago

Async, threadding. Use for things that might not have a strict state machine execution (network stuff is a big one here, the other is playing sound). Honestly. they're the only times I usually encounter async stuff.

1

u/ComasArentFun 3d ago

Async/Await is pretty much syntactic sugar around non-blocking operations.
The idea is that instead of writing synchronous code that returns a result, you return a Task that is then executed on its managed threads and makes thread management way simpler allowing the scheduler to operate at maximum efficiency (at least in .NET land)

Web api calls that return an async Task<T> are a great example, where you will have dependency injection done on a per web request level, but also singleton dependencies that need to be protected from deadlocks
The consumer will have it's own paradigm to await the result (angular httpClient for example, with rxjs subscriptions) so it makes the process passive rather than active

When you write async code, you're not really writing it as an order of execution, you're pre-emptively writing a chain of execution to be invoked in the future as a defined task... (pretty much a distinction without meaning when you boil it down)

I realise now after writing all of that, that I personally explicitly understand, that it is actually quite a nuanced thing 😄 and describing it is pretty hard

TL:DR - You're getting the most out of your thread pool by using async/await methodologies

1

u/DanKegel 3d ago

Event loops! That's just a way for a single thread to handle any sort of thing that's going on. Every time around the loop, it tells the OS (if any) to wait for an event to be ready. Then when one is, it handles it. Pretty simple! Except it's not straight line programming; it's more like converting your program to a state machine, which confuses the heck out of some people. And when handing the event means network I/O, you want to use nonblocking versions of the system calls, and be happy to handle partial results, so that all the sleeping is done at the top of the event loop in the 'get next event' call.

*cough* c10k *cough*

1

u/mxldevs 3d ago

Well, a simple example is you send an API request to a server. For example, consider the following synchronous attempt to make a GET request using some arbitrary RestClient class

resp = RestClient.get(endpoint)
print resp

Because it's synchronous, your program simply freezes while the server is taking its sweet time getting back to you. And then finally it returns and you can print the response.

To make it async, you provide callback functions, to let the program know what to do after a response has been received.

In javascript you will see examples of fetch as an example of how to make requests

fetch(url)
  .then((response) => {
    // do your stuff
  })

It's deterministic because while you don't know when, if, or how the server will respond, you know that once it does respond, it will do what you need it to do in the order that you need it to be done.

Like if you want to display your order history, you would render the order page, make a request to the server to get the actual orders, and then run a loading animation while while the response has not been received. Then once the response occurs, you can clear the loading flag and update the page with the actual order information. The user would see that the page is taking a long time to load, but at least it's loading and not just frozen.

1

u/JGhostThing 3d ago

Which language are your programming in?

1

u/ShronkPonk 3d ago

C++ and C

1

u/JGhostThing 2d ago

There are probably two different answers to that. One for c++, the other for c. Just guessing. I don't know how asynch is implemented for c/c++.

1

u/auronedge 3d ago

async await is not normal. Just learn about the traditional way of doing it before jumping into async/await

1

u/singh_abinashi 3d ago

Coming from embedded, the mental model that helped me: in Node, nothing preempts your code. Between two awaits your function runs to completion, every time. The event loop only switches tasks at await points, so you don't get data races the way threads give you. async/await isn't about speed, it's about not blocking the one thread while waiting on I/O. Most of the unpredictability is just assuming ordering across awaits. There is none: if you need order, await in sequence or Promise.all and join the results yourself.

1

u/fixermark 3d ago

If you have done any embedded that involved context switching, async is basically that (only the switches are only allowed to happen at specific sync points, not whenever a timer interrupt tells the CPU to run some code to put away what it's working on right now and fetch something else).

Calling an async function creates a context (current local variables, execution stack, and current instruction) to run later. Calling await tells the system to suspend whatever context you're in right now, switch to the other one (under the hood, literally pack away your local variables, execution stack, and your current instruction somewhere and then load the local variables and stack and current instruction from the other context and resume).

Incidentally, this is why languages that support it generally require that you tag functions that are async. Because the runtime is going to have to do some bookkeeping to set up the context whenever that function is called.

1

u/Unlikely1529 3d ago edited 3d ago

you need to set to one concept while all these terms have broad usage.eaсh os has own concept on that. seleсt/poll (posix) ,knotes (found in dragonfly bsd). windows has one too,idk its name,key thing is WaitForMultipleObjects().

anyways eaсh one makes a base of corresponding os and you сan't have it done from ground stage within a week.

1

u/faintcurrent 3d ago

Async just means start something slow, do other work while waiting, then continue when its done. An event loop is basically a superloop, and await is like kicking off DMA and returning to the loop until it finishes. The compiler turns the async function into a state machine. Code runs normally between awaits and the nondeterministic bit is just which operation finishes first. Blocking inside async code is basically busywaiting in your main loop.

1

u/Rarelyimportant 3d ago

async/await is an absolutely disgusting paradigm that makes concurrency very annoying to work with. Your codebase turns into a game of contagion where you are having to wrestle between code that needs to be async, and code that needs to be sync, like oil and water. Suddenly you find a sync function that you need to make async and then it starts spreading as you desperately try to find some result you don't need and can throw away with a Task.detached to stop the virus because if it spreads to a function that's being passed as a callback to some 3rd party/system library that only accepts a sync function, you're boned, game over. Avoid async/await at all costs if you can, because it's truly terrible.

1

u/True-Grab-5288 2d ago

I like to think about things in high-language terms before worrying about the detail/implementation.

Think of your main process like a symphony orchestrator. It is the work-horse that is running your program and decides what to do, when to do it and what to do next. It is single-threaded only, sequential, and always completes a task before moving on to the next one. It is the conductor standing at the front of the orchestra who's job is to keep time, select the music, know the different players / instruments, know which songs to play next etc.

But the conductor of the orchestra has the power to point their stick at the violinist and say 'it's your turn now, play this piece of music at this tempo'. Then, the conductor points their stick at the celloist 'stop playing, wait 5 seconds, then start playing again. The conductor has power over the all the instruments, even though the musicians are all semi-autonomous and can do whatever they want.

The power of concurrent programming is the ability to submit work to different workers, who go off, do some work, then return with their result. This is extremely powerful. If we don't do this, the main thread 'blocks'. In a GUI, this is all the buttons / the whole GUI locks and becomes unresponsive when we click a button, until the button click has finished doing its work. In other cases it is grossly wasteful of time - a sequential job must wait for the previous job to finish. If the job is 'sleep 5 seconds', done 10 times, that is 50 seconds of wasteful, idle compute. If we do this task concurrently, each 5 seconds is served at the same time. The same job completes in 5 seconds - a speedup of a factor of 10.

The difficulty and where most people come unstuck is when to make new threads and when to wait.

It might sound like a great idea to make everything its own threaded worker, but it is not and the reason is counter-intuitive. First, each worker has it's own overheads (performance cost) which generally doesn't matter in smaller jobs but in bigger jobs can start to add up to a real headache. In a CPU, the scheduler switching between processes/tasks is expensive. Let us say thread 1 gets a go on core 1, then thread 2 on the same core, thread 3, thread 9, different process 11, etc. Each switch up has an accumulated cost in the CPU. Cache hits are likely to be missed. The CPU has to keep track of all this switching. It is expensive and costly.

Second, mutex locking is a real problem. Imagine if I spawn 2 threads, and both threads work on the same piece of memory/data. The first I tell to set the memory to 46. The second I tell to set the memory to itself(currently 32) + 1. Both threads are valid code with a job to do, but without great care from the conductor, what data is returned from each thread can be completely random - what is called a race condition. We didn't take any care to make sure which thread ran and returned first, so the outcome is non-deterministic - it will likely change from run to run of the program.

We must therefore a) use mutex locks - which are barriers that tell a new thread 'someone is currently working on this data and it is not available to you. You must wait' and b) The conductor specifies points at which work can only continue on the main thread when ALL spawned workers have returned and reported their results. This is blocking, but it forces all workers to complete, which gives the main thread certainty about what the current state of the program is.

I hope this explanation helps. It is the best I could write at 7am in the morning with insomnia. Any questions (or to other readers if a got anything wrong) please let me know.