Browse Source

Example Lua module for coroutining (#2851)

Terry Ellison 4 years ago
parent
commit
6d9c5a49a4
3 changed files with 118 additions and 0 deletions
  1. 88 0
      docs/lua-modules/cohelper.md
  2. 3 0
      lua_modules/cohelper/README.md
  3. 27 0
      lua_modules/cohelper/cohelper.lua

+ 88 - 0
docs/lua-modules/cohelper.md

@@ -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 - 0
lua_modules/cohelper/README.md

@@ -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 - 0
lua_modules/cohelper/cohelper.lua

@@ -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 }
+
+