The asynchronous concept of programming is that the result of executing a function is not immediately available, but after some time in the form of some asynchronous (violating the usual order of execution) call. Why is this useful? Consider a few examples.
The first example is a network server, a web application. Most often, as such, calculations on the processor such applications do not perform. Most of the time (real, non-processor) is spent on I / O: reading a request from the client, accessing the disk for data, network access to other subsystems (databases caching servers, RPCs, etc.), recording the response to the client. During these I / O operations, the processor is idle; it can be loaded by processing requests from other clients. There are various ways to solve this problem: a separate process for each connection (
Apache mpm_prefork,
PostgreSQL ,
PHP FastCGI), a separate thread (thread) for each connection or a combined process / thread variant (
Apache mpm_worker,
MySQL ). The approach using processes or threads shifts processor multiplexing between the processed connections on the OS, while using relatively many resources (memory, context switches, etc.), this option is not suitable for processing a large number of simultaneous connections, but is ideal for situations where the amount of computation is quite high (for example, in a DBMS). The potential use of all available processors in a multiprocessor architecture can be added to the pluses of the thread and process model.
An alternative is to use a single-threaded model using asynchronous I / O primitives provided by the OS (select, poll, etc.). At the same time, the amount of resources for each new serviced connection is not so large (a new socket, some structures in the application's memory). However, programming is much more complicated, since data from network sockets comes in some “fragments”, and in one processing cycle data comes from different connections in different states, some connections may be incoming from clients, some coming from external resources (DB, other server, etc.). ). Various concepts are used to simplify development: callback, state machines, and others. Examples of network servers using asynchronous I / O:
nginx ,
lighttpd ,
HAProxy ,
pgBouncer , etc. It is with such a single-threaded model that the need for asynchronous programming arises. For example, we want to execute a query in the database. From the point of view of the program, the execution of a request is a network I / O: connecting to the server, sending a request, waiting for a response, reading the response of the database server. Therefore, if we call the function “execute a database query”, then it will not be able to return the result immediately (otherwise it would have to be blocked), and return only something that will allow to receive the result of the query or, possibly, an error (there is no server connection, incorrect request, etc.) This return value is convenient to do exactly Deferred.
The second example is related to the development of common desktop applications. Suppose we decided to make an analogue of
Miranda (
QIP ,
MDC , ...), that is, our own messenger. In the program interface there is a contact list where you can delete a contact. When the user selects this action, he expects the contact to disappear on the screen and that he will indeed be removed from the contact list. In fact, the delete operation from the server contact list relies on the network interaction with the server, and the user interface should not be blocked for the duration of the operation, so in any case, after the operation, some asynchronous interaction with the result of the operation will be required. You can use the signal-slots mechanism, callbacks, or something else, but Deferred is best suited: deleting from the contact list returns Deferred, in which either a positive result (everything is good) or an exception (an exact error that must be reported to the user): in case of an error, the contact must restore the contact in the contact list.
')
Examples can be cited for a long time and a lot, now about what is Deferred. Deferred is the heart of
Twisted asynchronous network programming framework in Python. This is a simple and slim concept that allows you to translate synchronous programming into asynchronous code without inventing a bicycle for each situation and ensuring high quality code. Deferred is just the return result of the function when this result is unknown (not received, will be received in another thread, etc.) What can we do with Deferred? We can “hang” in the chain of handlers that will be called when the result is obtained. At the same time, Deferred can carry not only a positive result of execution, but also exceptions generated by a function or handlers, it is possible to process, perevykin etc., etc. In fact, for a synchronous code, there is a more or less unambiguous parallel in terms of Deferred. For efficient development with Deferred, programming language features such as closures, lambda functions are useful.
Here is an example of synchronous code and its alternative in terms of Deferred:
try: # HTTP page = downloadPage(url) # print page except HTTPError, e: # print "An error occured: %s", e
In the asynchronous version with Deferred, it would be written as follows:
def printContents(contents): """ Callback, , . """ print contents def handleError(failure): """ Errback ( ), . """
In practice, we usually return Deferred from functions that receive Deferred in the course of our work, we hang a large number of handlers, we handle exceptions, we return some exceptions through Deferred (we throw it up). As a more complex example, let's take the code in an asynchronous version for the example of an atomic counter from the article about
data structures in memcached , here we assume that access to memcached as a network service goes through Deferred, i.e. Memcache class methods return Deferred (which returns either the result of the operation or an error):
class MCCounter(MemcacheObject): def __init__(self, mc, name): """ . @param name: @type name: C{str} """ super(MCCounter, self).__init__(mc) self.key = 'counter' + name def increment(self, value=1): """ . @param value: @type value: C{int} @return: Deferred, """ def tryAdd(failure): # KeyError, "" # failure.trap(KeyError) # , d = self.mc.add(self.key, value, 0) # - , # d.addErrback(tryIncr) # Deferred, "" # Deferred, return d def tryIncr(failure): # tryAdd failure.trap(KeyError) d = self.mc.incr(self.key, value) d.addErrback(tryAdd) return d # , Deferred d = self.mc.incr(self.key, value) # d.addErrback(tryAdd) # Deferred , : # ) , # ) (, ) return d def value(self): """ . @return: @rtype: C{int} @return: Deferred, """ def handleKeyError(failure): # KeyError failure.trap(KeyError) # — 0, # Deferred return 0 # d = self.mc.get(self.key) # d.addErrback(handleKeyError) # Deferred, # callback return d
The above code can be written “shorter” by combining commonly used operations, for example:
return self.mc.get(self.key).addErrback(handleKeyError)
For almost every construction of synchronous code, you can find an analogue in the asynchronous concept with Deferred:
- a sequence of synchronous operators corresponds to a callback chain with asynchronous calls;
- calling one subrgram with I / O from another corresponds to returning Deferred from Deferred (branching Deferred);
- deep nesting chain, the distribution of exceptions on the stack corresponds to the chain of functions that return each other Deferred;
- try..except blocks correspond to error handlers (errback), which can “throw” exceptions further, any exception in callback translates execution into errback;
- for the parallel execution of asynchronous operations, there is a
DeferredList
.
Threads are often used in asynchronous programs for performing computational procedures, performing blocking I / O (when there is no asynchronous counterpart). All this is easily modeled in a simple 'worker' model, then there is no need for explicit architecture in explicit synchronization, while everything is elegantly included in the general flow of calculations using Deferred:
def doCalculation(a, b): """ , -, . """ return a/b def printResult(result): print result def handleDivisionByZero(failure): failure.trap(ZeroDivisionError) print "Ooops! Division by zero!" deferToThread(doCalculation, 3, 2).addCallback(printResult).addCallback( lambda _: deferToThread(doCalculation, 3, 0).addErrback(handleDivisionByZero))
In the example above, the function
deferToThread
transfers the execution of the specified function to a separate thread and returns Deferred, through which the result of the function’s execution will be asynchronously received or an exception will be thrown. The first division (3/2) is performed in a separate thread, then its result is printed on the screen, and then another calculation (3/0) is started, which generates an exception processed by the
handleDivisionByZero
function.
In one article, I could not describe a part of what I would like to say about Deferred, but I did not write a word about how they work. If you have time to interest - read the materials below, and I promise to write more.
Additional materials