diff --git a/README.md b/README.md index 20f88de..649b3ee 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,27 @@ # giambio Giambio: Asynchronous Python made easy (and friendly) + + +# What's that? + +giambio is a Python framework that implements the aysnchronous mechanism and allow to perform I/O multiplexing and basically do more than one thing at once. +This library implements what is known as a stackless mode of execution, or "green threads", though the latter term is misleading as **no multithreading is involved**. + + +## Disclaimer + +Right now this is no more than a toy implementation to help me understand how Python and async work, but it will (hopefully) become a production ready library soon + + +## Let's Code! + +Libraries like giambio shine the most when it comes to performing asyncronous I/O (reading a socket, writing to a file, stuff like that). + +The most common example of this is a network server, which just can't be handle one client at a time only. + +One possible approach to achieve concurrency is to use threads, and despite their bad reputation in Python due to the GIL, they actually might be a good choice when it comes to I/O (it is performed out of control of the GIL so that isn't trotthled like CPU-intensive tasks). Problem is, if you have 10000 concurrent connections, you would need to set a limit to the number of threads that can be spawned (you definitely don't want to spawn 10 thousands threads do you?). On top of that, threads are known to be tricky when it comes to synchronization and coordination, so in general you'll hear anyone yelling at you "Don't use threads!". That's when libraries like giambio come in handy! + +A library like giambio implements some low-level primitives and a main loop (known as an **Event Loop**) that acts like a kernel: If a function needs a "system" call (in this case it's I/O) it will trigger a trap and stop, waiting for the event loop to return control and proceed. In the meantime, the loop catches the trap and uses its associated metadata to perform the requested operation. But since I/O 99% of the times implies waiting for a resource to become available, the loop will also run other tasks while it's waiting for that I/O to happen. + +And that's how it works! + diff --git a/giambio/__init__.py b/giambio/__init__.py index 40fadc0..f3a4e94 100644 --- a/giambio/__init__.py +++ b/giambio/__init__.py @@ -1,6 +1,8 @@ __author__ = "Nocturn9x aka Isgiambyy" __version__ = (0, 0, 1) -from .core import EventLoop, sleep +from .core import EventLoop from .exceptions import GiambioError, AlreadyJoinedError, CancelledError +from .traps import sleep, join -__all__ = ["EventLoop", "sleep", "GiambioError", "AlreadyJoinedError", "CancelledError"] \ No newline at end of file + +__all__ = ["EventLoop", "sleep", "join", "GiambioError", "AlreadyJoinedError", "CancelledError"] diff --git a/giambio/abstractions.py b/giambio/abstractions.py new file mode 100644 index 0000000..d3d26fd --- /dev/null +++ b/giambio/abstractions.py @@ -0,0 +1,51 @@ +import types + + +class Result: + """A wrapper for results of coroutines""" + + def __init__(self, val=None, exc: Exception = None): + self.val = val + self.exc = exc + + def __repr__(self): + return f"giambio.core.Result({self.val}, {self.exc})" + + +class Task: + + """A simple wrapper around a coroutine object""" + + def __init__(self, coroutine: types.coroutine, loop): + self.coroutine = coroutine + self.status = False # Not ran yet + self.joined = False + self.result = None # Updated when the coroutine execution ends + self.loop = loop # The EventLoop object that spawned the task + self.cancelled = False + + def run(self): + self.status = True + return self.coroutine.send(None) + + def __repr__(self): + return f"giambio.core.Task({self.coroutine}, {self.status}, {self.joined}, {self.result})" + + def throw(self, exception: Exception): + self.result = Result(None, exception) + return self.coroutine.throw(exception) + + async def cancel(self): + return await cancel(self) + + async def join(self): + return await join(self) + + def get_result(self): + if self.result: + if self.result.exc: + raise self.result.exc + else: + return self.result.val + + diff --git a/giambio/core.py b/giambio/core.py index 4b9d80c..272a056 100644 --- a/giambio/core.py +++ b/giambio/core.py @@ -7,6 +7,8 @@ from .exceptions import AlreadyJoinedError, CancelledError from timeit import default_timer from time import sleep as wait from .socket import AsyncSocket +from .traps import join, sleep, want_read, want_write, cancel +from .abstractions import Task, Result class EventLoop: @@ -114,6 +116,8 @@ class EventLoop: await want_read(sock) return sock.accept() + + async def sock_sendall(self, sock: socket.socket, data: bytes): """Sends all the passed data, as bytes, trough the socket asynchronously""" @@ -153,99 +157,9 @@ class EventLoop: async def connect_sock(self, sock: socket.socket, addr: tuple): - await want_write(sock) - return sock.connect(addr) + try: + sock.connect(addr) + except BlockingIOError: + await want_write(sock) -class Result: - """A wrapper for results of coroutines""" - - def __init__(self, val=None, exc: Exception = None): - self.val = val - self.exc = exc - - def __repr__(self): - return f"giambio.core.Result({self.val}, {self.exc})" - - -class Task: - - """A simple wrapper around a coroutine object""" - - def __init__(self, coroutine: types.coroutine, loop: EventLoop): - self.coroutine = coroutine - self.status = False # Not ran yet - self.joined = False - self.result = None # Updated when the coroutine execution ends - self.loop = loop # The EventLoop object that spawned the task - self.cancelled = False - - def run(self): - self.status = True - return self.coroutine.send(None) - - def __repr__(self): - return f"giambio.core.Task({self.coroutine}, {self.status}, {self.joined}, {self.result})" - - def throw(self, exception: Exception): - self.result = Result(None, exception) - return self.coroutine.throw(exception) - - async def cancel(self): - return await cancel(self) - - async def join(self): - return await join(self) - - def get_result(self): - if self.result: - if self.result.exc: - raise self.result.exc - else: - return self.result.val - - -@types.coroutine -def sleep(seconds: int): - """Pause the execution of a coroutine for the passed amount of seconds, - without blocking the entire event loop, which keeps watching for other events - - This function is also useful as a sort of checkpoint, because it returns the execution - control to the scheduler, which can then switch to another task. If a coroutine does not have - enough calls to async methods (or 'checkpoints'), e.g one that needs the 'await' keyword before it, this might - affect performance as it would prevent the scheduler from switching tasks properly. If you feel - like this happens in your code, try adding a call to giambio.sleep(0); this will act as a checkpoint without - actually pausing the execution of your coroutine""" - - yield "want_sleep", seconds - - -@types.coroutine -def want_read(sock: socket.socket): - """'Tells' the event loop that there is some coroutine that wants to read from the passed socket""" - - yield "want_read", sock - - -@types.coroutine -def want_write(sock: socket.socket): - """'Tells' the event loop that there is some coroutine that wants to write into the passed socket""" - - yield "want_write", sock - - -@types.coroutine -def join(task: Task): - """'Tells' the scheduler that the desired task MUST be awaited for completion""" - - if not task.cancelled: - task.joined = True - yield "want_join", task - return task.get_result() - - -@types.coroutine -def cancel(task: Task): - """'Tells' the scheduler that the passed task must be cancelled""" - - yield "want_cancel", task diff --git a/giambio/traps.py b/giambio/traps.py new file mode 100644 index 0000000..9b11ce4 --- /dev/null +++ b/giambio/traps.py @@ -0,0 +1,51 @@ +"""Helper methods to interact with the event loop""" + +import types +import socket +from .abstractions import Task + + +@types.coroutine +def sleep(seconds: int): + """Pause the execution of a coroutine for the passed amount of seconds, + without blocking the entire event loop, which keeps watching for other events + + This function is also useful as a sort of checkpoint, because it returns the execution + control to the scheduler, which can then switch to another task. If a coroutine does not have + enough calls to async methods (or 'checkpoints'), e.g one that needs the 'await' keyword before it, this might + affect performance as it would prevent the scheduler from switching tasks properly. If you feel + like this happens in your code, try adding a call to giambio.sleep(0); this will act as a checkpoint without + actually pausing the execution of your coroutine""" + + yield "want_sleep", seconds + + +@types.coroutine +def want_read(sock: socket.socket): + """'Tells' the event loop that there is some coroutine that wants to read from the passed socket""" + + yield "want_read", sock + + +@types.coroutine +def want_write(sock: socket.socket): + """'Tells' the event loop that there is some coroutine that wants to write into the passed socket""" + + yield "want_write", sock + + +@types.coroutine +def join(task: Task): + """'Tells' the scheduler that the desired task MUST be awaited for completion""" + + task.joined = True + yield "want_join", task + return task.get_result() # This raises an exception if the child task errored + + +@types.coroutine +def cancel(task: Task): + """'Tells' the scheduler that the passed task must be cancelled""" + + yield "want_cancel", task +