Similar, but interrupts are kernel or hardware level constructs and preemptive. Coroutine and generators are programming language constructs that control the flow of execution, which are non-preemptive (or cooperative). In interrupts, the interrupt handler is required to save states so that it can be restored once the interrupt handler complete. Interrupts can happen in very undesirable situation. Yielding is a voluntary operation by the callee to relinquish control back to the caller. Hence the yielder is always in control on when control is passed and also accepted back. There are differences between the 2 modes of interruption.
Normally functions, once invoked can only be pass back to the caller via a return or exit. Normally functions do not suspend. `yield` keyword in programming languages provide a way to suspend the currently executed function (or threads in some programming languages) and return the execution flow to the caller(function/thread).
Basically it is a flow suspension technique under the same thread/process. One can resume the flow after the `yield` instruction. Invocation of the function do not need to restart from the top of the function. Think if it like suspending your OS and put your system to sleep and then when you start your OS again, you resume from where you left off.
Programs flow is just a collection of memory(registers/heap/stack) state. Yield also allow for data to be injected back into the program flow on next execution. This form of execution manipulation provides both segregation of concern and still inter-interaction between 2 different flows.
Python:
def func_a():
counter = 0
while True:
print("FUNC_A: %d" % counter)
yield counter
counter += 1
def func_b():
counter = 0
while True:
print("FUNC_B: %d" % counter)
yield counter
counter += 1
a = func_a()
b = func_b()
print(a)
print(b)
counter = 10
while counter > 0:
next(a)
next(b)
counter -= 1
Execution:
Code:
<generator object func_a at 0x7fab9ba635f0>
<generator object func_b at 0x7fab9ba63580>
FUNC_A: 0
FUNC_B: 0
FUNC_A: 1
FUNC_B: 1
FUNC_A: 2
FUNC_B: 2
FUNC_A: 3
FUNC_B: 3
FUNC_A: 4
FUNC_B: 4
FUNC_A: 5
FUNC_B: 5
FUNC_A: 6
FUNC_B: 6
FUNC_A: 7
FUNC_B: 7
FUNC_A: 8
FUNC_B: 8
FUNC_A: 9
FUNC_B: 9
Notice the functions will never cease since each are in infinite loops. Without yield, you will never be able to halt the program normally. But with yield, you can suspend the execution of the function. Of course, this is not why you use such technique. It is the flows that is intended and with it, one does not need to design very complicated iterators functions that can suspend from one iteration to another in a controlled manner. Without it, the technique of the function can get somewhat complicated with the need to return states or using closures to achieve the same flow.
One very useful case is say you have a recursive sudoku solver, but you know a sudoku problem can have multiple solutions. Using generators, you can suspend halfway in a recursive execution for one solution, then return this solution to the caller. If the caller wanted more solutions(if they exist), just resume from where the recursive function suspended. Hope this helps to give you some insights to what are the potential use of it.
Note: The above is just superficially scratching the surface of yielding flow control. The caller can also control in the mist of suspension.
Python:
def func():
counter = 0
while True:
print("FUNC: %d" % counter)
val = (yield counter)
if val is None:
counter += 1
else:
counter = val
f = func()
print(f)
print("OUT: %d" % next(f))
print("OUT: %d" % next(f))
print("OUT: %d" % next(f))
print("OUT: %d" % f.send(20))
print("OUT: %d" % next(f))
print("OUT: %d" % next(f))
print("OUT: %d" % f.send(0))
print("OUT: %d" % next(f))
print("OUT: %d" % next(f))
Execution:
Code:
<generator object func at 0x7fd48421c580>
FUNC: 0
OUT: 0
FUNC: 1
OUT: 1
FUNC: 2
OUT: 2
FUNC: 20
OUT: 20
FUNC: 21
OUT: 21
FUNC: 22
OUT: 22
FUNC: 0
OUT: 0
FUNC: 1
OUT: 1
FUNC: 2
OUT: 2