← back

Do you know concurrency?

We’ve all heard it before - want to make your software faster? Just add concurrency! Break down your problem into smaller bits, run them at the same time, and boom - your program runs faster than a speeding bullet! At least, that’s what we’re often told. But here’s the thing - it’s not that simple. While concurrency can indeed bring impressive performance improvements when implemented correctly, it’s not the magical solution it’s often made out to be. Let me explain why.

The Difference Matters: Concurrency vs. Parallelization

Before we dive deeper, we need to clear up a common confusion. When we talk about making things run simultaneously, we’re actually talking about two different concepts: concurrency and parallelization. Concurrency is about structure - it’s dealing with multiple tasks by switching between them. Think of it like a chef preparing multiple dishes in a kitchen, moving between different tasks. Parallelization, on the other hand, is about actual simultaneous execution - like having multiple chefs each working on a different dish.

In most systems, we don’t have truly parallel requests. Instead, we often have a mix - some things running in parallel while others need to be processed one after another. This distinction becomes crucial when we talk about the biggest theoretical limitation of speeding things up: Amdahl’s Law.

Modern Concurrency Models: New Tools, Same Challenges

Now, before we get to the limitations, let’s talk about how we actually implement concurrency in modern software. We’ve come a long way from the days of manually managing threads and hoping for the best. Modern programming languages and frameworks give us much more sophisticated tools to work with.

Take async/await, for instance. If you’ve done any recent JavaScript or Python development, you’ve probably seen this pattern. It lets you write code that looks synchronous but can actually do other work while waiting for slow operations. It’s like how a chef might start the rice cooker and then chop vegetables while the rice cooks - they’re not doing two things at exactly the same time, but they’re making efficient use of their time.

Then there’s the actor model, used in systems like Erlang and Akka. Instead of sharing memory between threads, everything is done through message passing. Imagine each part of your program as an actor in a play, sending messages to other actors to get things done. No more worrying about two threads trying to write to the same memory at the same time - each actor minds its own business and just sends messages when it needs something.

Some languages, like Go, use what’s called the Communicating Sequential Processes (CSP) model. Go’s goroutines are like ultra-lightweight threads that can be created by the thousands, and channels provide a way for them to communicate safely. It’s a bit like having thousands of tiny workers, each focusing on a small task and coordinating through designated communication channels.

These modern approaches make concurrent programming more accessible and safer, but they don’t change the fundamental limitations we’re about to discuss. They’re better tools, but they’re still subject to the same underlying constraints.

The Theory: Amdahl’s Law

Amdahl’s Law states something that might seem obvious once you think about it: the maximum speedup you can get by adding more processors is limited by how much of your code has to run one thing at a time. In other words, the parts of your program that can’t be parallelized (the serial parts) put a hard limit on how fast your program can go. Even a small increase in the code that must run serially can have a huge effect on the maximum speedup you can achieve.

If you’re still not convinced, let’s look at the following graph

Amdahls Law Wikipedia

The y-axis represents the speedup, and the x-axis represents the number of processors. In this system, we assume that we have some percent of serial processes while the rest of the processes are parallel (the percent of the parallel portion is shown in the legend). When the percent of parallel processes is 95%, we observe that the speedup increases with the increase in the number of processors until a certain point, after which it flattens (We can calculate this point using Amdahl’s law). With 90% of parallel processes, the graph flattens much sooner. In other words, with only a 5% increase in the serial processes, the maximum speedup is reduced by 50%, and with 75% of parallel processes, the maximum speedup is almost as good as running all processes serially.

The Practice: When Concurrency Makes Things Slower

But wait, there’s more! Even when you can parallelize most of your code, concurrency can actually slow things down. Why? Because parallel execution comes with overhead. Every time your program needs to synchronize between threads or communicate between parallel processes, it’s doing extra work. It’s like having multiple chefs in a kitchen - sure, they can cook different dishes simultaneously, but they also need to coordinate, share kitchen tools, and avoid bumping into each other.

And let’s talk about bugs. Concurrent code can introduce some of the most subtle and maddening bugs you’ll ever encounter. When multiple threads are accessing shared resources, ensuring they don’t interfere with each other in unexpected ways becomes a nightmare. You never know exactly what order your threads will execute in, leading to race conditions - bugs that only show up sometimes, under specific conditions. These are the kinds of bugs that keep developers up at night.

Not All Problems Are Created Equal

Here’s another thing to consider: not all problems can be effectively broken down into parallel tasks. Some tasks are inherently serial - they need to happen in a specific order, one step at a time. Trying to force concurrency onto these types of problems is like trying to fit a square peg into a round hole - it just makes everything more complicated without any real benefit.

When Concurrency Actually Shines

Now, I’m not saying concurrency is all bad. There are definitely cases where it can be incredibly useful. Tasks involving a lot of I/O, like reading from files or making network requests, often benefit from concurrency. These tasks involve a lot of waiting around for external resources, so having other work happening during that wait time can be a big win.

Heavy computational tasks can also benefit from true parallelization. If you’re doing something like rendering complex graphics or running scientific simulations, breaking the work across multiple CPU cores can give you significant speedups. The key is knowing when concurrency will actually help and when it’s just adding unnecessary complexity.

The Bottom Line

Before you jump on the concurrency bandwagon, take a step back and consider if the added complexity is really worth it. Can your problem actually benefit from parallel execution? How much of your code truly needs to run serially? Are you prepared to deal with the potential bugs and debugging challenges that come with concurrent code?

Remember, the goal isn’t to use concurrency just because you can, but to make your software better. Sometimes that means embracing concurrency, and sometimes it means keeping things simple and serial. The key is knowing the difference.

So the next time someone suggests throwing more concurrency at a performance problem, remember - it’s not always the silver bullet it’s made out to be. But when used wisely, in the right situations, it can be a powerful tool in your toolbox.