Spawn

At the core of cosock is the ability to wrap any operation in a coroutine and register that with cosock. For this cosock exports the function cosock.spawn. This function takes 2 arguments, the first is a function that will be our coroutine, and the second is a name for that coroutine.

For example, this is a simple program that will spawn a single coroutine, which will print the current timestamp and the word "tick" and then sleep for 1 second in a loop forever.

--basic_spawn.lua
local cosock = require "cosock"

cosock.spawn(function()
  while true do
    print(cosock.socket.gettime(), "tick")
    cosock.socket.sleep(1)
  end
end, "clock")
cosock.run()

The act of calling cosock.spawn allows us to use the non-blocking cosock.socket.sleep function. This means we could extend our application to not only print this message every second but use the time this coroutine is sleeping to perform some other work. Let's extend our little example a bit.

--less_basic_spawn.lua
local cosock = require "cosock"

local function tick_task()
  while true do
    print(cosock.socket.gettime(), "tick")
    cosock.socket.sleep(2)
  end
end

local function tock_task()
  cosock.socket.sleep(1)
  while true do
    print(cosock.socket.gettime(), "tock")
    cosock.socket.sleep(2)
  end
end

cosock.spawn(tick_task, "tick-task")
cosock.spawn(tock_task, "tock-task")
cosock.run()

Very similar to our last example, this time we are spawning 2 coroutines one will print "tick" every two seconds the other will wait 1 second and then print "tock" every two seconds. This should result in a line getting printed to the terminal once a second alternating between our two strings. Notice though, there is a fair amount of code duplication as tick_task and tock_task are nearly identical. This is mostly driven by the fact that the first argument to cosock.spawn is a function that takes no arguments and returns no values which means we can't ask cosock to pass in any arguments. One way we can get around this is by using closures. So instead of passing a function to cosock.spawn we can return a function from another function and use it as the argument to cosock.spawn. For example:

--even_less_basic_spawn.lua
local cosock = require "cosock"

local function create_task(name, should_sleep_first)
  return function()
    if should_sleep_first then
      cosock.socket.sleep(1)
    end
    while true do
      print(cosock.socket.gettime(), name)
      cosock.socket.sleep(2)
    end
  end
end

cosock.spawn(create_task("tick", false), "tick-task")
cosock.spawn(create_task("tock", true), "tock-task")
cosock.run()

Notice here that create_task returns a function but takes a name argument and a should_sleep_first argument which are available to our returned function.

Now, let's consider our first example which may not look like it but is very similar to our tick/tock example.

Instead of using cosock.socket.sleep to tell cosock we are waiting around for something, it uses the receive method on a cosock.socket.tcp. Let's break down what is happening in that example.

To start, both tasks will be resumed which means that cosock has selected it to run, we can't say for sure which task will get resumed first which is why we used a cosock.channel to make the client task wait until the server was ready. Shortly after resuming, each task eventually calls some method that will yield which means that it is waiting on something so cosock can run another task. For the server, the first time we yield is in a call to accept, if the client hasn't already called connect we would end up blocking so instead of blocking, we let another task work, when we finally have a client connected cosock will wake us back up again. On the client-side we first yield on a call to channel:receive, if the server hasn't sent the port number we would end up blocking that task from calling bind so we let the other task work until we finally have a port number and then cosock will wake us back up.

This pattern continues, each task running exclusively until it needs to wait for something yielding control back to cosock. When the thing we were waiting for is ready, we can continue running again.

In both our tick/tock examples and our client/server example, we reach a point where cosock is just handing control from task 1 to task 2 and back again in an infinite loop. In a more real-world program, you might see any number of tasks, that need to be juggled. In our next example, we will extend the client/server example to handle any number of clients.

-- clients_server.lua
local cosock = require "cosock"
local socket = require "cosock.socket"
local ip = "0.0.0.0"
local server = socket.tcp()

local number_of_clients = 10

--- Since the clients and server are in the same application
--- we can use an OS assigned port and share it across the
--- two tasks, to coordinate the two tasks to start in the order
--- we want, we can use a cosock channel to make sure both tasks
--- have the same port number
local port_tx, port_rx = cosock.channel.new()

--- Spawn a task for handling the server side of the socket
cosock.spawn(function()
  server:bind(ip, 0)
  local _ip, p = server:getsockname()
  port_tx:send(p)
  server:listen()
  while true do
    local client = server:accept()
    cosock.spawn(function()
      while true do
        local request = assert(client:receive())
        print(string.format("received %q", request))
        if request:match("ping") then
          print("sending pong")
          client:send("pong\n")
        else
          client:close()
          break
        end
      end
    end)
  end
end, "server task")

--- A single client task
---@param id integer The task's identifier
---@param port integer The server's port number
local function spawn_client(id, port)
  print("spawn_client", id, port)
  local client = socket.tcp()
  client:connect(ip, port)
  while true do    
    print("sending ping", id)
    client:send(string.format("ping %s\n", id))
    local request = assert(client:receive())
    assert(request == "pong")
    socket.sleep(0.5)
  end
end

--- Wait for the port from the server task and then
--- spawn the `number_of_clients` client tasks
local function spawn_clients()
  local port = assert(port_rx:receive())
  for i=1,number_of_clients do
    cosock.spawn(function()
      spawn_client(i, port)
    end, string.format("client-task-%s", i))
  end
end

--- Spawn a bunch of client tasks
cosock.spawn(function()
  spawn_clients()
end, "client task")

--- Finally we tell cosock to run all our coroutines until they are done
--- which should be forever
cosock.run()

Surprisingly little has changed. First, we updated the socket task to call accept more than once and then pass the returned client into its own task to receive/send in a loop there.

For the client-side, we broke the client send/receive loop into its own task and added a parent task to wait for the port number and then cosock.spawn a bunch of client tasks.

If you were to run this example, you would see that the print statements end up in random order!