From e97adefde985e30fe31ffa036c74ffb0ce10ca26 Mon Sep 17 00:00:00 2001 From: Benoit Germain Date: Tue, 1 Mar 2011 21:12:56 +0100 Subject: * fixed potential crash at application shutdown when calling lua_close() on a killed thread's VM. * exposed cancel_test() in the lanes to enable manual testing for cancellation requests. * removed kludgy {globals={threadName}} support, replaced with a new function set_debug_threadname(). --- CHANGES | 14 ++-- Makefile | 4 + docs/index.html | 72 ++++++++--------- src/keeper.c | 6 ++ src/lanes.c | 238 +++++++++++++++++++++++++++++++++++-------------------- src/lanes.lua | 4 +- tests/atexit.lua | 45 +++++++++++ 7 files changed, 250 insertions(+), 133 deletions(-) create mode 100644 tests/atexit.lua diff --git a/CHANGES b/CHANGES index bc0368a..58b6061 100644 --- a/CHANGES +++ b/CHANGES @@ -3,12 +3,14 @@ CHANGES: CHANGE X: -CHANGE 29 BGe 21-Feb-2011 - Make the number of internal keeper states selectable by an optional parameter passed to require. +CHANGE 29 BGe 1-Mar-2011 + fixed potential crash at application shutdown when calling lua_close() on a killed thread's VM. + exposed cancel_test() in the lanes to enable manual testing for cancellation requests. + removed kludgy {globals={threadName}} support, replaced with a new function set_debug_threadname(). CHANGE 28 BGe 18-Feb-2011 - Moved keeper-related code in a separate source file - keeper.lua is now embedded in text form instead of bytecode to improve LuaJIT2-compatibility + - moved keeper-related code in a separate source file + - keeper.lua is now embedded in text form instead of bytecode to improve LuaJIT2-compatibility CHANGE 27 BGe 17-Feb-2011 - we know Lanes is loaded in the master state, so we don't force it @@ -29,7 +31,7 @@ CHANGE 26 BGe 14-Feb-2011: inter-state data copy for unsupported types CHANGE 25 BGe 12-Feb-2011: - Changed idfunc signature and contract to clarify the fact it is not lua-callable + Changed idfunc signature and contract to clarify that fact it is not lua-callable and to be able to require the module it was exported from in the target lanes CHANGE 24 DPtr 25-Jan-2011: @@ -59,7 +61,7 @@ CHANGE 19 BGe 2-Dec-2010: CHANGE 18 BGe 6-Oct-2010: Fixed 'memory leak' in some situations where a free running lane is collected before application shutdown - A bit of code cleanup + A bit of code cleanup CHANGE 17 BGe 21-Sept-2010: Fixed stupid compilation errors. diff --git a/Makefile b/Makefile index f27404a..ddd8675 100644 --- a/Makefile +++ b/Makefile @@ -84,6 +84,7 @@ test: $(MAKE) fibonacci $(MAKE) recursive $(MAKE) func_is_string + $(MAKE) atexit basic: tests/basic.lua $(_TARGET_SO) $(_PREFIX) $(LUA) $< @@ -154,6 +155,9 @@ appendud: tests/appendud.lua $(_TARGET_SO) func_is_string: tests/func_is_string.lua $(_TARGET_SO) $(_PREFIX) $(LUA) $< +atexit: tests/atexit.lua $(_TARGET_SO) + $(_PREFIX) $(LUA) $< + #--- perftest-plain: tests/perftest.lua $(_TARGET_SO) $(MAKE) _perftest ARGS="$< $(N) -plain" diff --git a/docs/index.html b/docs/index.html index 3d2ecf2..ba25515 100644 --- a/docs/index.html +++ b/docs/index.html @@ -56,7 +56,7 @@


Copyright © 2007-11 Asko Kauppi. All rights reserved.
Lua Lanes is published under the same MIT license as Lua 5.1. -

This document was revised on 21-Feb-11, and applies to version 2.1.0. +

This document was revised on 1-Mar-11, and applies to version 2.1.0.

@@ -243,6 +243,7 @@ also in the new lanes. :receive() or :send() call. With this option, one can set cancellation check to occur every N Lua statements. The value true uses a default value (100). + It is also possible to manually test for cancel requests with cancel_test(). @@ -250,20 +251,21 @@ also in the new lanes. Sets the globals table for the launched threads. This can be used for giving them constants. -

+
The global values of different lanes are in no manner connected; - modifying one will only affect the particular lane. Settings the variable 'threadName' in this table makes VS display the sent name instead of the normal thread name while debugging. + modifying one will only affect the particular lane. .priority
-2..+2 The priority of lanes generated. -2 is lowest, +2 is highest. -

+
Implementation and dependability of priorities varies by platform. Especially Linux kernel 2.6 is not supporting priorities in user mode. - +

Each lane also gets a function set_debug_threadname() that it can use anytime to do as the name says. +Supported debuggers are Microsoft Visual Studio (for the C side) and Decoda (for the Lua side).

Free running lanes

@@ -374,10 +376,15 @@ that id over a Linda once that thread is done (as the last thing you do).

Cancelling

- bool= lane_h:cancel( [timeout_secs=0.0,] [force_kill_bool=false] ) + bool= lane_h:cancel( [timeout_secs=0.0,] [force_kill_bool=false] ) +
+
+ + bool= cancel_test() +
-

Sends a cancellation request to the lane. If timeout_secs is non-zero, waits +

cancel()sends a cancellation request to the lane. If timeout_secs is non-zero, waits for the request to be processed, or a timeout to occur. Returns true if the lane was already done (in "done", "error" or "cancelled" status) or if the cancellation was fruitful within timeout period. @@ -387,8 +394,9 @@ OS thread running the lane is forcefully killed. This means no GC, and should generally be the last resort.

Cancellation is tested before going to sleep in receive() or send() calls -and after executing cancelstep Lua statements. A currently pending receive -or send call is currently not awakened, and may be a reason for a non-detected cancel. +and after executing cancelstep Lua statements. A currently pending receive() +or send() call is currently not awakened, and may be a reason for a non-detected cancel. +It is also possible to manually test for cancel requests with cancel_test().

@@ -799,7 +807,7 @@ can be used for custom userdata as well. Here's what to do.

  1. Provide an identity function for your userdata, in C. This function is - used for creation and deletion of your deep userdata (the shared resource), +used for creation and deletion of your deep userdata (the shared resource), and for making metatables for the state-specific proxies for accessing it. The prototype is @@ -817,12 +825,12 @@ can be used for custom userdata as well. Here's what to do. "delete": receives this same pointer on the stack, and should cleanup the object.
  2. "metatable": should build a metatable for the object. Don't cache the metatable - yourself, Lanes takes care of it ("metatable" should only be invoked once).
  3. +yourself, Lanes takes care of it ("metatable" should only be invoked once).
  4. "module": is the name of the module that exports the idfunc, - to be pushed on the stack as a string. It is necessary so that Lanes can require it in - any Lane and keeper state that receives a userdata. This is to prevent crashes in situations - where the module could be unloaded while the idfunc pointer is still held.
  5. +to be pushed on the stack as a string. It is necessary so that Lanes can require it in +any Lane and keeper state that receives a userdata. This is to prevent crashes in situations +where the module could be unloaded while the idfunc pointer is still held. Take a look at linda_id in lanes.c. @@ -917,10 +925,10 @@ Here are some things one should consider, if best performance is vital: merged into one main timer state (see timer.lua); no OS side timers are utilized. -
  6. Lindas are hashed to a number of "keeper states", which are a locking entity. - If you are using a lot of Linda objects, it may be useful to try having more of - these keeper states. By default, only one is used but this is an implementation detail. - It is possible to require( "lanes", N) to use more keeper states. +
  7. Lindas are hashed to a fixed number of "keeper states", which are a locking entity. + If you are using a lot of Linda objects, + it may be useful to try having more of these keeper states. By default, + only one is used (see KEEPER_STATES_N), but this is an implementation detail.
  8. @@ -952,31 +960,15 @@ its actual value.

    Change log

    -Feb-2011 (2.1.0) + +Mar-2011 (2.1.0)

      -
    • Added an auto-require mechanism to ensure any deep userdata transiting in a lane causes its parent module to be required in that lane -
        -
      • Changed idfunc signature and contract to clarify the fact it is not lua-callable and to be able to require the module it was exported from in the target lanes
      • -
      • When a deep userdata idfunc requests a module to be required, manually check that it is not loaded before requiring it instead of relying on the require function's loop detection feature
      • -
      • When a module must be required, raise an error if the 'require' function is not found in the target state
      • -
      • We know Lanes is loaded in the master state, so we don't force it to be required in every lane too when a linda deep userdata is copied
      • -
      -
    • -
    • Fixed application hang-up because keeper state was not released in case of errors thrown by inter-state data copy for unsupported types
    • -
    • Refactor lane proxy implementation: it is now a full userdata instead of a table, and its methods are implemented in C instead of Lua -
        -
      • its metatable is no longer accessible
      • -
      • writing to the proxy raises an error
      • -
      • it is no longer possible to overwrite its join() and cancel() methods
      • -
      -
    • -
    • Moved keeper-related code in a separate source file
    • -
    • keeper.lua is now embedded in text form instead of bytecode to improve LuaJIT2-compatibility
    • -
    • Make the number of internal keeper states selctable by an optional parameter passed to require.
    • +
    • fixed potential crash at application shutdown when calling lua_close() on a killed thread's VM.
    • +
    • exposed cancel_test() in the lanes to enable manual testing for cancellation requests.
    • +
    • removed kludgy {globals={threadName}} support, replaced with a new function set_debug_threadname().
    - Feb-2011 (2.0.11): -
      +
      • Fixed bug where reference to Linda object was dropped for a short time (crashing if GC was run during that time).
      • Changed the atexit code to trip the timer thread's write signal.
      • Changed lanes.c to export functions as a module rather than writing them directly to the globals table.
      • diff --git a/src/keeper.c b/src/keeper.c index f89c638..01e8880 100644 --- a/src/keeper.c +++ b/src/keeper.c @@ -94,6 +94,12 @@ char const *init_keepers( int const _nbKeepers) if (!L) return "out of memory"; + // to see VM name in Decoda debugger + lua_pushliteral( L, "Keeper #"); + lua_pushinteger( L, i + 1); + lua_concat( L, 2); + lua_setglobal( L, "decoda_name"); + luaG_openlibs( L, "io,table,package" ); // 'io' for debugging messages, package because we need to require modules exporting idfuncs serialize_require( L); diff --git a/src/lanes.c b/src/lanes.c index 8b62532..e5a2987 100644 --- a/src/lanes.c +++ b/src/lanes.c @@ -119,8 +119,6 @@ struct s_lane { // // M: sub-thread OS thread // S: not used - - char threadName[64]; //Optional, for debugging and such. owerflowable by a strcpy. lua_State *L; // @@ -132,6 +130,10 @@ struct s_lane { // M: sets to PENDING (before launching) // S: updates -> RUNNING/WAITING -> DONE/ERROR_ST/CANCELLED + volatile SIGNAL_T waiting_on; + // + // When status is WAITING, points on the linda's signal the thread waits on, else NULL + volatile bool_t cancel_request; // // M: sets to FALSE, flags TRUE for cancel request @@ -333,17 +335,21 @@ LUAG_FUNC( linda_send) { prev_status = s->status; s->status = WAITING; + ASSERT_L( s->waiting_on == NULL); + s->waiting_on = &linda->read_happened; } if( !SIGNAL_WAIT( &linda->read_happened, &K->lock_, timeout)) { if( s) { + s->waiting_on = NULL; s->status = prev_status; } break; } if( s) { + s->waiting_on = NULL; s->status = prev_status; } } @@ -450,17 +456,21 @@ LUAG_FUNC( linda_receive) { prev_status = s->status; s->status = WAITING; + ASSERT_L( s->waiting_on == NULL); + s->waiting_on = &linda->write_happened; } if( !SIGNAL_WAIT( &linda->write_happened, &K->lock_, timeout)) { if( s) { + s->waiting_on = NULL; s->status = prev_status; } break; } if( s) { + s->waiting_on = NULL; s->status = prev_status; } } @@ -870,28 +880,33 @@ volatile DEEP_PRELUDE *timer_deep; // = NULL /* * Process end; cancel any still free-running threads */ -static void selfdestruct_atexit( void ) { - +static void selfdestruct_atexit( void ) +{ if (selfdestruct_first == SELFDESTRUCT_END) return; // no free-running threads - // Signal _all_ still running threads to exit + // Signal _all_ still running threads to exit (including the timer thread) // MUTEX_LOCK( &selfdestruct_cs ); { struct s_lane *s= selfdestruct_first; - while( s != SELFDESTRUCT_END ) { - s->cancel_request= TRUE; - s= s->selfdestruct_next; + while( s != SELFDESTRUCT_END ) + { + // attempt a regular unforced cancel with a small timeout + bool_t cancelled = thread_cancel( s, 0.0001, FALSE); + // if we failed, and we know the thread is waiting on a linda + if( cancelled == FALSE && s->status == WAITING && s->waiting_on != NULL) + { + // signal the linda the wake up the thread so that it can react to the cancel query + // let us hope we never land here with a pointer on a linda that has been destroyed... + SIGNAL_T waiting_on = s->waiting_on; + s->waiting_on = NULL; + SIGNAL_ALL( waiting_on); + } + s = s->selfdestruct_next; } } MUTEX_UNLOCK( &selfdestruct_cs ); - // Tell the timer thread to check it's cancel request - { - struct s_Linda *td = timer_deep->deep; - SIGNAL_ALL( &td->write_happened); - } - // When noticing their cancel, the lanes will remove themselves from // the selfdestruct chain. @@ -914,15 +929,37 @@ static void selfdestruct_atexit( void ) { // -- AKa 25-Oct-2008 // #ifndef ATEXIT_WAIT_SECS - # define ATEXIT_WAIT_SECS (0.1) + # define ATEXIT_WAIT_SECS (0.25) #endif { double t_until= now_secs() + ATEXIT_WAIT_SECS; - - while( selfdestruct_first != SELFDESTRUCT_END ) { + + while( selfdestruct_first != SELFDESTRUCT_END ) + { YIELD(); // give threads time to act on their cancel - - if (now_secs() >= t_until) break; + { + // count the number of cancelled thread that didn't have the time to act yet + int n = 0; + double t_now = 0.0; + MUTEX_LOCK( &selfdestruct_cs ); + { + struct s_lane *s = selfdestruct_first; + while( s != SELFDESTRUCT_END) + { + if( s->cancel_request) + ++ n; + s = s->selfdestruct_next; + } + } + MUTEX_UNLOCK( &selfdestruct_cs ); + // if timeout elapsed, or we know all threads have acted, stop waiting + t_now = now_secs(); + if( n == 0 || ( t_now >= t_until)) + { + DEBUGEXEC(fprintf( stderr, "%d uncancelled lane(s) remain after waiting %fs at process end.\n", n, ATEXIT_WAIT_SECS - (t_until - t_now))); + break; + } + } } } #endif @@ -951,18 +988,21 @@ static void selfdestruct_atexit( void ) { // DEBUGEXEC(fprintf( stderr, "Left %d lane(s) with cancel request at process end.\n", n )); #else + // first thing we did was to raise the linda signals the threads were waiting on (if any) + // therefore, any well-behaved thread should be in CANCELLED state + // these are not running, and the state can be closed n=0; MUTEX_LOCK( &selfdestruct_cs ); { struct s_lane *s= selfdestruct_first; - while( s != SELFDESTRUCT_END ) { + while( s != SELFDESTRUCT_END) + { struct s_lane *next_s= s->selfdestruct_next; s->selfdestruct_next= NULL; // detach from selfdestruct chain - - THREAD_KILL( &s->thread ); - lua_close(s->L); - free(s); - s= next_s; + THREAD_KILL( &s->thread); + // NO lua_close() in this case because we don't know where execution of the state was interrupted + free( s); + s = next_s; n++; } selfdestruct_first= SELFDESTRUCT_END; @@ -1021,6 +1061,19 @@ static void cancel_hook( lua_State *L, lua_Debug *ar ) { } +//--- +// bool= cancel_test() +// +// Available inside the global namespace of lanes +// returns a boolean saying if a cancel request is pending +// +LUAG_FUNC( cancel_test) +{ + bool_t test = cancel_test( L); + lua_pushboolean( L, test); + return 1; +} + //--- // = _single( [cores_uint=1] ) // @@ -1131,12 +1184,12 @@ typedef struct tagTHREADNAME_INFO } THREADNAME_INFO; #pragma pack(pop) -void SetThreadName( DWORD dwThreadID, char* threadName) +void SetThreadName( DWORD dwThreadID, char const *_threadName) { THREADNAME_INFO info; Sleep(10); info.dwType = 0x1000; - info.szName = threadName; + info.szName = _threadName; info.dwThreadID = dwThreadID; info.dwFlags = 0; @@ -1150,6 +1203,22 @@ void SetThreadName( DWORD dwThreadID, char* threadName) } #endif +LUAG_FUNC( set_debug_threadname) +{ + char const *threadName; + luaL_checktype( L, -1, LUA_TSTRING); + threadName = lua_tostring( L, -1); + +#if defined PLATFORM_WIN32 && !defined __GNUC__ + // to see thead name in Visual Studio C debugger + SetThreadName(-1, threadName); +#endif + + // to see VM name in Decoda debugger Virtual Machine window + lua_setglobal( L, "decoda_name"); + + return 0; +} //--- #if (defined PLATFORM_WIN32) || (defined PLATFORM_POCKETPC) @@ -1162,11 +1231,6 @@ void SetThreadName( DWORD dwThreadID, char* threadName) int rc, rc2; lua_State *L= s->L; - -#if defined PLATFORM_WIN32 && !defined __GNUC__ - SetThreadName(-1, s->threadName); -#endif - s->status= RUNNING; // PENDING -> RUNNING // Tie "set_finalizer()" to the state @@ -1174,6 +1238,16 @@ void SetThreadName( DWORD dwThreadID, char* threadName) lua_pushcfunction( L, LG_set_finalizer ); lua_setglobal( L, "set_finalizer" ); + // Tie "set_debug_threadname()" to the state + // + lua_pushcfunction( L, LG_set_debug_threadname); + lua_setglobal( L, "set_debug_threadname" ); + + // Tie "cancel_test()" to the state + // + lua_pushcfunction( L, LG_cancel_test); + lua_setglobal( L, "cancel_test" ); + #ifdef ERROR_FULL_STACK STACK_GROW( L, 1 ); lua_pushcfunction( L, lane_error ); @@ -1240,7 +1314,7 @@ void SetThreadName( DWORD dwThreadID, char* threadName) // lua_newtable(L); } - + s->waiting_on = NULL; // just in case if (s->selfdestruct_next != NULL) { // We're a free-running thread and no-one's there to clean us up. // @@ -1276,7 +1350,6 @@ void SetThreadName( DWORD dwThreadID, char* threadName) MUTEX_UNLOCK( &s->done_lock_ ); #endif } - return 0; // ignored } @@ -1295,7 +1368,6 @@ LUAG_FUNC( thread_new ) lua_State *L2; struct s_lane *s; struct s_lane **ud; - const char *threadName = 0; const char *libs= lua_tostring( L, 2 ); uint_t cs= luaG_optunsigned( L, 3,0); @@ -1330,10 +1402,7 @@ LUAG_FUNC( thread_new ) luaL_error( L, "Expected table, got %s", luaG_typename(L,glob) ); lua_pushvalue( L, glob ); - lua_pushstring( L, "threadName"); - lua_gettable( L, -2); - threadName = lua_tostring( L, -1); - lua_pop( L, 1); + luaG_inter_move( L, L2, 1); // moves the table to L2 // L2 [-1]: table of globals @@ -1398,11 +1467,9 @@ LUAG_FUNC( thread_new ) //memset( s, 0, sizeof(struct s_lane) ); s->L= L2; s->status= PENDING; + s->waiting_on = NULL; s->cancel_request= FALSE; - threadName = threadName ? threadName : ""; - strcpy(s->threadName, threadName); - #if !( (defined PLATFORM_WIN32) || (defined PLATFORM_POCKETPC) || (defined PTHREAD_TIMEDJOIN) ) MUTEX_INIT( &s->done_lock_ ); SIGNAL_INIT( &s->done_signal_ ); @@ -1484,7 +1551,7 @@ LUAG_FUNC( thread_gc ) #if 0 lua_close( s->L ); s->L = 0; -#else +#else // 0 DEBUGEXEC(fprintf( stderr, "** Joining with a killed thread (needs testing) **" )); #if (defined PLATFORM_WIN32) || (defined PLATFORM_POCKETPC) || (defined PTHREAD_TIMEDJOIN) THREAD_WAIT( &s->thread, -1 ); @@ -1492,7 +1559,7 @@ LUAG_FUNC( thread_gc ) THREAD_WAIT( &s->thread, &s->done_signal_, &s->done_lock_, &s->status, -1 ); #endif DEBUGEXEC(fprintf( stderr, "** Joined ok **" )); -#endif +#endif // 0 } else if( s->L) { @@ -1529,54 +1596,55 @@ LUAG_FUNC( thread_gc ) // managed to cancel it. // false if the cancellation timed out, or a kill was needed. // -LUAG_FUNC( thread_cancel ) +static bool_t thread_cancel( struct s_lane *s, double secs, bool_t force) { - struct s_lane *s= lua_toLane(L,1); - double secs= 0.0; - uint_t force_i=2; - bool_t force, done= TRUE; - - if (lua_isnumber(L,2)) { - secs= lua_tonumber(L,2); - force_i++; - } else if (lua_isnil(L,2)) - force_i++; - - force= lua_toboolean(L,force_i); // FALSE if nothing there - - // We can read 's->status' without locks, but not wait for it (if Posix no PTHREAD_TIMEDJOIN) - // - if (s->status < DONE) { - s->cancel_request= TRUE; // it's now signalled to stop - - done= thread_cancel( s, secs, force ); - } - - lua_pushboolean( L, done ); - return 1; -} - -static bool_t thread_cancel( struct s_lane *s, double secs, bool_t force ) -{ - bool_t done= + bool_t done= TRUE; + // We can read 's->status' without locks, but not wait for it (if Posix no PTHREAD_TIMEDJOIN) + // + if( s->status < DONE) + { + s->cancel_request = TRUE; // it's now signalled to stop + done= #if (defined PLATFORM_WIN32) || (defined PLATFORM_POCKETPC) || (defined PTHREAD_TIMEDJOIN) - THREAD_WAIT( &s->thread, secs ); + THREAD_WAIT( &s->thread, secs); #else - THREAD_WAIT( &s->thread, &s->done_signal_, &s->done_lock_, &s->status, secs ); + THREAD_WAIT( &s->thread, &s->done_signal_, &s->done_lock_, &s->status, secs); #endif - if ((!done) && force) { - // Killing is asynchronous; we _will_ wait for it to be done at - // GC, to make sure the data structure can be released (alternative - // would be use of "cancellation cleanup handlers" that at least - // PThread seems to have). - // - THREAD_KILL( &s->thread ); - s->mstatus= KILLED; // mark 'gc' to wait for it - } - return done; + if ((!done) && force) + { + // Killing is asynchronous; we _will_ wait for it to be done at + // GC, to make sure the data structure can be released (alternative + // would be use of "cancellation cleanup handlers" that at least + // PThread seems to have). + // + THREAD_KILL( &s->thread); + s->mstatus= KILLED; // mark 'gc' to wait for it + } + } + return done; } +LUAG_FUNC( thread_cancel) +{ + struct s_lane *s= lua_toLane(L,1); + double secs= 0.0; + uint_t force_i=2; + bool_t force, done= TRUE; + + if (lua_isnumber(L,2)) { + secs= lua_tonumber(L,2); + force_i++; + } else if (lua_isnil(L,2)) + force_i++; + + force= lua_toboolean(L,force_i); // FALSE if nothing there + + done = thread_cancel( s, secs, force); + + lua_pushboolean( L, done); + return 1; +} //--- // str= thread_status( lane ) diff --git a/src/lanes.lua b/src/lanes.lua index 78582f9..704559a 100644 --- a/src/lanes.lua +++ b/src/lanes.lua @@ -399,8 +399,8 @@ if first_time then -- We let the timer lane be a "free running" thread; no handle to it -- remains. -- - gen( "io,package", { priority=max_prio, globals={threadName="LanesTimer"} }, function() - + gen( "io,package", { priority=max_prio}, function() + set_debugger_threadname( "LanesTimer") while true do local next_wakeup= check_timers() diff --git a/tests/atexit.lua b/tests/atexit.lua new file mode 100644 index 0000000..fb4f34a --- /dev/null +++ b/tests/atexit.lua @@ -0,0 +1,45 @@ +require "lanes" + +-- create a free-running lane + +local linda = lanes.linda() + +local f = function( _linda) + _linda:receive("oy") +end + +local g = function() + local cancelled + repeat + cancelled = cancel_test() + until cancelled + print "User cancellation detected!" +end + +local genF = lanes.gen( "", {globals = {threadName = "mylane"}}, f) +local genG = lanes.gen( "", g) + + +-- launch a good batch of free running lanes +for i = 1, 10 do + -- if i%50 == 0 then print( i) end + local h = genF( linda) + local status + repeat + status = h.status + --print( status) + until status == "waiting" + + -- [[ + local h = genG() + local status + repeat + status = h.status + --print( status) + until status == "running" + --]] +end + +print "exiting" + +-- let process end terminate them and see what happens \ No newline at end of file -- cgit v1.2.3-55-g6feb