Dead simple threads example

Questions about the LÖVE API, installing LÖVE and other support related questions go here.
Forum rules
Before you make a thread asking for help, read this.
User avatar
Rishavs
Party member
Posts: 103
Joined: Sat Oct 17, 2009 5:29 am
Contact:

Dead simple threads example

Post by Rishavs » Tue Sep 06, 2016 1:34 pm

--------------------------------------------------------------

EDIT:
If you are looking for the threads example, i am putting that in this section so you dont have to go through other questions and discussions.

Here is my default code.
if you run it, you can see how the window is unresponsive for a few seconds while the logic loop runs. (if you have a non potato PC, you may increase the for loop range so the effect is more prominent)

Code: Select all

debug = true
local progress = 0

local scrWidth = love.graphics.getWidth()
local scrHeight = love.graphics.getHeight()

function love.load(arg)
    stime = love.timer.getTime()
    backend_logic()
    ttime = (love.timer.getTime() - stime)
end

function love.draw(dt)
    love.graphics.print("Loading: " .. progress .. " %", scrWidth/2 - 50, scrHeight/2)

    love.graphics.print("FPS: "..tostring(love.timer.getFPS( )), 10, 10)    
    love.graphics.print("Time taken : "..tostring(round(ttime, 1)) .. " seconds", 10, 30)    
end

function love.update(dt)

end

function backend_logic()
    local smin = 1
    local smax = 5000
    for i = smin, smax, 1 do
        print("ALL WORK AND NO PLAY MAKES JACK A DULL BOY")
        progress = round(i * 100 / smax)
    end
end

function round(num, dp)
    local mult = 10^(dp or 0)
    return (math.floor(num * mult + 0.5)) / mult
end
Now the same code using threads. the main file is now broekn into 2 files: main.lua and backend.lua.
main calls backend as a thread and the backend keeps the main appraised of the current for loop progress via a channel.

main.lua

Code: Select all

debug = true
-- inspect = require "inspect"

local progress = 0
local msg_queue = {}
local scrWidth = love.graphics.getWidth()
local scrHeight = love.graphics.getHeight()

function love.load(arg)
    stime = love.timer.getTime()

    thread = love.thread.newThread( "backend.lua")
    channel = love.thread.getChannel("test" )
    thread:start()

    ttime = (love.timer.getTime() - stime)
end

function love.draw(dt)
    love.graphics.print("Loading: " .. progress .. " %", scrWidth/2 - 50, scrHeight/2)

    love.graphics.print("FPS: "..tostring(love.timer.getFPS( )), 10, 10)    
    love.graphics.print("Time taken : "..tostring(round(ttime, 1)) .. " seconds", 10, 30)    
end

function love.update(dt)

    progress = channel:pop() or progress

end

function love.threaderror(thread, errorstr)
    print("Thread error!\n"..errorstr)
    -- thread:getError() will return the same error string now.
end

function get_progress()
    return msg_queue[#msg_queue] or "starting..."
end

function round(num, dp)
    local mult = 10^(dp or 0)
    return (math.floor(num * mult + 0.5)) / mult
end
and backend.lua

Code: Select all

local smin = 1
local smax = 1000

channel = love.thread.getChannel("test")

for i = smin, smax, 1 do
    print(i .. " > ALL WORK AND NO PLAY MAKES JACK A DULL BOY")
    channel:push((i * 100 / smax))
end


function round(num, dp)
    local mult = 10^(dp or 0)
    return (math.floor(num * mult + 0.5)) / mult
end
Finally, we run the same code using coroutines

Code: Select all

debug = true
local progress = 0

local scrWidth = love.graphics.getWidth()
local scrHeight = love.graphics.getHeight()

function love.load(arg)
    stime = love.timer.getTime()
    co = coroutine.create(backend_logic)

    ttime = (love.timer.getTime() - stime)
end

function love.draw(dt)

    love.graphics.print("Loading: " .. progress .. " %", scrWidth/2 - 50, scrHeight/2)

    love.graphics.print("FPS: "..tostring(love.timer.getFPS( )), 10, 10)    
    love.graphics.print("Time taken : "..tostring(round(ttime, 1)) .. " seconds", 10, 30)    
end

function love.update(dt)
    if coroutine.status(co) ~= "dead" then
         _, progress = coroutine.resume(co)
    end
end

function backend_logic()
    local smin = 1
    local smax = 5000
    for i = smin, smax, 1 do
        print(i .. " > ALL WORK AND NO PLAY MAKES JACK A DULL BOY")
        local prg = round(i * 100 / smax)
        coroutine.yield(prg)
    end
    ttime = (love.timer.getTime() - stime)
    return 100
end

function round(num, dp)
    local mult = 10^(dp or 0)
    return (math.floor(num * mult + 0.5)) / mult
end
some basic takeaway:
threads is just as fast as normal blocking operation. However, it feels like threads are meant for long running operations like physics. creating a new thread for every short lived operation may not be ideal. i still need to research this more.

coroutines is definitely slower. the lag becomes more apparent when you increase the for loop range (for small ranges the lag is negligible).

Anyway, if i find any more ways of doing non blocking code (I might try async and some other parallel/concurrent programming libs next) i'll update my thread.

--------------------------------------------------------------


hi

I am trying to use threads but I fear I dont understand the implementation.
So, i created a simple example that you can help me with.

I have a simple function which takes a bit of time to finish. while it runs, the rendering is stopped. I instead want the rendering to go on and show me a progress indicator for it.



This is the code that i am using for it:

Code: Select all

debug = true
local progress = 0

local scrWidth = love.graphics.getWidth()
local scrHeight = love.graphics.getHeight()

function love.load(arg)
    stime = love.timer.getTime()
    -- thread = love.thread.newThread( backend_logic() )
    -- backend_logic()
    ttime = (love.timer.getTime() - stime)
end

function love.draw(dt)
    love.graphics.print("Loading: " .. progress .. " %", scrWidth/2 - 50, scrHeight/2)

    love.graphics.print("FPS: "..tostring(love.timer.getFPS( )), 10, 10)    
    love.graphics.print("Time taken : "..tostring(round(ttime, 1)) .. " seconds", 10, 30)    
end

function love.update(dt)

end

function backend_logic()
    local smin = 1
    local smax = 10000
    for i = smin, smax, 1 do
        print("ALL WORK AND NO PLAY MAKES JACK A DULL BOY")
        progress = round(i * 100 / smax)
    end
end

function round(num, dp)
    local mult = 10^(dp or 0)
    return (math.floor(num * mult + 0.5)) / mult
end
How can i implement threads so that the backend_logic is run in its own thread without blocking the main thread?
the "progress" variable is a shared one. it is updated every tick in the child thread, but its value should be shown on the main thread.

EDIT:
this is my try.

Code: Select all

function love.load(arg)
    stime = love.timer.getTime()

    channel = love.thread.newChannel( )
    progress = channel:pop()
    thread = love.thread.newThread( backend_logic() )
    thread:start()
    -- backend_logic()
    ttime = (love.timer.getTime() - stime)
end

Code: Select all

function backend_logic()
    local prg = 0
    local smin = 1
    local smax = 1000
    for i = smin, smax, 1 do
        print("ALL WORK AND NO PLAY MAKES JACK A DULL BOY")
        prg = round(i * 100 / smax)
        channel:push( prg )
    end
end
It doesnt works. :/
I get "Error: main.lua:9: bad argument #1 to 'newThread' (Data expected, got no value)" error. Halp.
Last edited by Rishavs on Mon Sep 12, 2016 7:09 am, edited 1 time in total.

User avatar
s-ol
Party member
Posts: 1077
Joined: Mon Sep 15, 2014 7:41 pm
Location: Cologne, Germany
Contact:

Re: Dead simple threads example

Post by s-ol » Tue Sep 06, 2016 1:47 pm

https://love2d.org/wiki/love.thread.newThread

read this again. you need to give it a filename, and even if it took a function, it would need to be newThread( backend_logic ) not newThread( backend_logic() ) for the function to even 'reach' the newThread call.

also you will need to use a named channel to communicate to your thread unless you pass the unnamed channel to it via arguments in :start() perhaps.

s-ol.nu /blog  -  p.s-ol.be /st8.lua  -  g.s-ol.be /gtglg /curcur

Code: Select all

print( type(love) )
if false then
  baby:hurt(me)
end

User avatar
Rishavs
Party member
Posts: 103
Joined: Sat Oct 17, 2009 5:29 am
Contact:

Re: Dead simple threads example

Post by Rishavs » Tue Sep 06, 2016 2:39 pm

Hmmm

I thought i could do it all in the main file only using
thread = love.thread.newThread( codestring )

Can you check my try in the first post please?
If i reuse the same code but put in a different file, will it work?

Or do i remove the function call and have the raw code in the new file?

User avatar
0x72
Citizen
Posts: 51
Joined: Thu Jun 18, 2015 9:02 am

Re: Dead simple threads example

Post by 0x72 » Tue Sep 06, 2016 4:34 pm

Consider this code, I hope it'll make the thing clear:

Code: Select all

-- using codesting, but as you can see just a filename would be much cleaner
local backend_logic_str = [[

love.timer = require('love.timer') -- you won't have it out of box in new thread

channel = love.thread.getChannel( 'backend_logic_channel' ) -- named channel - same as in the main thread

function backend_logic()
  local prg = 0
  local smin = 1
  local smax = 1000
  for i = smin, smax, 1 do
      print("ALL WORK AND NO PLAY MAKES JACK A DULL BOY")
      channel:push( i )
      love.timer.sleep(0.5) -- this thread locks but it won't lock the main thread
  end
end

backend_logic() -- the function won't run itself without some help, could also be an anonymus, immediately 
               -- invoked function ( (function() --[[...]] end)() ), o no function at all (just naked 
               -- code) if you will
]]


function love.load(arg)
  channel = love.thread.getChannel( 'backend_logic_channel' )
  thread = love.thread.newThread( backend_logic_str )
  thread:start()
end

function love.update(dt)
  io.write('.') -- just some dots so we see when love.update() runs
  io.flush()
  while true do -- in a loop so we pop all of them in one update(), - just in case the other thread runs faster then the main loop
    local msg = channel:pop()
    if msg then -- the other thread have just pushed message, nice!
      print("MSG: ", msg)
    else -- channel queue's empty, we can move on
      break
    end
  end
end
-- edit --
minor thing: you used round, while most likely you wanted math.floor

User avatar
Rishavs
Party member
Posts: 103
Joined: Sat Oct 17, 2009 5:29 am
Contact:

Re: Dead simple threads example

Post by Rishavs » Tue Sep 06, 2016 8:43 pm

thanks to you all, i got my code to work.

Still have 1 question though:
If I try to use a function call in my thread file, the function name isnt recognized.

Code: Select all

local smin = 1
local smax = 1000
channel = love.thread.getChannel("test")
for i = smin, smax, 1 do
    print(i .. " > ALL WORK AND NO PLAY MAKES JACK A DULL BOY")
    channel:push(round(i * 100 / smax))
end

function round(num, dp)
    local mult = 10^(dp or 0)
    return (math.floor(num * mult + 0.5)) / mult
end
gives me:
"Thread error!
backend.lua:9: attempt to call global 'round' (a nil value)"


ps. adding my main and thread files for anyone coming in after me.
main.lua

Code: Select all

debug = true
local progress = 0
local msg_queue = {}
local scrWidth = love.graphics.getWidth()
local scrHeight = love.graphics.getHeight()

function love.load(arg)
    stime = love.timer.getTime()

    thread = love.thread.newThread( "backend.lua")
    channel = love.thread.getChannel("test" )
    thread:start()

    -- backend_logic()
    ttime = (love.timer.getTime() - stime)
end

function love.draw(dt)
    love.graphics.print("Loading: " .. get_progress() .. " %", scrWidth/2 - 50, scrHeight/2)

    love.graphics.print("FPS: "..tostring(love.timer.getFPS( )), 10, 10)    
    love.graphics.print("Time taken : "..tostring(round(ttime, 1)) .. " seconds", 10, 30)    
end

function love.update(dt)

    v = channel:pop()
    if v then
        table.insert(msg_queue, v)
    end
    -- print(msg_queue[10])
end

function love.threaderror(thread, errorstr)
    print("Thread error!\n"..errorstr)
    -- thread:getError() will return the same error string now.
end

function get_progress()
    return msg_queue[#msg_queue] or "starting..."
end

function round(num, dp)
    local mult = 10^(dp or 0)
    return (math.floor(num * mult + 0.5)) / mult
end
and backend.lua

Code: Select all

local smin = 1
local smax = 1000

channel = love.thread.getChannel("test")

for i = smin, smax, 1 do
    print(i .. " > ALL WORK AND NO PLAY MAKES JACK A DULL BOY")
    channel:push(i * 100 / smax)
end

function round(num, dp)
    local mult = 10^(dp or 0)
    return (math.floor(num * mult + 0.5)) / mult
end

User avatar
Nixola
Inner party member
Posts: 1940
Joined: Tue Dec 06, 2011 7:11 pm
Location: Italy

Re: Dead simple threads example

Post by Nixola » Tue Sep 06, 2016 9:00 pm

The code in the first

Code: Select all

 tag and the last one is different. In the first one, you're calling the round function which will only be defined after that for loop runs; so you'd need to define the function before the loop.
lf = love.filesystem
ls = love.sound
la = love.audio
lp = love.physics
lt = love.thread
li = love.image
lg = love.graphics

User avatar
Rishavs
Party member
Posts: 103
Joined: Sat Oct 17, 2009 5:29 am
Contact:

Re: Dead simple threads example

Post by Rishavs » Wed Sep 07, 2016 4:02 am

Ok. so when i put the round function before the for loop in my thread file, it works.
But this is the first time i have seen anything like it. In my other scripts I can use function calls above the function definitions.
I think i am missing something very basic to lua. :roll:

KayleMaster
Party member
Posts: 212
Joined: Mon Aug 29, 2016 8:51 am

Re: Dead simple threads example

Post by KayleMaster » Wed Sep 07, 2016 6:35 am

Lua threads are separate environments and so they cannot access the variables and functions of the main thread, and communication between threads is limited.
They are to be used for for loops, io loading/saving, multiplayer, physics etc.

User avatar
Rishavs
Party member
Posts: 103
Joined: Sat Oct 17, 2009 5:29 am
Contact:

Re: Dead simple threads example

Post by Rishavs » Wed Sep 07, 2016 7:57 am

But I also put the function in the thread file as well.
the function is only recognized if i place it before the function call in the script.

If i put the function below the place where it is called, then i get an exception.
This means I will have to always put functions on top, and raw code below in a script if i were to use it in a thread.

sucks. :(

User avatar
0x72
Citizen
Posts: 51
Joined: Thu Jun 18, 2015 9:02 am

Re: Dead simple threads example

Post by 0x72 » Wed Sep 07, 2016 8:00 am

My bet is you mean something like that:

Code: Select all

function love.load() -- update/draw/whatever
  other_later_defined_function() -- valid call
end

--  other_later_defined_function() -- this would be an error

function other_later_defined_function() -- definition
  print('foobar')
end
It might not be obvious at glance but here the valid call actually will happen after the definition.

Please run and consider:

Code: Select all

foo = 'foo'

function love.load()
  print(foo)
end

foo = 'bar'

Post Reply

Who is online

Users browsing this forum: No registered users and 6 guests