This is the first post in a series of articles about concurrency and concurrency software patterns. While the thinking is not polished yet, I want to gather feedback and understand what resonates with readers, so please comment.
Concurrency is about writing code to do multiple things at once. It is fundamentally complex, and has led to a plethora of software patterns and theoretical work to attack the problem. Which approach to use when confronted with a concurrent problem is an unending source of confusion and debate amongst developers. I think a lot of this confusion and debate comes from attaching more importance to individual patterns than they deserve.
Code is the means by which programmers solve business problems. It is both a way to instruct computers to do certain things, and a common language that allows programmers to collaborate. In our case, we want to write code to handle "multiple domain activities happening at the same time". What "things happening at the same time" means is dictated by the problem domain.
How the code is organized influences:
- how programmers think about the problem
- how programmers communicate about the problem
- how the code behaves at runtime
- how the runtime behaviour is instrumented and logged
- how bugs are handled
Thinking of concurrency patterns such as
state machines ,
callback handlers as means of organizing code cuts through a lot of the debates about "power" or "expressiveness". It takes debates from theoretical concerns to a more pragmatic and thus tractable discussion about:
- code organization
- modularity / reuse
- team coordination
- runtime behaviour
In this first article, I am going to show how a recipe with two concurrent steps can be organized in two ways.
To show what I mean by code organization, let me start with a real-world example that has nothing to do with computers. How can we write up the recipe for cooking pasta with tomato sauce (I'm not a great cook, but I hope I get this roughly right). Cooking pasta with sauce can be decomposed in three problem domains:
- cook the pasta perfectly
- make the perfect sauce
- combine the two and serve
One way to write the recipe is to separate the steps for the pasta from the steps for the sauce.
The steps to cook the perfect pasta:
- Put water to boil
- Put pasta in water
- Wait for exactly 7 minutes
- Take paste out of water
The steps to cook the perfect sauce:
- Slice the tomatoes, onion, garlic
- Brown the onions and garlic
- Add the tomatoes
- Let it cook
- Add hot bolognese to hot pasta and serve
This structure allows for easy reuse, because it is modular and structured around the problem domain.
If we wanted to cook pasta with a different cooking time, we would just edit the cooking step of the pasta recipe. If we wanted a different sauce, we would swap out the sauce recipe.
This separation puts the burden of linearizing the execution on the cook. Even in an ideal setting, the cook needs to be experienced enough to know when to start putting the pasta in, how long it takes to brown onions, remember to check the cooking of the pasta.
For a beginner, the recipe might better be written as:
- Slice the onions and garlic
- Put some water to boil and heat up a pan
- Put the onion and garlic to brown
- Slice the tomatoes while the onions are browning, but be quick about it
- Add the tomatoes to the pan
- Wait 5 minutes, then put the pasta in the pan
- Check the pasta every couple of minutes and stir the tomatoes
- Once the pasta is done, drain, pour sauce and serve
This however makes it much harder to swap out for a different sauce. If we suddenly had a much longer cooking pasta, we might need to shuffle the steps around.
If we were to turn this into a restaurant ready recipe, different business constraints might speak for one recipe style or more for another:
- what happens if we had only one burner, can you cook both the pasta and the sauce at the same time?
- what happens if we wanted to make 10 dishes in less than 10 minutes?
- what happens if we had 3 mediocre cooks and 4 unreliable stoves?
- what if tomatoes take longer to cook than expected?
- how do would we deal with failing ovens?
- how do would we track dish quality?
- which cook is consistently putting out mushy pasta?
- could we do with one less oven?
These constraints might speak for one organizational style more than the other, but I assume that in general, cooks in a professional setting deal much more with single process recipes than the separate process form, to make results more consistent and scalable.
In following posts, I plan to write about:
- What does concurrency mean at the hardware level?
- What is the impact of an OS?
- The two fundamental concurrency styles: event-centered vs code-centered
- How does theoretical thinking help reason about concurrency (monads, effect handlers, programming language research)
- Different concurrency styles (callbacks, promises, OS threads, green threads, async/await implementations)
- Different small scale concurrency patterns: promise combinators, state machines
- Impacts of concurrency patterns on runtime behaviour
- Impacts of concurrency patterns on logging and debugging
- Impacts of concurrency patterns on code organization
Did illustrating concurrency to cooking spaghetti make sense to you? Can you think of other ways of writing such a recipe down? What do you want to hear about next?
Feel free to leave a comment below!