The last part of the tutorial deals with a more “advanced” issue of RPC programming,
asynchronous operation, which is a key feature of RPyC. The code you’ve seen so far was
synchronous – which is probably very similar to the code you normally write:
when you invoke a function, you block until the result arrives. Asynchronous invocation,
on the other hand, allows you to start the request and continue, rather than waiting.
Instead of getting the result of the call, you get a special object known as an
AsyncResult (also known as a “future” or “promise”]),
that will eventually hold the result.

Note that there is no guarantee on execution order for async requests!

In order to turn the invocation of a remote function (or any callable object) asynchronous,
all you have to do is wrap it with async_, which creates a
wrapper function that will return an AsyncResult instead of blocking. AsyncResult
objects have several properties and methods that

ready - indicates whether or not the result arrived

error - indicates whether the result is a value or an exception

expired - indicates whether the AsyncResult object is expired (its time-to-wait has
elapsed before the result has arrived). Unless set by set_expiry, the object will
never expire

value - the value contained in the AsyncResult. If the value has not yet arrived,
accessing this property will block. If the result is an exception, accessing this property
will raise it. If the object has expired, an exception will be raised. Otherwise, the
value is returned

wait() - wait for the result to arrive, or until the object is expired

add_callback(func) - adds a callback to be invoked when the value arrives

set_expiry(seconds) - sets the expiry time of the AsyncResult. By default, no
expiry time is set

This may sound a bit complicated, so let’s have a look at some real-life code, to convince you
it’s really not that scary:

Combining async_ and callbacks yields a rather interesting result: async callbacks,
also known as events. Generally speaking, events are sent by an “event producer” to
notify an “event consumer” of relevant changes, and this flow is normally one-way
(from producer to consumer). In other words, in RPC terms, events can be implemented as
async callbacks, where the return value is ignored. To better illustrate the situation,
consider the following FileMonitor example – it monitors a file
(using os.stat()) for changes, and notifies the client when a change occurs
(with the old and new stat results).

importrpycimportosimporttimefromthreadingimportThreadclassFileMonitorService(rpyc.SlaveService):classexposed_FileMonitor(object):# exposing names is not limited to methods :)def__init__(self,filename,callback,interval=1):self.filename=filenameself.interval=intervalself.last_stat=Noneself.callback=rpyc.async_(callback)# create an async callbackself.active=Trueself.thread=Thread(target=self.work)self.thread.start()defexposed_stop(self):# this method has to be exposed tooself.active=Falseself.thread.join()defwork(self):whileself.active:stat=os.stat(self.filename)ifself.last_statisnotNoneandself.last_stat!=stat:self.callback(self.last_stat,stat)# notify the client of the changeself.last_stat=stattime.sleep(self.interval)if__name__=="__main__":fromrpyc.utils.serverimportThreadedServerThreadedServer(FileMonitorService,port=18871).start()

Note that in this demo I used BgServingThread,
which basically starts a background thread to serve all incoming requests, while the main
thread is free to do as it wills. You don’t have to open a second thread for that,
if your application has a reactor (like gtk’s gobject.io_add_watch): simply register
the connection with the reactor for read, invoking conn.serve. If you don’t have a
reactor and don’t wish to open threads, you should be aware that these notifications will
not be processed until you make some interaction with the connection (which pulls all
incoming requests). Here’s an example of that: