🎓 A1Learner
🔍
0 of 46 lessons completed

Zig Project: Task Scheduler

A task scheduler is code that holds a list of steps and runs them in order, one after another. Each step is a function, and the scheduler calls each one with the same shared state.

Steps in order

Three steps share one total. Adding one, multiplying by ten, and adding one again gives a final total that depends only on the order.

const std = @import("std");
const Step = *const fn (ctx: *i64) void;
const Task = struct {
    name: []const u8,
    run: Step,
};
fn addOne(ctx: *i64) void {
    ctx.* += 1;
}
fn timesTen(ctx: *i64) void {
    ctx.* *= 10;
}
pub fn main() !void {
    var total: i64 = 0;
    const tasks = [_]Task{
        .{ .name = "add-one", .run = addOne },
        .{ .name = "times-ten", .run = timesTen },
        .{ .name = "add-one", .run = addOne },
    };
    const out = std.io.getStdOut().writer();
    for (tasks) |t| {
        t.run(&total);
        try out.print("{s}: {d}\n", .{ t.name, total });
    }
}

Stopping on a failed step

The second step fails, so the scheduler stops there. The third step never runs, and the report names the step that failed.

const std = @import("std");
const TaskError = error{TaskFailed};
const Step = *const fn (ctx: *i64) TaskError!void;
const Task = struct {
    name: []const u8,
    run: Step,
};
fn first(ctx: *i64) TaskError!void {
    ctx.* += 1;
}
fn second(ctx: *i64) TaskError!void {
    _ = ctx;
    return error.TaskFailed;
}
fn third(ctx: *i64) TaskError!void {
    ctx.* += 100;
}
pub fn main() !void {
    var total: i64 = 0;
    const tasks = [_]Task{
        .{ .name = "first", .run = first },
        .{ .name = "second", .run = second },
        .{ .name = "third", .run = third },
    };
    const out = std.io.getStdOut().writer();
    for (tasks) |t| {
        t.run(&total) catch {
            try out.print("stopped: {s}\n", .{t.name});
            break;
        };
        try out.print("ran: {s}\n", .{t.name});
    }
}

One step on a thread

A worker thread adds five to a counter while the main flow waits. Joining the thread finishes the step before the result is read and printed.

const std = @import("std");
const Counter = struct {
    total: i64 = 0,
};
fn work(state: *Counter) void {
    state.total += 5;
}
pub fn main() !void {
    var state = Counter{};
    var t = try std.Thread.spawn(.{}, work, .{&state});
    t.join();
    const out = std.io.getStdOut().writer();
    try out.print("worker total: {d}\n", .{state.total});
}

Example

const std = @import("std");
const Step = *const fn (ctx: *i64) void;
const Task = struct {
    name: []const u8,
    run: Step,
};
fn markDone(ctx: *i64) void {
    ctx.* += 1;
}
pub fn main() !void {
    var done: i64 = 0;
    const tasks = [_]Task{
        .{ .name = "fetch", .run = markDone },
        .{ .name = "build", .run = markDone },
        .{ .name = "report", .run = markDone },
    };
    const out = std.io.getStdOut().writer();
    for (tasks) |t| {
        t.run(&done);
        try out.print("{s} done\n", .{t.name});
    }
    try out.print("done: {d}/3\n", .{done});
}

Key points

  • Task scheduler: a task scheduler is code that runs a list of steps in order, one after another.
  • Step: a step is a function that takes the shared state and changes it in one small way.
  • Storing steps as function pointers in an array lets the scheduler loop over them without knowing each step in advance.
  • Running steps in array order makes the result deterministic, since the same steps always run in the same order.
  • A step that returns an error stops the run, and the scheduler reports which step failed instead of running the rest.
  • A thread runs a step beside the main flow, and joining the thread waits until that step is fully done.
  • Shared state is read only after every thread is joined, so no result is read while it is still changing.

Three steps share one counter of finished work, and each step simply marks one more piece done. The scheduler runs them in array order and prints each name as it finishes. The final line counts all three steps, proving the whole run completed in order.

Try it Yourself

Edit the code, then press Run.

Running is simulated

Exercise

A scheduler runs steps A, then B, then C, but B returns an error. Which steps run?