Page 1 of 1

LOVE-SharedMemory-> Sharing variables between threads

Posted: Thu Jul 23, 2020 4:26 am
by Hipreme
LOVE-SharedMemory
Sharing variables between threads


Why
In Love2D, child thread variables are separated from the main thread, the main way to connect those variables are between
Channels, where you need to push to your thread and pop inside it, while it does the work, sometimes you just don't want
to keep pushing and popping those variables, and it becomes even worser if you have multiple threads doing the same work

What it does
Create variables that are accessible in any thread, and every update on the shared variable will be available on every
thread

How it works
It creates a SoundData(because their data are inherently shared by the Love framework) object for your threads,
that will be pushed in every thread, those SoundData are populated with some ASCII values that are divided from
some number to become in the accepted range of the SoundData, then the library makes available some function to convert
the SoundData to the available type (those are defined on SHARED_MEMORY.AVAILABLE_TYPES), the first bit of the sounddata represents
the type, and the bit which represents that has finished data is the -1 number divided by the magic number.

How to use it
There are 2 ways to use it, there is the intrusive way and the non-instrusive way, the intrusive way will take care of boilerplate
code, but in the full way, it works like this:
  1. . Require sharedmemory
  2. . Define your variables
  3. . Create and register your thread
  4. . Inside your thread code make the requires
  5. . Connect your thread and your variables
  6. . Define the data update listener function
  7. . Call a function to read your variables
  8. . Get the updated variables
The first two steps
The first two steps shares the same code to be called, and it is very easy to do it:

Code: Select all

require "sharedmemory"
SHARED_MEMORY.newVar("myVariable", SHARED_MEMORY.AVAILABLE_TYPES.STRING, "This is a shared string variable")
This function will create your variable called "myVariable" inside your sharedmemory variables, for data awareness, those
sharedmemory variables types are Immutable, with that, you won't change your code silently and break something, if you
need to change its type, just create a new variable.

If you wish to get or set it's content, the functions available are:

Code: Select all

SHARED_MEMORY.setVarValue("myVariable", "This is an updated value")
print(SHARED_MEMORY.getVarValue("myVariable"))
There is a shorter way to do that, and the type is inferred by the variable content, really much easier and faster:
Shorter way

Code: Select all

_Shared.myVariable = "This is an updated value"
print(_Shared.myVariable)
Always remember that the shorter way still calls getVarValue, and getting those variables in tight loops can be costly depending on the
size of the shared variable, remember that you can cache your variable with

Code: Select all

local myVariable = _Shared.myVariable
Intrusive
The instrusive way will take care of the steps 3, 4, 5, 6 and 7

There are 2 ways to send your shared variables to your thread, the one which you specify which variables to use
or the other that sends every shared variable:

Specifying
For specifying, you will need to use | as an escape argument, after that argument, every variable name will be converted to your shared
variable.
The first argument is the thread code that can be defined with lua string literal

Code: Select all

[[ ]]
or you can send a file path.
Then, for every argument you define it in newThread, they will be sent first, for you not have to deal with assignment order

Code: Select all

shareThread = SHARED_MEMORY.newThread("sample.lua", 55, "|", "myVariable")
--You can define any number of arguments on the vertical bar left side and right side
-- shareThread = SHARED_MEMORY.newThread("sample.lua", 55, 90, "testing some feature", "|", "myVariable", "myVariable2", "mySharedNumber", "mySharedArray")
Specifying is the preferred way when searching for performance, as your thread will not be aware of every variable update, only "myVariable"

Unspecified

Code: Select all

shareThread = SHARED_MEMORY.newThread("sample.lua", 55, 90, "anything", "*")
SHARED_MEMORY.start(shareThread)
No code for update sharing
Other advantage of the intrusive way is that create a really nice syntactic sugar:
For every shared var it is using, calling the = symbol will make your new variable value available to every other thread,
by appending a setVarValue right after assigning it, so, this code:

Code: Select all

myVariable = "This is an updated value"
turns into this

Code: Select all

myVariable = "This is an updated value"
SHARED_MEMORY.setVarValue("myVariable", myVariable)
Non instrusive way
There is actually no reason for using it unless you want to have full control of its pipeline, the intrusive way just modifies your code and then creates
a thread with it
Third step

Code: Select all

thread = love.thread.newThread("mythreadcode.lua")
thread:start(SHARED_MEMORY.registerThread(55, "|", "myVariable")) --Same of the last time, but now you call it inside thread:start
Fourth step

Code: Select all

require("love.event")
require("love.sound")
require "memoryaccess"
Fifth step

Code: Select all

SHARED_MEMORY.connectThread(...)
sentVar1, sentVar2, sharedMem1, sharedMem2 = ...
SHARED_MEMORY.connectVariables(sharedMem1, "myvariableDefined1", sharedMem2, "myvariableDefined2")
Sixth step

Code: Select all

__MEMORY_ACCESS_ON_UPDATE = function(toUpdate)
    if toUpdate == "myvariableDefined1" then
        myvariableDefined1 = SHARED_MEMORY.getVarValue("myvariableDefined1")
    elseif toUpdate == "myvariableDefined2" then
        myvariableDefined2 = SHARED_MEMORY.getVarValue("myvariableDefined2")
    end
end
Seventh step

Code: Select all

myvariableDefined1 = SHARED_MEMORY.getVarValue("myvariableDefined1")
myvariableDefined2 = SHARED_MEMORY.getVarValue("myvariableDefined2")
The eigthth step
This step is unfortunately required for both ways, as I would need to search for a thread loop, it is much work for something
really simple to do, so, before any variable access, just call the simple function

Code: Select all

SHARED_MEMORY._checkUpdates()
Guaranteeing a great performance
This is a much easier way to define, although it is better for debugging purposes or if you don't need much extra performance
The performance is measured by how many times you modifiy a shared variable value, how many times you getVarValue your variable
and how many threads are listening to the same variable(as every thread will need to call getVarValue when any thread set its value)
Always remember that getVarValue and setVarValue are data unserialization/serialization

Some gotchas
This code have some limitations
  • It only accepts numbers, strings, arrays and maps
  • It doesn't supports any kind of nested table(array nor map)
  • It has a variable size limitation, currently is 2800 character values(To keep sample sizes small)
  • You can change its size at your own need(Just search for SHARED_MEMORY.newVar), but changing its size will change for every variable, and i recommend not changing it at runtime

Some sample code

main.lua:

Code: Select all

require "sharedmemory"


function love.load()
    SHARED_MEMORY.newVar("maptest", SHARED_MEMORY.AVAILABLE_TYPES.MAP, 
    {
        ["hello"] = "oi",
        ["there"] = "hipreme"
    })
    globalCount = 1
    --Short way to mean the same thing above
    _Shared.numtest = globalCount
    _Shared.strtest = "String defined inside main"
    _Shared.arraytest = {5, 3, 10, 90}
    local shareThread = SHARED_MEMORY.newThread("sample.lua", 55, "*")
    SHARED_MEMORY.start(shareThread)
end

function love.keypressed(k)
    if(k == "k") then
        SHARED_MEMORY.setVarValue("maptest", {["hello"] = "god!"})
    elseif k == "j" then
        _Shared.maptest =  {["there"] = "nohipreme"}
    elseif k == "h" then
        print(SHARED_MEMORY.getVarValue("strtest"))
    elseif k == "g" then
        globalCount = globalCount + 1
        _Shared.numtest = globalCount
    end
end

function love.update(dt)
    
end
sample.lua:

Code: Select all

require("love.timer")
maptest = {beware = "insider"}
numtest = 100
strtest = "my string was modified inside sample thread"
arraytest = {"hello", 525, "dear", "friend"}
while true do
    love.timer.sleep(1)
    print(numtest)
    SHARED_MEMORY._checkUpdates()
end
Download and contribute at : Hipreme LOVE-SharedMemory

Re: LOVE-SharedMemory-> Sharing variables between threads

Posted: Thu Jul 23, 2020 5:44 am
by grump
Had a quick look into it. I commend the effort.

The limitations seem arbitrary. A better, more flexible and stable way to implement this would be a ByteData object instead of abusing SoundData, and a general purpose serialization lib on top of that.

This also seems to rely on the "shared" variables being global inside the threads, which enforces bad coding style and creation of spaghetti code. The implicit code injection also has a bad smell imo.

The threads need to run a busy loop because the "synchronization" is implemented via polling. This drives up CPU load. In your sample code the remedy is a 1s sleep, which introduces huge latency. I'm not convinced this will work well in many real scenarios, but I haven't looked deeply enough into it to come up with a better solution.

Oh, and newThread should not print the thread's code.

Re: LOVE-SharedMemory-> Sharing variables between threads

Posted: Thu Jul 23, 2020 1:36 pm
by Hipreme
SoundData was used instead for not requiring FFI.
I don't see the point of not making them a global access point, specially when you have the choice to choose which variables to use in your thread.
Code injection is absolutely optional, the documentation shows how to not use it, it reduces how much code you need to write and it just inserts boilerplate code, boilerplate code is just a time thrower.

Via code injection, it could actually tackle the problem of making every $mySharedVariable actually changes the $ for a namespace, it does not requires them to be global, it just makes it easier to access that way

The busy loop is actually popping a dedicated channel for only that purpose, if there's no data, there's no update, so it's not actually busy, the notification system was created for your variable don't need to be desserialized everytime it is accessed.
This polling was created for not having to deserialize your variable every step, so it actually improves the performance

Having a loop for actually synchronize your variable is entirely optional, you could just call it before using your variable.

This is not a remedy, this is a sample for not breaking anyone's computer while printing, just try at home calling:

Code: Select all

while true do
print("something")
end
New samples on what to do with shared memory are welcome by the way.

If you looked at main, you can press some keys for you to test if it is actually updating your variable
About the newThread, I'm committing right now :)