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!