# task_yield

Definition: | task_yield()
Description: | Yield control to the scheduler. The scheduler can then choose another task to run, or perhaps let the current task continue running.
pwa/p2js: | Not supported.
Comments: | Tasks should call task_yield() periodically so other that tasks have a chance to run. Only when task_yield() is called, is there a way for the scheduler to take back control from a task. This is known as cooperative multitasking. A task can have calls to task_yield() in many different places in its code, and at any depth of subroutine call. Note however there are significant restrictions for c_func/call_func/call_back/delete_routine/type-check, see technicalia. The scheduler uses the current value of {min, max} or the current number of consecutive runs remaining, in determining when to return to the current task. When control returns, execution continues with the statement that follows task_yield(). The call-stack and all private variables remain as they were when task_yield() was called. Global and local variables may have changed, due to the execution of other tasks. Tasks should try to call task_yield() often enough to avoid causing real-time tasks to miss their time window, and to avoid blocking time-shared tasks for an excessive period of time. On the other hand, there is a bit of overhead in calling task_yield(), and this overhead is slightly larger when an actual switch to a different task takes place. A task should avoid calling task_yield() when it is in the middle of a delicate operation that requires exclusive access to some data. Otherwise a race-condition could occur, where one task might interfere with an operation being carried out by another task. In some cases a task might need to mark some data as "locked" or "unlocked" in order to prevent this possibility. With cooperative multitasking, these concurrency issues are much less of a problem than with preemptive multithreading.
Example 1: |
    -- this is how the main thread might wait for all tasks to complete
    terminate_application = 1   -- tell threads to start shutting down, if appropriate
    while active_tasks>0 do     -- (active_tasks is a manually maintained counter)
        task_yield()
        sleep(0.1)              -- (recommended if any real-time tasks are in use)
    end while
    abort(0)
Example 2: |
    -- an alternative to the above:
    main_task_id = task_self()
    task_suspend(main_task_id)
    task_yield()
    -- (nb: if no other task invokes task_schedule(main_task_id), control never returns)
Example Programs | Both demo\rosetta\Generator_Exponential.exw and demo\rosetta\Same_Fringe.exw use task_yeild to implement a routine that can suspend itself mid-execution and later be resumed to obtain the next value, as many times as desired.
Implementation: | See builtins\VM\pTask.e (an autoinclude) for details of the actual implementation.
See Also: | task_create, task_schedule, task_suspend, task_self
Technicalia | It is important to note that in Phix the call stack is managed on the heap rather than the system stack. Each task has a private call stack, all of which are actually allocated from a single shared heap, but they could never overlap, and therefore each task also has unique private working space and unique references to private data. However all tasks share the same system stack (which in general is fairly lightly used by Phix). Consider the following code:
    #ilASM { push reg }
    task_yield()
    #ilASM { pop reg }
If multiple tasks attempt that, they will mix up each others values. At most one task gets to play with the system stack - in fact in the taskwire demo, the WM_TIMER handler is messing with the system stack, in the form of return addresses into kernel32.dll, but all the other tasks leave the stack balanced at all times. (I suppose that technically the "WM_TIMER task" does too, except in the other direction.) A simple way to ensure the stack remains balanced is to (only) invoke task_yield at the top level of the main task routine, and avoid calls to task_yield from (nested) invocations of c_func/proc/call_func/proc, and anything targetted by delete_routine, plus of course explicit inline assembly like that just shown. Note that it is perfectly valid to invoke task_yield from nested hll function and procedure calls, since they affect the heap stack rather than the system stack, with the exception of from within type-check routines. There is a way to invoke call_proc and leave the stack balanced, see pTask.e/call_current_task(). A similar trick is not currently available for call_func, but I suspect it would be mildly tricky as opposed to downright impossible (which I will reserve for call_back and delete_routine). It may be possible to write a thread-based version of VM/pTask.e that uses lock-step to ensure only one thread is actually running, which might alleviate many of these restrictions, but it may be tricky and I’m not particularly motivated at the moment. Generally I would say that the main GUI task always plays with the system stack, and must therefore be the only task allowed to do so, unless/except when the stack is properly restored prior to the next call to task_yield.