Does each task get launched on separate thread?
No, usually asyncio runs in single thread.
How asyncio package enables bar, to be an async task, with these keywords, under the hood?
When you define function as async this function becomes generator what allows to execute it "by steps" using __next__() method. await - is yield (yield from actually) point where execution flow returns to global event loop that manages executing of all coroutines.
This simple example shows how you can switch between execution flow of different generators:
def task(i): yield 1 print('task {}: step 1'.format(i)) yield 2 print('task {}: step 2'.format(i)) tasks = [ task(1), task(2), task(3), ] def execute_tasks(tasks): i = 0 finished = [] while True: # start executing tasks: try: tasks[i].__next__() except StopIteration: finished.append(i) # check if any task unfinished: if len(finished) == len(tasks): return # move to next unfinished task: while True: i += 1 if i > len(tasks) - 1: i = 0 if not i in finished: break if __name__ == '__main__': execute_tasks(tasks)
Output:
task 1: step 1 task 2: step 1 task 3: step 1 task 1: step 2 task 2: step 2 task 3: step 2
asyncio of course is much more complex and allows you much more.
Probably best explanation of how you can implement coroutines using generators I saw in this PyCon 2015 video: David Beazley - Python Concurrency From the Ground Up: LIVE! (source code). You should definitely watch it if you're going implement this.
But I advice you to use asyncio instead - it already exists for you, there's no need to invent your own.