Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

A Threads class with coroutine

edited June 2016 in Code Sharing Posts: 113

A Threads class with coroutine

Introduction

Problem description

When I tried to run some functions like terrain generating. Because it need a long time to finish, so I have to face a black screen without any information appear. This is because of the mechanism of Codea:

when setup() is running, the draw() won't run, when setup() is finished, it will run the draw(). But these functions must run once, so they need to be put into setup().

When I use these functions in setup() and I want to see some information drawing on the screen, how can I do?

I found coroutine can deal with this problem, then I began to learn it.

Solution

I am learning and writing a Threads class. Using it we can solve the problem.

Threads class

Class code

The class code

--# Threads
Threads = class()

function Threads:init()
    self.threads = {}    
    self.taskList = {}
    self.time = os.clock()   
    self.timeTick = 0.01
    self.taskID = 1
    self.taskStatus = {}
    self.taskVT = {}
    self.img = image(100,100)
end

-- Add task function into the task list
function Threads:addTaskToList(task)
    local t = function() task() end
    table.insert(self.taskList, t)
end

-- Create coroutine for all task, run once
function Threads:job()    
    local n = #self.taskList
    for id = 1, n do
        -- local f = function () self.taskList[id]() end
        local f = function () self:taskUnit(id) end
        local co = coroutine.create(f)
        table.insert(self.threads, co)   
        -- Record all tasks' status,  now should be suspended
        self.taskStatus[id] = coroutine.status(co)
    end
end

-- Task unit
function Threads:taskUnit(id)
    self.taskID = id
    self.taskList[id]()

    -- self:switchPoint(id)      

    -- self.taskStatus[id] = "Finished" 
end

-- Switch point, put this function into the task function
function Threads:switchPoint(id)
    -- Switch task,when its timetick is used and task have not finished
    if (os.clock() - self.time) >= self.timeTick then   
        -- Debug info, do not put the print into the task function
        print("hello: No."..id.." is "..self.taskStatus[id])  
        -- self:visual(id)
        -- reset task time
        self.time = os.clock()  
        -- Pause the task 
        coroutine.yield()    
    end
end

-- Put this function into draw()
function Threads:dispatch()
    local n = #self.threads
    if n == 0 then return end   
    for i = 1, n do
        -- Record the current task
        self.taskID = i    
        local status = coroutine.resume(self.threads[i])
        -- Record all the tasks' status
        self.taskStatus[i] = coroutine.status(self.threads[i])
        -- If task finished, then remove it from self.threads, and return
        if not status then
            self.taskStatus[i] = "Finished" 
            table.remove(self.threads, i)
            -- table.remove(self.taskList,i)
            return
        end
    end
end 

--[[
function Threads:visual(id)
    local n = #self.taskList
    local vt = {}
    background(18, 16, 16, 255)
    setContext(self.img)
    pushStyle()
    strokeWidth(1)
    fill(255, 211, 0, 255)
    -- if self.taskID == 1 then fill(241, 7, 7, 255) else fill(255, 211, 0, 255) end
    local w,h = self.img.width/n, self.img.height/n
    local x,y = 0,0
    for i = 1, n do
        vt[i] = function () rect(100+x+(i-1)*w,100+y+(i-1)*h,w,h) end
    end    
    popStyle()
    setContext()
    -- sprite(self.img,300,300)
    -- vt[self.taskID]()
    print("id: "..id)
    vt[id]()
end
--]]

The test code:

--# Main
function setup()
    print("Thread testing ...")

    myT = Threads()
    myT.timeTick = 1/60
    myT:addTaskToList(tt)
    myT:addTaskToList(oo)
    myT:addTaskToList(mf)
    myT:addTaskToList(pk)

    --[[ myT.taskList[2]()
    --]]

    myT:job()

    print(unpack(myT.taskList))

end

function draw()
    background(0)

    myT:dispatch()

    fill(244, 27, 27, 255)
    print("2: "..myT.taskStatus[1])
    print("length: "..#myT.taskList)

    sysInfo()
end


-- The task functions

function tt ()
    while true do
        -- print("tt: "..os.clock())
        myT:switchPoint(myT.taskID)
    end
end

function oo ()
    while true do
        -- print("oo: "..os.clock())
        myT:switchPoint(myT.taskID)
    end
end

function mf ()
    local k = 0
    for i=1,10000000 do
        k = k + i
        -- print("mf: "..k)
        -- If the time >= timeTick then pause
        myT:switchPoint(myT.taskID)
    end
end

function pk ()
    local k = 0
    for i=1,10000000 do
        k = k + i
        -- print("pk: "..k)
        -- If the time >= timeTick then pause
        myT:switchPoint(myT.taskID)
    end
end

Usage

Assume we have a function createTerrain() to generate the terrain in setup(), and it will take a long time, we can do like below:

function setup()

    myT = Threads()
    myT.timeTick = 1/60
    myT:addTaskToList(createTerrain)

    myT:job()        
end

function draw()
    background(0)

    myT:dispatch()
    drawLoadingScreen()
end

-- This is the loading screen
function drawLoadingScreen()
    ...
    -- Show some information
    text("Creating the terrain, please wait...")
    ...
end

-- The function will run for a long time
function createTerrain()
    ...
    for i=1,10000 do
        for j= 1, 100000 do
            ...
            -- Put the switchPoint() here
            myT:switchPoint(myT.taskID)
        end
    end
    ...

    for i=1,200000 do
        for j= 1, 300000 do
            ...
            -- Put the switchPoint() here
            myT:switchPoint(myT.taskID)
        end
    end
    ...
end

If the function is a method of a class, like Map:createTerrain(), we can load it like below:

myT:addTaskToList(function () Map:createTerrain() end)

OK, it is all.

BTW. This is for the newbie, if you are experienced, please ignore it. :)

Comments

  • dave1707dave1707 Mod
    Posts: 9,112

    I didn't look at any of your code, I just read the problem discription. One way I get around the blank screen is to set some variable to true in startup. In draw(), if that variable is still true, I display what ever messages to say I'm downloading something. Once the download is done, I set that variable to false and run the normal code. But it's nice you're learning about coroutines and threads.

  • edited June 2016 Posts: 113

    @dave1707 Thanks for you suggestion, maybe my problem description is not so exact. I will modify it.

    This is the scene: when we use socket to download several files from network, without coroutine, it have to run one after another, so we have to wait, but with coroutine, we can save time.

    It is a sample in book Programming in Lua, the chapter 9. In its example, using socket to download 4 files, without coroutine need 15 seconds, with coroutine need 6 seconds. I do not know if it is different in Codea. I will try it.

  • dave1707dave1707 Mod
    Posts: 9,112

    @binaryblues I've never tried to download a lot of large files, so I never tried loading them all at the same time. I've done several small files one after the other that didn't take a very long time. Maybe I'll find a large file somewhere and try downloading it multiple times at the same time.

  • dave1707dave1707 Mod
    Posts: 9,112

    @binaryblues Here's an example of something I use. Running this the way it is will run forever because I don't have a file in the url to download. If you put a large file to download in the url, it will eventually end once it downloads it 4 times.

    function setup()    
        url="http://www."
        ang=0
        loading=4
        rectMode(CENTER)
        http.request(url,response1)
        http.request(url,response2)
        http.request(url,response3)
        http.request(url,response4)
    end
    
    function response1(data,status,headers)
        loading=loading-1
    end
    
    function response2(data,status,headers)
        loading=loading-1
    end
    
    function response3(data,status,headers)
        loading=loading-1
    end
    
    function response4(data,status,headers)
        loading=loading-1
    end
    
    function draw()
        background(40, 40, 50)
        fill(255)
        if loading>0 then
            text("Files loading  "..loading,WIDTH/2,HEIGHT/2-50)
            ang=ang+10
            pushMatrix()
            translate(WIDTH/2,HEIGHT/2)
            rotate(ang)
            rect(0,0,50,50)
            popMatrix()
        else
            text("Done loading, running normal code now  ",WIDTH/2,HEIGHT/2)
        end
    end
    
  • Posts: 2,020

    @dave1707 http.request is asynchronous anyway, so you wouldn't need to put it on a coroutine thread.

  • Posts: 113

    @dave1707 @yojimbo2000 Yes, the http.request() is asynchronous, I dont know clear if the socket is asynchronous, I will delete the scene, thanks!

  • @binaryblues it seems like you got satisfactory solutions from the people who have replied, so I'm glad.

    If you are still interested in any feedback on your code, I did run it and play with it a little, and there are a couple things I could say.

    • First off I think it's a great idea, and useful for more than newcomers. coroutine is not in the included documentation, and it's slippery to understand even if you do find reference for it. A class that simplifies it would be great.
    • I think you could cut down on the obligations you require of users: they have to initialize the class, then load all their tasks, then and only then call job(), make sure to include the appropriate breakpoints in their functions, and make sure to call the threads object in every draw() cycle.
    • If I understand correctly, you can only add tasks to a threads object before you call job()? It would be great if you could dynamically add tasks at any time.
    • I may be wrong, but it seems to me in theory you could reduce the obligations to three: initialize the object, put the breakpoints where needed in the functions, and call the threads object every draw() cycle. You don't need to explicitly send the functions to the threading object, because coroutine remembers where to resume from, and if the breakpoints are wrappers for coroutine calls, they will remember where to resume from also. Or am I misunderstanding?
    • If you no longer need to explicitly send functions to the threader, then in theory you no longer need to call job, and perhaps that alone would enable dynamically adding tasks?

    Again, I think this is a noble effort, and I know it's been a long time, but if you wanted to revisit it I think it would be well worth doing.

  • edited April 1 Posts: 123

    @UberGoober

    Using your feedback I was able to simplify this into a class that is responsible for a single thread. Then in the parent where one would call a Thread they can deal with managing multiple threads. In my case I also use an inversion of control technique to manage the draw loop:


    Thread = class() function Thread:init(name, task, tick) self.time = os.clock() self.timeTick = tick or 0.016 self.thread = coroutine.create(function() task() end) self.status = coroutine.status(self.thread) self.id = name end -- Switch point, put this function into the task function function Thread:switchPoint() if (os.clock() - self.time) >= self.timeTick then self.time = os.clock() coroutine.yield() end end -- Put this function into draw() function Thread:dispatch() local status = coroutine.resume(self.thread) if not status then self.status = "Finished" end end

    And then using IOC and a mapped queue for drawing, you can dynamically add a thread anytime.


    function draw() --loading for i = 1, #loadingThreadQueue.queue do if loadingThreadQueue.queue[i] == nil then print('thread nil') goto done else loadingThreadQueue.queue[i]:dispatch() if loadingThreadQueue.queue[i].status == 'Finished' then loadingThreadQueue:unqueue(loadingThreadQueue.map[i]) end end ::done:: end --drawing for i = 1, #drawQueue.queue do if drawQueue.queue[i] == nil then print('draw nil') goto done else drawQueue.queue[i]() end ::done:: end end

    Queue = class() function Queue:init() self.queue = {} self.map = {} end function Queue:enqueue(name, func) self.queue[#self.queue + 1] = func self.map[name] = #self.queue self.map[#self.queue] = name end function Queue:unqueue(name) local index = self.map[name] for i = index, #self.queue do self.map[i] = self.map[i + 1] if self.map[i] then self.map[self.map[i]] = i end self.queue[i] = self.queue[i + 1] end self.map[name] = nil end
  • edited April 1 Posts: 978

    Wow this looks really interesting. I wrote my comments back in 2017 when I was actively using coroutines in a game, so I had some experience. I barely remember what my own words above mean now. I’m very glad if I was helpful and this looks like a great thing to use.

    Is there any chance you could make a demo project showing it in action? I think I get it in theory but it would be nice to see it in practice.

  • Posts: 123

    Yeah check out my thread here - I’ll add my project in a zip.

  • Posts: 57

    @skar could you provide a simple example of using thread to play a sound?

  • Posts: 123

    @krdavis based on what I understand about Codea, it’s already doing sound() and music() in threads behind the curtain. I could be wrong.

    But either way, there is no data processing function that you can hook into to thread the sound/music. Wherever you call the functions will be when the sound is played, draw loops continue on their own.

    The way this Thread works is by having a callback switchPoint() that you use inside a large data processing function, the callback checks to see how much time has passed, if more than the allotted time has passed the threads pauses and lets Codea loop the draw. From what I can tell there’s no way and no reason to use this for sounds.

  • Posts: 57

    @skar thanks for the info, I will take it into account while working on my project.

Sign In or Register to comment.