Sep 9, 2024
Concurrency Models - Threads, Actors and Coroutines.
blinx
2024-09-09 9:35 PM
Concurrency in programming is the ability of different parts of a program to be executed "out of order" or in "partial order" without affecting the outcome. Ultimately this means controlling when code is executed, rather than it being linear. This concept is crucial for improving the performance of software, especially when today we have multi core processors and various systems.
Threads
So what are threads? Threads are the most traditional and widely used model for concurrency. A thread is a lightweght unit of execution within a process, multiple threads in the same process share mem space and resources. Cool right? (no)
Each thread has it's own stack and pc (program counter), within a process; they share the same memory space, allowing for ease of data sharing. The OS's scheduler manages thread execution.
So what are the advantages to using threads? well they're very familiar for many developers already, so picking up on threads is simple. Thread are efficient for CPU bound tasks aswell.
Example in Python
import threading
def job(id):
print(f"thread {id} starting...")
# do something very important
print(f"thread {id} ending...")
threads = []
for i in range(5):
t = threading.Thread(target=job, args=(i,))
threads.append(t)
t.start()
for t in threads:
t.join()
Actors
Actors. A bit of a strange name for something to do with programming. It's nothing but a mathematical model that treats "actors" as the universal primitives of concurrent computation. In this model, everything is an actor.
Now how do they work?? Okay let me explain. Each actor is an independent unit of computation that runs concurrently with other actors, they communicate by sending messages to each other (duh), but each actor has it's own private state, which can be modified by that actor. They can send messages to other actors, create new actors and designate how to handle the next message it recieves.
Actors come with a few advantages, such as no shared state, which reduces synchronization issues, better fault tolerance and isolation, easier to reason about in complex systems (using Actors in games for example) and they are scalable to distributed systems.
Example in Akka
class Worker extends Actor {
def receive = {
case "Start" =>
println(s"worker ${self.path.name} starting")
// do some important work
println(s"worker ${self.path.name} finished")
}
}
val sys = ActorSystem("WorkerSystem")
val workers = (1 to 5).map(i => sys.actorOf(Props[Worker], s"worker-$i"))
workers.foreach(_ ! "Start")
Coroutines
Okay last one. Coroutines. Coroutines are computer program components that allow execution to be suspended and resumed. They are a form of co-op multi tasking, where coroutines voluntarily yield control periodically or when idle.
Now how do they work... coroutines can pause execution and return control to the caller, when resumed they continue from where they left off, they maintain they own stack, allowing them to be paused and resumed. There are a lot of advantages, but the cherry picked ones consist of being lightweight compared to threads, efficient for i/o bound tasks and can achieve high concurrency with low resource usage; so super good!
Comparison and Use Cases
Threads: Best for CPU bound tasks, shared-memory parallel processing. Challenges are synchronization and scalability limitations
Actors: Best for distributed systems, fault-tolerant applications, complex concurrent systems. Challenges are message-passing overhead and potential bottlenecks
Coroutines: Best for i/o bound tasks, high-concurrency scenarios with limited resources. Challenges: not suitable for CPU-bound tasks, requires careful design
Each concurrency model has its strengths and weaknesses. The choice between threads, actors, and coroutines depends on the specific requirements of your application, the nature of the problem you're solving, and the characteristics of your system. Modern software often combines these models, using each where it's most appropriate.