Appendix A: Bounded Channel Limits

In our BoundedChannelSender:send, BoundedChannel:set_waker_sendr and BoundedChannel:try_wake we have made some decisions that make our implementation a little easier to read/write but might cause some problems if we end up depending on it.

To review, any time BoundedChannelSender:send would yield we create a temporary table to handle our setwaker calls and in BoundedChannel:set_waker_sendr we use that table as the key to _wakers.sendr to store/remove the waker function.

In BoundedChannel:try_wake we use the next function to choose which entry of _wakers.sendr to call. The next function will always return the "first" key/value pair but what does that mean, how can Lua tables be ordered? When a table is created, it is assigned a memory address, unique to that table, we can see what the address is by using the default __tostring metamethod.

lua -e "print({})"
table: 0x5581bc0236a0

In the above, we can see the memory address of an empty table is 0x5581bc0236a0, which will change if we were to run it again. If we were to use this table as the key in another table that table would look something like this.

{
  [0x5581bc0236a0] = "first element"
}

So, let's look at how Lua might order a table like this with more than one key.

local tables = {}
for i=1, 10 do
  tables[{}] = i
end

for i=1, 10 do
  local t, v = next(tables)
  local tp = tonumber(tostring(t):match("table: 0x(.+)"), 16)
  print(tp, v)
  tables[t] = nil
end

This example will create a table tables then loop 10 times assigning tables[{}] with i. In a second loop to 10, we use the next function to pull the "first" key/value pair from our tables. We then convert t by converting it into a hex string and then converting it back into a number, which may be easier to read for some than trying to tell which hex value is larger than another. It prints out the table representation and which i was assigned to it then removes it from tables by assigning tables[t] with nil. If we run this once we might see something like.

93877018273488  1
93877018274240  9
93877018274016  7
93877018273792  5
93877018273616  3
93877018274352  10
93877018274128  8
93877018273904  6
93877018273680  4
93877018273552  2

At first, it looks like it might go in order because we get 1 but then we see the second return from next is 9. If we run it again we might see something like:

93837605766864  2
93837605767120  6
93837605767376  10
93837605766928  3
93837605767184  7
93837605766992  4
93837605767248  8
93837605766800  1
93837605767056  5
93837605767312  9

This time, we get 2 first and 9 last which means that we can expect the order to be somewhat random. We can also see pretty obviously that Lua has ordered the keys by the lowest memory address first. That means that next will return the waker associated to the wake_t that has the lowest memory address. So what happens if one coroutine always gets the lowest value? It could starve other coroutines from being able to send.

This might not be all bad though, randomness can be good since we don't want to show a preference and randomness does just that but that would require something like "a normalized distribution" which would mean it would take a very long time to see the same order. Let's see how random our temporary table keys are.

local m = {
  sets = {}
}

local table_size = 9

function m:add_set(set)
  for idx, existing in ipairs(self.sets) do
    local exact_match = false
    local left_t, left_v, right_t, right_v
    for i=1,table_size do
      left_t, left_v = next(set, left_t)
      right_t, right_v = next(existing, right_t)
      exact_match = left_v == right_v
      if not exact_match then
        break
      end
    end
    
    if exact_match then
      table.insert(self.sets, set)
      return {
        first = existing,
        first_idx = idx,
        second = set,
        second_idx = #self.sets
      }
    end
  end
  table.insert(self.sets, set)
end

local function gen_set()
  local tables = {}
  for i=1, table_size do
    tables[{}] = i
  end
  return tables
end

local result

repeat
  result = m:add_set(gen_set())
until result

print("RESULT")
print(result.first_idx, result.second_idx)
print("f,s")
for i=1,table_size do
  local t1, v1 = next(result.first)
  local t2, v2 = next(result.second)
  print(string.format("%s,%s", v1, v2))
  result.first[t1] = nil
  result.second[t2] = nil
end

Here we have extended our example to allow for repeating the creation of tables and then checking to see if the new version matches any of the previous versions. We reduced the number of entries to 9 to make it easier to read the results but otherwise, get_set will create the same table as our original example. We have defined a table to hold all of our sets named m and defined a method there add_set which will either return nil if the set argument isn't already in the list or a results table if it was found. So what happens if we run this?

RESULT
1       4
f,s
4,4
8,8
1,1
5,5
9,9
2,2
6,6
3,3
7,7

It looks like it only took us 3 sets to find the exact same order. Considering that 0-9 have a potential number of combinations greater than 300,000 it seems that our distribution not very normal.