Gheorghina Gligor

Thoughts on Coding, Software Architecture, Solving Business Problems, Growing as an Engineer and more importantly, growing as a Leader.

Email address:

Four concurrency models comparison

03 Oct 2022 »

layout: post title: “Concurrency Models: Python vs Elixir vs C# vs GO” date: 2022-10-03 00:18:23 +0700 updated: 2026-06-21 00:00:00 +0700 categories: [concurrency, python, elixir, c#, go, golang] —

Updated 2026: Added comprehensive GO (golang) section with 4-language comparisons and performance benchmarks.

Parallelism

When two or more processes can run at the exact same time. This can happen, for instance, in multicore systems, where each process runs on a different processor.

Concurrency

When two or more processes try to run at the same time on top of the same processor. This is usually solved by techniques such as time slicing.

However, these techniques do not execute in a truly parallel fashion. It just looks parallel to observers because of the speed at which the processor switches between tasks.

The operative system will take care of scheduling time with the processor for each process that requires it. Then, it’ll switch context between them, giving each one a slice of time.

No matter of the order of execution, the outcome shall be the same

  
CPU Schedulingan operating system module selecting next jobs and processes to run(uses context switch to stop and resume the context of a running thread or process)
GIL - Python Global Interpreter Locka mutex (or a lock) that allows only one thread to hold the control of the Python interpreter at a time.

Elixir vs Python vs C# vs GO

Running concurrent processes in Elixir, Python, C#, and GO can have significant differences in terms of performance, syntax, and ease of use. Here are some key considerations:

Performance:

  • Elixir is designed to be highly concurrent and can handle large numbers of processes efficiently on the BEAM virtual machine.
  • GO (golang) excels at concurrent I/O with its lightweight goroutines and built-in channels, providing near-native performance.
  • Python supports concurrency through threads (limited by GIL) and async/await, performing well for I/O-bound tasks.
  • C# offers good performance with modern async/await and Task-based concurrency with full parallelism on multi-core systems.

Syntax:

  • Elixir uses a functional programming paradigm with its own process syntax.
  • GO provides simple syntax with goroutines (go func()) and synchronous-looking channels - “Don’t communicate by sharing memory; share memory by communicating.”
  • Python and C# have familiar syntax and built-in support for threads and async/await.

Ease of use:

  • GO’s simplicity makes it extremely approachable for concurrent programming - goroutines are easy to spawn and channels are intuitive.
  • Python is easy for I/O concurrency but GIL creates complexity for CPU-bound tasks.
  • Elixir’s functional paradigm has a steeper learning curve but excel at massive concurrency.
  • C# provides a good balance with familiar syntax and strong tooling support.

Use Cases

Adding multiprocessing support in python comes with an overhead. Therefore the use cases have to be justified for the introduced complexity.

  
Multiprocessing - CPU Intensive Tasksstring operations, search algo, graphics processing, number crunching, algo…
Async Programming - IO Intensive taskspass the task to another Actor, get a callback once it is done; Ex: database read/writes, web service calls, copy, download, upload data to disk or a network, ..

Thread vs Process

  
Processis an instance of a computer program that is being executed. Any process has 3 basic components: an executable program, the associated data needed by the program (variables, work space, buffers, etc.) and the execution context of the program (State of process)
Threadis an entity within a process that can be scheduled for execution; it is the smallest unit of processing that can be performed in an OS (Operating System)

Threading (Python)

Module: threading (available since Python 1.5+)

Threads are lightweight compared to processes and share the same memory space. However, Python threads are affected by the Global Interpreter Lock (GIL), which means only one thread can execute Python bytecode at a time.

Thread Lifecycle: NEW → READY → BLOCKED → RUNNING → TERMINATED

Key characteristics:

  • Shared memory and OS resources (open files, network connections)
  • Lower overhead compared to multiprocessing
  • Subject to thread interference and race conditions

Challenges:

  • Race conditions - multiple threads accessing/modifying shared data without synchronization
  • Deadlocks - threads waiting indefinitely for each other to release resources

Thread Synchronization:

  • Lock: Ensures only one thread accesses a resource at a time
    lock = threading.Lock()
    with lock:  # Recommended approach
        # critical section
        pass
    
  • RLock (Reentrant Lock): Allows the same thread to acquire the lock multiple times
  • Semaphore: Controls access to a shared resource through a counter
  • Event: Allows threads to wait until a specific condition is met

Multiprocessing (Python)

Module: multiprocessing (available since Python 2.6+)

Multiprocessing bypasses the GIL by using separate Python processes instead of threads. Each process has its own Python interpreter and memory space.

Key characteristics:

  • Each process is independent with its own memory space
  • True parallelism on multi-core systems
  • Higher overhead compared to threading due to process creation
  • Inter-process communication (IPC) requires serialization/pickling

When to use:

  • CPU-intensive tasks (calculations, data processing)
  • When you need true parallelism without GIL limitations
  • Tasks that can be easily divided among multiple processes

Example:

from multiprocessing import Pool

def cpu_intensive_task(n):
    return n * n

if __name__ == '__main__':
    with Pool(4) as p:
        results = p.map(cpu_intensive_task, range(10))

Abstract Concurrency (Python)

Module: concurrent.futures (available since Python 3.2+)

Provides a high-level interface for asynchronously executing callables. This module abstracts away the complexity of managing threads or processes directly.

Key features:

  • ThreadPoolExecutor: Uses a pool of threads for I/O-bound tasks
  • ProcessPoolExecutor: Uses a pool of processes for CPU-bound tasks
  • Futures: Represent the eventual result of an asynchronous operation
  • Automatic resource management

Example:

from concurrent.futures import ThreadPoolExecutor, as_completed

def io_task(url):
    # fetch from url
    return result

with ThreadPoolExecutor(max_workers=5) as executor:
    futures = {executor.submit(io_task, url): url for url in urls}
    for future in as_completed(futures):
        result = future.result()

Asynchronous Programming (Python)

Module: asyncio (available since Python 3.4+)

Asyncio provides a framework for writing single-threaded concurrent code using coroutines. It’s ideal for I/O-bound operations and allows many concurrent operations on a single thread using event loops.

Key concepts:

  • Coroutines: Functions defined with async def that can be paused and resumed
  • Tasks: Coroutines wrapped in a Task object scheduled on the event loop
  • Event loop: Runs asynchronous tasks and manages their execution
  • Awaitables: Objects that can be used with the await keyword

Advantages:

  • Much lighter weight than threads
  • Better control over task scheduling
  • No GIL limitations for I/O operations
  • Cleaner code flow with async/await syntax

Example:

import asyncio

async def fetch_data(url):
    # Simulate async I/O operation
    await asyncio.sleep(1)
    return f"Data from {url}"

async def main():
    results = await asyncio.gather(
        fetch_data("url1"),
        fetch_data("url2"),
        fetch_data("url3")
    )
    return results

asyncio.run(main())

When to use:

  • I/O-bound operations (network requests, file operations)
  • When you need high concurrency with minimal resource overhead
  • Building responsive web services and APIs

Lightweight Processes and Message Passing (Elixir)

Module: Process, send, receive (Core language features)

Elixir’s concurrency model is built on the actor model via lightweight processes that communicate through message passing. Each process is independent with its own memory space and mailbox.

Key concepts:

  • Processes: Lightweight actors managed by BEAM VM (10-50KB each)
  • Message Passing: Asynchronous, reliable delivery to process mailboxes
  • Receive: Pattern matching on incoming messages
  • Supervision: Hierarchical process supervision for fault tolerance
  • OTP: Open Telecom Platform - battle-tested concurrency framework

Process Lifecycle: SPAWN → RUNNING → RECEIVE/SEND → TERMINATE

Key characteristics:

  • Extremely lightweight (BEAM can handle 1M+ processes)
  • Isolated memory spaces prevent race conditions
  • Pattern matching on messages
  • Built-in fault recovery (Let It Crash philosophy)
  • Hot code reloading support

Example 1: Basic Process and Message Passing:

defmodule Counter do
  def start_link(initial_value) do
    spawn_link(fn -> loop(initial_value) end)
  end

  defp loop(count) do
    receive do
      {:increment} ->
        IO.puts("Count: #{count + 1}")
        loop(count + 1)
      
      {:decrement} ->
        IO.puts("Count: #{count - 1}")
        loop(count - 1)
      
      {:get} ->
        IO.puts("Current: #{count}")
        loop(count)
      
      :stop ->
        IO.puts("Stopping counter")
    end
  end
end

# Usage
counter_pid = Counter.start_link(0)
send(counter_pid, {:increment})  # Count: 1
send(counter_pid, {:increment})  # Count: 2
send(counter_pid, {:get})        # Current: 2
send(counter_pid, :stop)

Example 2: Worker Pool with Supervisor (Elixir Pattern):

defmodule Worker do
  def start_link(worker_id) do
    spawn_link(fn -> loop(worker_id) end)
  end

  defp loop(worker_id) do
    receive do
      {:job, job_data, caller_pid} ->
        result = process_job(job_data)
        send(caller_pid, {:result, worker_id, result})
        loop(worker_id)
      
      :stop ->
        :ok
    end
  end

  defp process_job(data) do
    # Simulate work
    :timer.sleep(100)
    data * 2
  end
end

defmodule WorkerSupervisor do
  def start_workers(count) do
    Enum.map(1..count, fn id ->
      {:ok, pid} = Task.start_link(fn -> Worker.start_link(id) end)
      pid
    end)
  end

  def distribute_jobs(workers, jobs) do
    workers_cycle = Stream.cycle(workers)
    
    Enum.each(Enum.zip(jobs, workers_cycle), fn {job, worker} ->
      send(worker, {:job, job, self()})
    end)
  end
end

# Usage
workers = WorkerSupervisor.start_workers(3)
WorkerSupervisor.distribute_jobs(workers, [1, 2, 3, 4, 5])

# Collect results
results = Enum.map(1..5, fn _ ->
  receive do
    {:result, worker_id, result} ->
      {worker_id, result}
  end
end)

Example 3: GenServer (Recommended for Statefull Processes):

defmodule Store do
  use GenServer

  # Client API
  def start_link(initial_state) do
    GenServer.start_link(__MODULE__, initial_state, name: :store)
  end

  def get(key) do
    GenServer.call(:store, {:get, key})
  end

  def put(key, value) do
    GenServer.cast(:store, {:put, key, value})
  end

  # Server Callbacks
  @impl true
  def init(state) do
    {:ok, state}
  end

  @impl true
  def handle_call({:get, key}, _from, state) do
    {:reply, Map.get(state, key), state}
  end

  @impl true
  def handle_cast({:put, key, value}, state) do
    {:noreply, Map.put(state, key, value)}
  end
end

# Usage
{:ok, _} = Store.start_link(%{})
Store.put(:name, "Alice")
Store.put(:age, 30)
Store.get(:name)  # "Alice"

Advantages:

  • Fault tolerance with supervisor trees
  • Hot code reloading for live updates
  • Distributed computing built-in
  • Excellent for systems requiring high availability and resilience
  • Pattern matching makes message handling elegant

When to use:

  • Distributed systems requiring fault tolerance
  • Real-time applications (chat, notifications, IoT)
  • Telecom and messaging platforms
  • Systems requiring 99.99%+ uptime
  • When you need automatic process supervision and recovery

Goroutines and Channels (GO)

Package: goroutine (part of Go runtime)

GO’s concurrency model is built on goroutines (lightweight threads) and channels (typed conduits for communication between goroutines). This is fundamentally different from traditional thread-based models.

Key concepts:

  • Goroutines: Managed by the GO runtime, cheaper than OS threads (thousands to millions can coexist)
  • Channels: Enable safe communication and synchronization between goroutines
  • Scheduler: The GO runtime scheduler multiplexes many goroutines onto OS threads
  • CSP Model: Communicating Sequential Processes - “Share memory by communicating, don’t communicate by sharing memory”

Goroutine Lifecycle: CREATED → RUNNABLE → RUNNING → BLOCKED → TERMINATED

Key characteristics:

  • Ultra-lightweight (can spawn 100k+ goroutines easily)
  • Automatic scheduling by the runtime
  • Memory efficient (each goroutine ~2KB vs threads ~1-2MB)
  • First-class language support for concurrency

Thread Synchronization with Channels:

  • Unbuffered channels: Block until both sender and receiver are ready
  • Buffered channels: Decouple sender and receiver for limited buffering
  • Select statement: Multiplex communication on multiple channels
  • Mutex: Traditional mutual exclusion locks when needed

Example:

package main
import "fmt"

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Println("worker", id, "started  job", j)
        results <- j * 2
    }
}

func main() {
    const numJobs = 5
    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)

    // Start workers
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // Send jobs
    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)

    // Collect results
    for a := 1; a <= numJobs; a++ {
        fmt.Println(<-results)
    }
}

Advantages:

  • Exceptional performance for I/O-bound concurrent systems
  • Simple, intuitive syntax for parallel programming
  • Minimal runtime overhead
  • Excellent for building scalable network services, web servers, and microservices

When to use:

  • High-performance web servers and APIs
  • Microservices and distributed systems
  • Real-time applications with many concurrent connections
  • When you need predictable performance without GIL limitations

Tasks and Async/Await (C#)

Namespace: System.Threading.Tasks (Core .NET framework)

C# offers modern task-based asynchronous programming with async/await syntax, combined with traditional threading for CPU-bound work. Tasks represent pending or completed asynchronous operations.

Key concepts:

  • Tasks: Represent asynchronous operations, allowing composition and chaining
  • async/await: Syntactic sugar for writing asynchronous code that reads like synchronous code
  • ThreadPool: Managed thread reuse for background operations
  • Cancellation: Built-in cancellation token support for graceful shutdown
  • Continuations: Chain operations together with .ContinueWith()

Task Lifecycle: CREATED → SCHEDULED → RUNNING → COMPLETED (or FAULTED, CANCELED)

Key characteristics:

  • Modern async/await syntax (similar to Python but more polished)
  • Excellent integration with frameworks (ASP.NET, Blazor)
  • Strong tooling support in Visual Studio
  • Automatic thread management
  • Built-in exception handling in async contexts

Example 1: Basic Async/Await Pattern:

using System;
using System.Threading.Tasks;

// Async method returns Task<T>
async Task<string> FetchDataAsync(string url)
{
    // Simulate async I/O (network call)
    await Task.Delay(1000);
    return $"Data from {url}";
}

// Usage
async Task Main()
{
    var result = await FetchDataAsync("https://api.example.com");
    Console.WriteLine(result);  // Data from https://api.example.com
    
    // Concurrent operations
    var task1 = FetchDataAsync("url1");
    var task2 = FetchDataAsync("url2");
    var task3 = FetchDataAsync("url3");
    
    var results = await Task.WhenAll(task1, task2, task3);
    foreach (var data in results)
    {
        Console.WriteLine(data);
    }
}

Example 2: Task Parallel Processing (CPU-Bound):

using System;
using System.Threading.Tasks;

// CPU-intensive work distributed across threads
int ComputeHeavy(int number)
{
    long result = 0;
    for (long i = 0; i < 100_000_000; i++)
        result += i * number;
    return (int)(result % int.MaxValue);
}

Task Main()
{
    int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8 };
    
    // Parallel.ForEach distributes work across ThreadPool threads
    Parallel.ForEach(numbers, number =>
    {
        var result = ComputeHeavy(number);
        Console.WriteLine($"Number {number}: {result}");
    });
    
    // Or with Task.Run for explicit task control
    var tasks = numbers.Select(n => Task.Run(() => ComputeHeavy(n))).ToArray();
    var results = Task.WaitAll(tasks);
    
    return Task.CompletedTask;
}

Example 3: Producer-Consumer Pattern with Channels:

using System;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;

async Task ProducerAsync(ChannelWriter<int> writer)
{
    for (int i = 0; i < 10; i++)
    {
        await writer.WriteAsync(i);
        await Task.Delay(100);
    }
    writer.TryComplete();  // Signal completion
}

async Task ConsumerAsync(ChannelReader<int> reader)
{
    await foreach (var item in reader.ReadAllAsync())
    {
        Console.WriteLine($"Consumed: {item}");
        await Task.Delay(200);
    }
}

async Task Main()
{
    var channel = Channel.CreateUnbounded<int>();
    
    // Run producer and consumer concurrently
    var producer = ProducerAsync(channel.Writer);
    var consumer = ConsumerAsync(channel.Reader);
    
    await Task.WhenAll(producer, consumer);
    Console.WriteLine("Done!");
}

Example 4: Cancellation with CancellationToken:

using System;
using System.Threading;
using System.Threading.Tasks;

async Task LongRunningOperationAsync(CancellationToken ct)
{
    try
    {
        for (int i = 0; i < 10; i++)
        {
            ct.ThrowIfCancellationRequested();  // Check for cancellation
            Console.WriteLine($"Working... {i}");
            await Task.Delay(1000, ct);  // Async delay respects cancellation
        }
        Console.WriteLine("Completed successfully");
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("Operation was cancelled");
    }
}

async Task Main()
{
    using var cts = new CancellationTokenSource();
    
    // Cancel after 3.5 seconds
    cts.CancelAfter(3500);
    
    await LongRunningOperationAsync(cts.Token);
}

Example 5: Custom Async Iterator (Async Enumerable):

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

// Returns IAsyncEnumerable - can be awaited in foreach
async IAsyncEnumerable<int> FetchDataStreamAsync()
{
    for (int i = 1; i <= 5; i++)
    {
        // Simulate async data fetching
        await Task.Delay(500);
        yield return i * 10;
    }
}

async Task Main()
{
    // Await foreach for async iteration
    await foreach (var item in FetchDataStreamAsync())
    {
        Console.WriteLine($"Item: {item}");
    }
}

Example 6: Task Synchronization with Lock:

using System;
using System.Threading.Tasks;

class Counter
{
    private int _count = 0;
    private readonly object _lock = new object();
    
    public void Increment()
    {
        lock (_lock)  // Mutual exclusion
        {
            _count++;
        }
    }
    
    public int GetCount()
    {
        lock (_lock)
        {
            return _count;
        }
    }
    
    // Modern alternative: ReaderWriterLockSlim or Semaphore
}

async Task Main()
{
    var counter = new Counter();
    
    // Multiple concurrent increments
    var tasks = Enumerable.Range(0, 10)
        .Select(_ => Task.Run(() => counter.Increment()))
        .ToArray();
    
    await Task.WhenAll(tasks);
    Console.WriteLine($"Final count: {counter.GetCount()}");  // 10
}

Advantages:

  • Modern, clean async/await syntax
  • Excellent framework integration (ASP.NET Core, Blazor)
  • Strong Visual Studio tooling and debugger support
  • Task composition with WhenAll, WhenAny, ContinueWith
  • Built-in cancellation token support
  • Good performance for both I/O and CPU-bound work

When to use:

  • Enterprise applications and business logic
  • ASP.NET Core web services and APIs
  • Windows desktop applications
  • Game development (Unity)
  • Systems where .NET ecosystem is already in use

Comparison Summary

AspectPythonElixirC#GO
Concurrency ModelThreads (limited by GIL), async/await, multiprocessingLightweight processes (BEAM VM)Threads, async/await, TasksGoroutines, Channels
PerformanceGood for I/O, limited for CPU tasksExcellent for high concurrencyGood balanceExcellent for I/O & concurrent systems
ScalabilityModerate (GIL bottleneck for threads)Excellent (handle millions of processes)Good (modern .NET)Exceptional (millions of goroutines)
Learning CurveLow for imperative programmersSteeper (functional paradigm)Moderate (familiar C-like syntax)Very Low (simple goroutine syntax)
Memory per Unit~1-2 MB per thread~10-100 KB per process~500 KB per task~2 KB per goroutine
Best ForData processing, web APIs, scriptingTelecom, messaging, real-time systemsEnterprise apps, Windows integrationWeb servers, microservices, distributed systems
Async Supportasyncio (manual)OTP (automatic)Task-based (modern)Native (first-class)
Use Case Sweet SpotBackend services, ML/AIFault-tolerant distributed systemsEnterprise applicationsHigh-concurrency services, APIs

Performance Benchmarks (4-Language Comparison)

Benchmark 1: Goroutine/Thread Creation Speed

Creating and spawning concurrent units:

LanguageConcurrent UnitsCreation TimeMemory/UnitTotal Memory
GO1,000,000 goroutines~1-2 seconds~2 KB~2 GB
Python (threads)10,000 threads~5-10 seconds~1-2 MB~10-20 GB
C# (tasks)100,000 tasks~2-3 seconds~500 KB~50 GB
Elixir100,000 processes~3-5 seconds~10-50 KB~1-5 GB

References: GO concurrency patterns, Python threading limits

Benchmark 2: Web Server Concurrent Connections

Handling concurrent HTTP requests (simulated):

LanguageConcurrent RequestsThroughput (req/s)Memory UsageLatency (p99)
GO (net/http)100,000~50,000-100,000200-300 MB<10ms
Python (asyncio)10,000~3,000-5,000500 MB50-100ms
C# (ASP.NET Core)50,000~20,000-40,000400-600 MB20-50ms
Elixir (Phoenix)100,000+~30,000-60,000300-500 MB10-20ms

Benchmark sources:

Benchmark 3: CPU-Intensive Tasks (Fibonacci)

Computing fibonacci(40) with 4 concurrent tasks:

Python (multiprocessing): ~4.2 seconds (4 cores) / ~16.8 seconds (1 thread, GIL)
Elixir:                   ~5.1 seconds (4 cores)
C# (Parallel):            ~3.8 seconds (4 cores)
GO:                       ~3.5 seconds (4 cores)

Key insight: Python threading is 4x slower due to GIL; multiprocessing works but has overhead.

References:

Benchmark 4: Memory Efficiency at Scale

Spawning 100K concurrent units:

LanguageTotal MemoryMemory per UnitContext Switch Time
GO~200 MB~2 KB<1μs (user-space)
Python (processes)~100-200 GB~1-2 MB~10μs (OS kernel)
C# (ThreadPool)~20-30 GB~500 KB~5-10μs (OS kernel)
Elixir~5-10 GB~50-100 KB~1-5μs (BEAM scheduled)

Performance characteristics:

  • GO: Unbeatable at scale due to user-space scheduling and minimal overhead
  • Elixir: Excellent BEAM VM scheduler, competes with GO for massive concurrency
  • C#: Good for 1000s of concurrent operations, ThreadPool limits at 10K+
  • Python: Struggles beyond 1000 OS threads; multiprocessing adds IPC overhead

References:

Real-World Recommendations

Use GO when:

  • Building high-performance API servers (millions of concurrent connections)
  • You need simplicity and fast development with excellent concurrency
  • Performance predictability is critical
  • Example: HTTP servers, gRPC services, microservices, WebSocket servers

Use Python when:

  • Rapid prototyping and developer productivity are priorities
  • Integrating with rich data science/ML ecosystem
  • GIL limitations are acceptable for I/O-bound workloads
  • Example: Web services (FastAPI, asyncio), data pipelines, backend APIs

Use Elixir when:

  • Building distributed, fault-tolerant systems requiring high availability
  • Need Erlang’s battle-tested OTP framework
  • Message passing and actor model fit your problem domain
  • Example: Real-time systems, telecom, live updates, messaging platforms

Use C# when:

  • Enterprise ecosystem and Windows integration are needed
  • Using .NET Core for cross-platform development
  • Familiar C-like syntax with enterprise tooling
  • Example: Enterprise applications, cloud services (Azure), Windows applications

Seven Concurrency Models

The book “Seven Concurrency Models in Seven Weeks” by Paul Butcher introduces seven fundamental approaches to concurrency:

  1. Threads and Locks - Traditional shared-memory concurrency
  2. Functional Programming - Immutability and functional transformations
  3. Separation of Concerns - Actor model and message passing
  4. Shared Variable Concurrency - Clojure’s STM (Software Transactional Memory)
  5. Map-Reduce - Distributed data processing
  6. Lambda Architecture - Batch and real-time processing
  7. Data Parallelism - GPU and vector processing

Key Takeaways

  • Python: Excellent for I/O-bound tasks via asyncio; avoid threading for CPU tasks due to GIL; consider multiprocessing for CPU-bound work
  • Elixir: Superior for systems requiring massive concurrency, fault tolerance, and distributed message passing
  • C#: Modern, versatile with excellent async/await support; good balance of performance, scalability, and enterprise ecosystem
  • GO: Best-in-class for concurrent I/O-bound systems; exceptional scalability (millions of goroutines); simple, elegant syntax

Golden Rule:

  • High-concurrency I/O-bound systems → GO or Elixir
  • Enterprise applications with Windows integration → C#
  • Machine learning, data science, or existing Python expertise → Python
  • Fault-tolerant distributed systems → Elixir

Resources

Books:

Official Documentation:

Articles & Benchmarks:

Video Resources: