Concurrency, Reimagined: From CSP to Erlang, Go, .NET — and Even Vue
Concurrency is one of those topics where theory sounds clean… until you open a production system and see threads, locks, retries, and “temporary” fixes everywhere.
So let’s do something different.
Instead of starting with threads, we’ll start with an idea—Communicating Sequential Processes (CSP)—and prove its value the way engineers prefer:
👉 through code.
The Core Idea: Don’t Share Memory — Communicate
CSP flips the traditional model:
- ❌ Threads + shared memory + locks
- ✅ Independent processes + message passing
Let’s make that concrete.
Traditional Shared-State (The Pain)
int counter = 0;
object _lock = new object();
void Increment()
{
lock (_lock)
{
counter++;
}
}
Problems:
- Locks everywhere
- Easy to forget
- Deadlocks, contention, performance issues
CSP-Style Thinking (The Shift)
Instead of sharing counter, we own it in one place and send messages to it.
process Counter:
value = 0
loop:
msg = receive()
if msg == "increment":
value += 1
No locks. No races. Just messages.
Erlang: Concurrency as a First-Class Citizen
Erlang took this idea and built a production-ready system around it.
Lightweight Processes + Message Passing
loop(Value) ->
receive
increment ->
loop(Value + 1);
{get, Sender} ->
Sender ! Value,
loop(Value)
end.
Spawning a process:
Pid = spawn(fun() -> loop(0) end).
Pid ! increment.
Pid ! {get, self()}.
receive
Value -> io:format("Value = ~p~n", [Value])
end.
Why This Matters
- Each process is isolated
- No shared memory
- Communication is explicit
This model scales insanely well—and that’s exactly why systems like RabbitMQ could thrive on it.
RabbitMQ Connection
RabbitMQ isn’t just “a queue”—it’s a system built on:
- Message passing
- Process isolation
- Fault tolerance
Which means when you use it:
# Python producer example
channel.basic_publish(
exchange='',
routing_key='task_queue',
body='Hello CSP world!'
)
You’re participating in a system deeply aligned with CSP principles.
Go: CSP Built Into the Language
Go didn’t just adopt CSP—it made it idiomatic.
Goroutines + Channels
func counter(ch chan int) {
value := 0
for {
value++
ch <- value
}
}
func main() {
ch := make(chan int)
go counter(ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
}
Fan-Out / Fan-In Pattern
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
results <- j * 2
}
}
func main() {
jobs := make(chan int, 5)
results := make(chan int, 5)
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
for a := 1; a <= 5; a++ {
fmt.Println(<-results)
}
}
.NET Channels: CSP Meets Async/Await
.NET didn’t start with CSP—but it’s clearly moving in that direction.
Basic Channel Example
using System.Threading.Channels;
var channel = Channel.CreateUnbounded<int>();
// Producer
_ = Task.Run(async () =>
{
for (int i = 0; i < 5; i++)
{
await channel.Writer.WriteAsync(i);
}
channel.Writer.Complete();
});
// Consumer
await foreach (var item in channel.Reader.ReadAllAsync())
{
Console.WriteLine(item);
}
Pipeline Example
var channel = Channel.CreateUnbounded<int>();
async Task Producer()
{
for (int i = 1; i <= 5; i++)
{
await channel.Writer.WriteAsync(i);
}
channel.Writer.Complete();
}
async Task Consumer()
{
await foreach (var item in channel.Reader.ReadAllAsync())
{
Console.WriteLine(item * 2);
}
}
await Task.WhenAll(Producer(), Consumer());
Vue: CSP Thinking in the Frontend
Here’s where things get interesting.
Frontend apps are implicitly concurrent:
- User interactions
- Network requests
- State updates
- Rendering cycles
Vue doesn’t implement CSP—but it echoes the same philosophy:
👉 Don’t let everything mutate shared state freely 👉 Force communication through clear pathways
The Problem: Shared Mutable State
const state = {
count: 0
}
function increment() {
state.count++
}
Now imagine multiple components touching this. It’s basically shared-memory concurrency again.
Vue’s Solution: Props Down, Events Up
<!-- Parent.vue -->
<Counter :count="count" @increment="count++" />
<!-- Counter.vue -->
<script setup>
defineProps(['count'])
const emit = defineEmits(['increment'])
</script>
<template>
<button @click="emit('increment')">
8
</button>
</template>
What’s happening:
- Child cannot mutate parent state directly
- It must send a message (event)
That’s conceptually very close to CSP:
| CSP Concept | Vue Equivalent |
|---|---|
| Process | Component |
| Channel | Event / Prop |
| Message | Emitted event |
Reactive Streams (A Familiar Pattern)
import { ref, watch } from 'vue'
const count = ref(0)
watch(count, (newValue) => {
console.log("Count changed:", newValue)
})
This behaves like a stream of values over time:
- Producers update state
- Consumers react automatically
Not pure CSP—but clearly aligned in spirit.
Connecting the Dots
| System | Concurrency Model | CSP Influence |
|---|---|---|
| Erlang | Actor model | Strong |
| RabbitMQ | Message broker | Indirect |
| Go | Goroutines + Channels | Direct |
| .NET | Channels | Emerging |
| Vue | Reactive + Events | Conceptual |
The Real Takeaway
Most engineers start here:
“How do I make shared memory safe?”
CSP nudges you toward a better question:
“Why is this shared in the first place?”
And once you start designing systems like this:
- Independent components
- Explicit communication
- Message-driven flow
You’ll notice something surprising:
👉 Concurrency stops feeling like a problem—and starts feeling like composition.
Final Thought
From backend systems to frontend frameworks, the same idea keeps showing up:
Systems work better when components talk instead of share.
CSP just gave us the vocabulary for it decades ago.
We’re still catching up.