# Threads in Zig

### Threads in Zig

A thread is a separate path of execution inside one program.

A normal program starts with one thread. That thread begins at `main`, runs each statement in order, and ends when `main` returns.

```zig
pub fn main() void {
    // one thread runs this code
}
```

A multithreaded program has more than one thread running at the same time. One thread may read a file. Another thread may handle a network connection. Another thread may perform a calculation.

Threads are useful when you want to do independent work in parallel.

#### The Basic Idea

Imagine this program:

```zig
const std = @import("std");

fn worker() void {
    std.debug.print("Hello from worker thread\n", .{});
}

pub fn main() !void {
    const thread = try std.Thread.spawn(.{}, worker, .{});
    thread.join();

    std.debug.print("Back in main thread\n", .{});
}
```

This program creates a new thread.

The new thread runs this function:

```zig
fn worker() void {
    std.debug.print("Hello from worker thread\n", .{});
}
```

The main thread continues after calling `std.Thread.spawn`.

This line waits for the worker thread to finish:

```zig
thread.join();
```

Without `join`, the main function could finish before the worker thread has completed its work.

#### What `std.Thread.spawn` Does

The general shape is:

```zig
const thread = try std.Thread.spawn(.{}, function_name, arguments);
```

The first argument is the thread configuration:

```zig
.{}
```

For now, this means “use the default thread options.”

The second argument is the function the new thread should run:

```zig
worker
```

The third argument is a tuple of arguments passed to that function:

```zig
.{}
```

An empty tuple means the function receives no arguments.

So this:

```zig
try std.Thread.spawn(.{}, worker, .{});
```

means:

Create a new thread. Run `worker`. Pass it no arguments.

#### Passing Arguments to a Thread

A thread function can receive arguments like any other function.

```zig
const std = @import("std");

fn printNumber(n: u32) void {
    std.debug.print("number = {}\n", .{n});
}

pub fn main() !void {
    const thread = try std.Thread.spawn(.{}, printNumber, .{42});
    thread.join();
}
```

The function expects one argument:

```zig
fn printNumber(n: u32) void
```

The thread receives that argument here:

```zig
.{42}
```

The argument list must match the function parameters.

#### Multiple Threads

You can start more than one thread.

```zig
const std = @import("std");

fn worker(id: u32) void {
    std.debug.print("worker {} started\n", .{id});
}

pub fn main() !void {
    const t1 = try std.Thread.spawn(.{}, worker, .{1});
    const t2 = try std.Thread.spawn(.{}, worker, .{2});
    const t3 = try std.Thread.spawn(.{}, worker, .{3});

    t1.join();
    t2.join();
    t3.join();

    std.debug.print("all workers finished\n", .{});
}
```

Each call to `spawn` creates a new thread.

Each call to `join` waits for one thread to finish.

The output order is not guaranteed. You may see:

```text
worker 1 started
worker 3 started
worker 2 started
all workers finished
```

That is normal. Once threads run at the same time, the operating system decides when each thread gets CPU time.

#### Threads Share Memory

Threads inside the same process share memory.

This is powerful, but dangerous.

If two threads access the same data at the same time, and at least one thread writes to it, you can get a data race.

Here is a bad example:

```zig
const std = @import("std");

var counter: u32 = 0;

fn incrementMany() void {
    var i: u32 = 0;
    while (i < 1000) : (i += 1) {
        counter += 1;
    }
}

pub fn main() !void {
    const t1 = try std.Thread.spawn(.{}, incrementMany, .{});
    const t2 = try std.Thread.spawn(.{}, incrementMany, .{});

    t1.join();
    t2.join();

    std.debug.print("counter = {}\n", .{counter});
}
```

You might expect the final value to be `2000`.

But this program is not safe. Both threads update `counter` at the same time.

This line looks simple:

```zig
counter += 1;
```

But internally it means something like:

1. read `counter`
2. add `1`
3. write the result back

Two threads can interleave those steps and lose updates.

#### Use a Mutex to Protect Shared Data

A mutex is a lock.

Only one thread can hold the lock at a time. If another thread tries to lock the same mutex, it must wait.

```zig
const std = @import("std");

var counter: u32 = 0;
var mutex = std.Thread.Mutex{};

fn incrementMany() void {
    var i: u32 = 0;
    while (i < 1000) : (i += 1) {
        mutex.lock();
        counter += 1;
        mutex.unlock();
    }
}

pub fn main() !void {
    const t1 = try std.Thread.spawn(.{}, incrementMany, .{});
    const t2 = try std.Thread.spawn(.{}, incrementMany, .{});

    t1.join();
    t2.join();

    std.debug.print("counter = {}\n", .{counter});
}
```

Now the update is protected:

```zig
mutex.lock();
counter += 1;
mutex.unlock();
```

Only one thread can change `counter` at a time.

A safer style uses `defer`:

```zig
mutex.lock();
defer mutex.unlock();

counter += 1;
```

This ensures the mutex is unlocked when the current scope exits.

#### Passing Shared State Explicitly

Global variables are easy for small examples, but larger programs should usually pass shared state explicitly.

```zig
const std = @import("std");

const State = struct {
    mutex: std.Thread.Mutex = .{},
    counter: u32 = 0,
};

fn incrementMany(state: *State) void {
    var i: u32 = 0;
    while (i < 1000) : (i += 1) {
        state.mutex.lock();
        defer state.mutex.unlock();

        state.counter += 1;
    }
}

pub fn main() !void {
    var state = State{};

    const t1 = try std.Thread.spawn(.{}, incrementMany, .{&state});
    const t2 = try std.Thread.spawn(.{}, incrementMany, .{&state});

    t1.join();
    t2.join();

    std.debug.print("counter = {}\n", .{state.counter});
}
```

This is better because the shared data is grouped in one place.

```zig
const State = struct {
    mutex: std.Thread.Mutex = .{},
    counter: u32 = 0,
};
```

The thread function receives a pointer:

```zig
fn incrementMany(state: *State) void
```

That means each thread works with the same shared `State`.

#### Thread Lifetime Matters

A thread must not use memory that has already gone out of scope.

This is unsafe:

```zig
fn startThread() !std.Thread {
    var value: u32 = 123;
    return try std.Thread.spawn(.{}, usePointer, .{&value});
}
```

The variable `value` lives only inside `startThread`.

When `startThread` returns, `value` is gone. The new thread may still hold a pointer to invalid memory.

The rule is simple:

A thread must not receive a pointer to data that may disappear before the thread finishes.

The easiest beginner rule is:

Create the data in the same scope where you also join the thread.

```zig
pub fn main() !void {
    var value: u32 = 123;

    const thread = try std.Thread.spawn(.{}, usePointer, .{&value});
    thread.join();
}
```

Here, `value` stays alive until after `join`.

#### Thread Functions and Errors

A thread function can return an error union, but the error does not automatically return to the parent thread in the same way that `try` works in one call stack.

For beginners, start with thread functions that return `void`:

```zig
fn worker() void {
    // do work
}
```

If a worker can fail, store the result somewhere shared and protect it with synchronization, or design the worker to report errors through a queue, channel-like structure, or shared result object.

A simple pattern is:

```zig
const std = @import("std");

const Result = struct {
    mutex: std.Thread.Mutex = .{},
    failed: bool = false,
};

fn worker(result: *Result) void {
    const ok = false;

    if (!ok) {
        result.mutex.lock();
        defer result.mutex.unlock();

        result.failed = true;
    }
}

pub fn main() !void {
    var result = Result{};

    const thread = try std.Thread.spawn(.{}, worker, .{&result});
    thread.join();

    if (result.failed) {
        std.debug.print("worker failed\n", .{});
    }
}
```

This is not the only way to report errors, but it shows the main idea: the parent thread and worker thread need an explicit communication path.

#### Threads Are Not Always the Best Tool

Threads are powerful, but they add complexity.

Use threads when work is truly independent or parallel. Good examples include:

| Use case | Why threads help |
|---|---|
| CPU-heavy work | Multiple CPU cores can run work in parallel |
| Blocking file or network work | One thread can wait while another keeps working |
| Background jobs | Long work can run without stopping the main flow |
| Servers | Different connections or tasks can be handled concurrently |

Avoid threads when a simple loop is enough. A single-threaded program is easier to test, easier to debug, and easier to reason about.

#### A Good Beginner Rule

Start with one thread.

Add threads only when you can clearly answer these questions:

What data does each thread own?

What data is shared?

Who protects the shared data?

When does each thread finish?

Who calls `join`?

If those answers are unclear, the program is not ready for threads.

Threads in Zig are explicit. You create them directly, pass data directly, synchronize directly, and wait for them directly. That makes them a good fit for systems programming, but it also means you must be precise.

