Skip to content

Commit

Permalink
Example Lua module for coroutining (#2851)
Browse files Browse the repository at this point in the history
  • Loading branch information
TerryE authored Jul 26, 2019
1 parent 21b3f38 commit 6d9c5a4
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 0 deletions.
88 changes: 88 additions & 0 deletions docs/lua-modules/cohelper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# cohelper Module
| Since | Origin / Contributor | Maintainer | Source |
| :----- | :-------------------- | :---------- | :------ |
| 2019-07-24 | [TerryE](https://github.com/TerryE) | [TerryE](https://github.com/TerryE) | [cohelper.lua](../../lua_modules/cohelper/cohelper.lua) |

This module provides a simple wrapper around long running functions to allow
these to execute within the SDK and its advised limit of 15 mSec per individual
task execution. It does this by exploiting the standard Lua coroutine
functionality as described in the [Lua RM §2.11](https://www.lua.org/manual/5.1/manual.html#2.11) and [PiL Chapter 9](https://www.lua.org/pil/9.html).

The NodeMCU Lua VM fully supports the standard coroutine functionality. Any
interactive or callback tasks are executed in the default thread, and the coroutine
itself runs in a second separate Lua stack. The coroutine can call any library
functions, but any subsequent callbacks will, of course, execute in the default
stack.

Interaction between the coroutine and the parent is through yield and resume
statements, and since the order of SDK tasks is indeterminate, the application
must take care to handle any ordering issues. This particular example uses
the `node.task.post()` API with the `taskYield()`function to resume itself,
so the running code can call `taskYield()` at regular points in the processing
to spilt the work into separate SDK tasks.

A similar approach could be based on timer or on a socket or pipe CB. If you
want to develop such a variant then start by reviewing the source and understanding
what it does.

### Require
```lua
local cohelper = require("cohelper")
-- or linked directly with the `exec()` method
require("cohelper").exec(func, <params>)
```

### Release

Not required. All resources are released on completion of the `exec()` method.

## `cohelper.exec()`
Execute a function which is wrapped by a coroutine handler.

#### Syntax
`require("cohelper").exec(func, <params>)`

#### Parameters
- `func`: Lua function to be executed as a coroutine.
- `<params>`: list of 0 or more parameters used to initialise func. the number and types must be matched to the funct declaration

#### Returns
Return result of first yield.

#### Notes
1. The coroutine function `func()` has 1+_n_ arguments The first is the supplied task yield function. Calling this yield function within `func()` will temporarily break execution and cause an SDK reschedule which migh allow other executinng tasks to be executed before is resumed. The remaining arguments are passed to the `func()` on first call.
2. The current implementation passes a single integer parameter across `resume()` / `yield()` interface. This acts to count the number of yields that occur. Depending on your appplication requirements, you might wish to amend this.

### Full Example

Here is a function which recursively walks the globals environment, the ROM table
and the Registry. Without coroutining, this walk terminate with a PANIC following
a watchdog timout. I don't want to sprinkle the code with `tmr.wdclr(`) that could
in turn cause the network stack to fail. Here is how to do it using coroutining:

```Lua
require "cohelper".exec(
function(taskYield, list)
local s, n, nCBs = {}, 0, 0

local function list_entry (name, v) -- upval: taskYield, nCBs
print(name, v)
n = n + 1
if n % 20 == 0 then nCBs = taskYield(nCBs) end
if type(v):sub(-5) ~= 'table' or s[v] or name == 'Reg.stdout' then return end
s[v]=true
for k,tv in pairs(v) do
list_entry(name..'.'..k, tv)
end
s[v] = nil
end

for k,v in pairs(list) do
list_entry(k, v)
end
print ('Total lines, print batches = ', n, nCBs)
end,
{_G = _G, Reg = debug.getregistry(), ROM = ROM}
)
```

3 changes: 3 additions & 0 deletions lua_modules/cohelper/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Coroutine Helper Module

Documentation for this Lua module is available in the [Lua Modules->cohelper](../../docs/lua-modules/cohelper.md) MD file and in the [Official NodeMCU Documentation](https://nodemcu.readthedocs.io/) in `Lua Modules` section.
27 changes: 27 additions & 0 deletions lua_modules/cohelper/cohelper.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
--[[ A coroutine Helper T. Ellison, June 2019
This version of couroutine helper demonstrates the use of corouting within
NodeMCU execution to split structured Lua code into smaller tasks
]]
--luacheck: read globals node

local modname = ...

local function taskYieldFactory(co)
local post = node.task.post
return function(nCBs) -- upval: co,post
post(function () -- upval: co, nCBs
coroutine.resume(co, nCBs or 0)
end)
return coroutine.yield() + 1
end
end

return { exec = function(func, ...) -- upval: modname
package.loaded[modname] = nil
local co = coroutine.create(func)
return coroutine.resume(co, taskYieldFactory(co), ... )
end }


0 comments on commit 6d9c5a4

Please sign in to comment.