Skip to content

Instantly share code, notes, and snippets.

@hugeblank
Created December 20, 2020 14:16
Show Gist options
  • Save hugeblank/845b6f9e06d586877d0493bb10d0a6a0 to your computer and use it in GitHub Desktop.
Save hugeblank/845b6f9e06d586877d0493bb10d0a6a0 to your computer and use it in GitHub Desktop.
raisin 4.0-pre.0 with the thread demo program
--[[ Raisin by Hugeblank
This code is my property, but I will let you use it so long as you don't redistribute this manager for
monetary gain and leave this comment block untouched. Add/remove code as you wish. Should you decide to freely
distribute with additional modifications, please credit yourself. :)
Raisin can be found on github at:
`https://github.com/hugeblank/raisin`
Demonstrations of the library can also be found at:
`https://github.com/hugeblank/raisin-demos`
]]
local function manager(listener)
local this = {} -- Thread/group creation and runner
local threads = {}
local assert = function(condition, message, level) -- Local assert function that has a third parameter so that you can set the level of the error
if not condition then -- If the condition is not met
level = level or 0
error(message, 3+level) -- Error at the level defined or 3 as the default, one level above here
end
end
assert(type(listener) == "function", "Invalid argument #1 (function expected, got "..type(listener)..")", -2)
local function sort(unsorted) -- TODO: Not use such a garbage sorting method
local sorted = {}
sorted[#sorted+1] = unsorted[1] -- Add the first item to start sorting
for i = 2, #unsorted do -- For each item other than that one
for j = 1, #sorted do -- Iterate over the sorted list
if unsorted[i].priority < sorted[j].priority then -- If the priority of the current unsorted item is less than the value of the current sorted item
table.insert(sorted, j, unsorted[i]) -- Insert it such that it will go before the sorted item in the sorted table
break -- Break out of the checking
elseif j == #sorted then -- OTHERWISE if this is the last iteration
sorted[#sorted+1] = unsorted[i] -- Tack the unsorted item onto the end of the sorted table
end
end
end
return sorted
end
local function resume(thread, event) -- Simple coroutine resume wrapper
local suc, err = coroutine.resume(thread.coro, table.unpack(event, 1, event.n))
assert(suc, err, 2)
if suc then
return err
end
end
this.run = function(onDeath) -- Function to execute thread managment
assert(type(onDeath) == "function", "Invalid argument #1 (function expected, got "..type(onDeath)..")")
local halt = false
local e = {} -- Event variable
local initial = {} -- Existing thread instances before manager started, for onDeath
for i = 1, #threads do
initial[#initial+1] = threads[i].instance
end
while true do -- Begin thread management
local s_threads = sort(threads) -- Sort threads by priority
for j = 1, #s_threads do -- For each sorted thread
local thread = s_threads[j]
if thread.enabled and coroutine.status(thread.coro) == "suspended" and (thread.event == nil or thread.event == e[1]) then
-- There's a lot going on here, a newline was a must.
-- If the group is enabled and the thread is enabled, and the thread is suspended and the target event is either nil, or equal to the event detected, or equal to terminate
while #thread.queue ~= 0 do -- until the queue is empty
if thread.enabled and coroutine.status(thread.coro) == "suspended" and (thread.event == nil or thread.event == thread.queue[1][1]) then
-- This line looks awfully familiar...
-- Factors in threads that self disable.
thread.event = resume(thread, thread.queue[1]) -- Process the queued event
end
table.remove(thread.queue, 1) -- Remove that event from the queue
end
thread.event = resume(thread, e) -- Process latest event
elseif not thread.enabled then -- OTHERWISE if the thread isn't enabled and isn't dead add the event to the thread queue
thread.queue[#thread.queue+1] = e
end
if coroutine.status(thread.coro) == "dead" then
local living = {} -- All living thread instances
for i = 1, #threads do
living[i] = threads[i].instance
end
halt = onDeath(thread.instance, living, initial) -- Trigger user defined onDeath function to determine whether to halt execution
for k = 1, threads do -- Search for the thread to remove
if threads[k] == thread then
table.remove(threads, k)
break
end
end
end
end
if halt then -- Check exit condition
return -- We're done here
end
e = table.pack(listener()) -- Pull a raw event, package it immediately
end
end
local interface = function(internal) -- General interface used for both groups and threads
return {
state = function() -- Whether the object is processing events/buffering them
return internal.enabled
end,
toggle = function(value) -- Toggle processing/buffering of events
internal.enabled = value or not internal.enabled
end,
getPriority = function() -- Get the current priority of the object
return internal.priority
end,
setPriority = function(value) -- Set the current priority of the object
assert(type(value) == "number", "Invalid argument #1 (number expected, got "..type(value)..")")
internal.priority = value
end,
remove = function() -- Remove the object from execution immediately
for i = 1, #threads do
if threads[i] == internal then
table.remove(threads, i)
return true
end
end
return false -- Object cannot be found
end
}
end
this.thread = function(func, priority) -- Initialize a thread
priority = priority or 0
assert(type(func) == "function", "Invalid argument #1 (function expected, got "..type(func)..")")
assert(type(priority) == "number", "Invalid argument #2 (number expected, got "..type(priority)..")")
func = coroutine.create(func) -- Create a coroutine out of the function
local internal = {
coro = func,
queue = {},
priority = priority,
enabled = true,
event = nil
}
internal.instance = interface(internal)
threads[#threads+1] = internal
return internal.instance
end
this.group = function(priority, onDeath) -- Initialize a group
assert(type(priority) == "number", "Invalid argument #1 (number expected, got "..type(priority)..")")
assert(type(onDeath) == "function", "Invalid argument #2 (function expected, got "..type(onDeath)..")")
priority = priority or 0
local subman = manager(listener)
local func = coroutine.create(function() subman.run(onDeath) end)
local internal = {
coro = func,
queue = {},
priority = priority,
enabled = true,
event = nil
}
internal.instance = interface(internal)
internal.instance.run = subman.run
internal.instance.thread = subman.thread
internal.instance.group = subman.group
return internal.instance
end
this.onDeath = {-- Template thread/group death handlers
waitForAll = function() -- Wait for all threads regardless of when added to die
return function(_, all)
return #all == 0
end
end,
waitForN = function(n) -- Wait for n threads regardless of when added to die
assert(type(n) == "number", "Invalid argument #1 (number expected, got "..type(n)..")")
local amt = 0
return function()
amt = amt+1
return amt == n
end
end,
waitForAllInitial = function() -- Wait for all threads created before runtime to die
return function(dead, _, init)
for i = 1, #init do
if init[i] == dead then
table.remove(init, i)
end
break
end
return #init == 0
end
end,
waitForNInitial = function(n) -- Wait for n threads created before runtime to die
assert(type(n) == "number", "Invalid argument #1 (number expected, got "..type(n)..")")
local amt = 0
return function(dead, _, init)
for i = 1, #init do
if init[i] == dead then
amt = amt+1
end
break
end
return amt == n
end
end,
-- The following "waitForXRuntime" functions assume that runtime threads were created before any initial thread died
waitForAllRuntime = function() -- Wait for all threads created during runtime to die
return function(dead, all, init)
for i = 1, #init do
if init[i] == dead then
return false
end
for j = 1, #all do
if all[j] == init[i] then
table.remove(all, j)
end
end
end
return #all == 0
end
end,
waitForNRuntime = function(n) -- Wait for n threads created during runtime to die
assert(type(n) == "number", "Invalid argument #1 (number expected, got "..type(n)..")")
local amt = 0
return function(dead, all, init)
for i = 1, #init do
if init[i] == dead then
return false
end
for j = 1, #all do
if all[j] == init[i] then
table.remove(all, j)
end
end
end
amt = amt+1
return n == amt
end
end
}
return this -- Return the API
end
return {
manager = manager
}
-- threaddemo.lua by hugeblank
-- This program is for a demonstration of Raisin, a program by hugeblank. Found at: https://github.com/hugeblank/raisin
-- You are free to add, remove, and distribute from this program as you wish as long as these first three lines are kept in tact
local raisin = require("raisin").manager(os.pullEventRaw) -- Load Raisin
--[[ GENERIC RAISIN THREAD DEMONSTRATION
Our objective will be to make 2 threads with different priorities
The first thread will be a generic thread that counts the seconds.
The second thread will stop the first thread every five seconds, and wait for a mouse click to continue counting.
The master group will be used in this demonstration. By default when a group number is not provided to thread.add, it goes into the master group.
This allows for simple programs to be created in just a few lines without the need for creating a group. All this mention of groups may be going over your head.
I suggest after this demonstration you look at my groupdemo.lua file.
TL;DR the thread library is an easy access point for multithreading without getting into the raisin 'group' kerfuffle
Let's begin!
]]
local a, clicked = 1, false -- Create a basic counting value
-- We start by creating the counter, since we'll need it's thread data later on to toggle it.
local slave = raisin.thread(function() -- Create a new thread.
while true do
print(a)
sleep(1)
a = a+1
clicked = false
end
end, 0) -- Set the priority of this thread to 0. This way on starting the program this thread goes first before the one below.
-- If we let the one below go first, we'd have to click the first time the program starts.
-- Now let's create the thread stopper
raisin.thread(function() -- Create another new thread
while true do -- Begin thread
if a%5 == 0 and not clicked then
print("pausing thread...") -- Notify the user that the thread is being paused
slave.toggle() -- Toggle the slave thread above
print("click anywhere to continue counting") -- Notify the user that they need to click to re-enable the slave
os.pullEvent("mouse_click") -- pull that mouse click event
clicked = true
print('continuing...') -- Notify the user we're continuing execution
slave.toggle() -- Toggle the thread again to enable it
end
sleep() -- Yield for a second
end
end, 1) -- Set the priority of this thread to something lower than the first one.
-- We could set this thread to priority 0 and it would still execute after the thread above. Threads follow priority order, but if 2 threads share the same priority they go in the order that they were written.
--[[thread.add(function() -- Mysterious Function for Additional Activities
print("> exiting in")
for i = 3, 1, -1 do
print("> "..i)
sleep(1)
end
end, 2)]]
raisin.run(raisin.onDeath.waitForAll()) -- Signal to start execution
--[[ADDITIONAL ACTIVITIES
Replace the mouse click thread with something that requires you to type in a specific word, or do a specific combination of actions
Tamper around with the `raisin.manager.run()` above. The first parameter it takes in is the amount of threads that need to be dead in order for it to exit. See what effect that mystery function has on the execution of the program
]]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment