Created
December 20, 2020 14:16
-
-
Save hugeblank/845b6f9e06d586877d0493bb10d0a6a0 to your computer and use it in GitHub Desktop.
raisin 4.0-pre.0 with the thread demo program
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ 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 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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