In Web programming, there are often services that need to perform asynchronous operations when processing requests, such as time-consuming IO operations. After the asynchronous execution is completed, the request is terminated and the response is returned to the client. During this process, the connection between the client and server is kept. That is, the current request is blocked from the client’s point of view until the request ends.
The most important feature of asynchracy is that the server can continue processing other requests without being blocked.
Different languages handle this asynchronous scenario in very different ways. Common strategies are: Message sharing (asynchronous task queue), multi-threaded multi-process, Event (Linux signals, nodeJS event loop), coroutine (return Future, Promise represents the Future state of program execution), The Coroutine is the most widely used and is the subject of today’s article.
What is a coroutine? Simply put, it is a program that can be freely suspend, execute, and kill at any given moment. Programs control Coroutine like operating systems control Process, but at a much lower cost. This is one of the main reasons that many languages support asynchronous operations in a Coroutine manner, including Golang, Python, JavaScript(ES6), Erlang, and others.
Talk is cheap, show me your code. Here we use a very simple Web Chat demo app to list asynchrony in Golang, Python, and Nodejs.
Chat Demo App description
The Demo is just to show how Coroutine works in different languages, so the scenario is very simple: a content input box, and messages sent by any client can be displayed by other clients.
The project address
Github.com/zhyq0826/ch…
How Chat Demo App works
There are two main apis:
/a/message/new
Used for sending messages, called message-new here/a/message/updates
Used for message acceptance, referred to here as message-update
The Client obtains the latest message from the server through message-update. If there is no new message, the request is suspended and waits for a new message to be sent. When a new message arrives, the Client disconnects from the connection after receiving the latest message. After a certain interval, the server is asked to continue getting new messages and the process is repeated.
Since message-update may take a long time to get messages from the server, the server will hold on to the client connection. Therefore, requests from the message-Update client are required not to block the server from processing other requests, and message-Update needs to remain suspended until no messages arrive.
The Server processing of message-Update is an asynchronous process.
The implementation of the Python
Python uses yield to implement coroutines, but implementing coroutines on the Web requires special handling, We use Tornado, a Web framework supporting asynchronous Network, to implement message-Update processing.
A Future in Tornado represents a Future outcome. In an asynchronous request, yield resolves the Future, and if the Future is not completed, the request will continue to wait.
@gen.coroutine #1
def post(self):
cursor = self.get_argument("cursor".None)
# Save the future returned by wait_for_messages so we can cancel
# it in wait_for_messages
self.future = GLOBAL_MESSAGE_BUFFER.wait_for_messages(cursor=cursor)
messages = yield self.future # 2
if self.request.connection.stream.closed():
return
self.write(dict(messages=messages))Copy the code
#1 support coroutine for the current request via Tornado specific Gen. Coroutine, #2 is the future execution result of the current request waiting, Each message-Update client generates a future with a call to global_message_buffer. wait_for_messages, which is then added to the waiting list for messages. The request is suspended until the future is parsed. Tornado completes an asynchronous request through the combination of yield and future.
To understand how yield waits for the future to complete is to understand how the Python Generator parses, as we’ll see in detail in the table.
The realization of the Golang
Golang naturally supports coroutine at the language level. If you go func(), you can start coroutine execution. Isn’t it easy and exciting? The NET/HTTP package implemented by Go naturally supports Coroutine for HTTP requests, which does not require third-party libraries like Tornado (Python 2 here) to support. What Golang does better than Python is support for channel-based communication between Coroutines.
func MessageUpdatesHandler(w http.ResponseWriter, r *http.Request) {
client := Client{id: uuid.NewV4().String(), c: make(chan []byte#)}1
messageBuffer.NewWaiter(&client) #2
msg := <-client.c // Suspend the request and wait for the message #3
w.Header().Set("Content-Type"."application/json")
w.Write(msg)
}Copy the code
#1 generates a unique identity and channel for each client, and then the client joins the message #2 waiting list to wait for the message to arrive. #3 is the key to suspend the request: waiting for the message from the channel. By default, the message-update coroutine is blocked by #3, and the client connection is not broken.
The realization of Nodejs
Nodejs is asynchronous by nature and receives and executes asynchronous notifications through callback. For demonstration purposes we used Express, in which a request does not end without actively calling res.end or res.send or res.json. How does a request know in NodeJS that a message has arrived and needs a response? Python uses a Future, Golang uses a channel, and there’s more than one Nodejs implementation. Here we use events and promises.
A Promise, like a Future, represents a Future execution and is notified of the result in resolve and Reject, then or catch. Promise can effectively solve the problems of callback nesting and asynchronous execution errors that cannot be thrown out in NodeJS.
Promise as a specification is implemented in several third-party libraries in NodeJS, including bluebird.
Events are a common programming model in NodeJS, which should be familiar to those familiar with JavaScript.
app.post('/a/message/updates', function(req, res){
var p = new Promise(function(resolve, reject){
messageBuffer.messageEmitter.on("newMessage", function(data){ #1
resolve(data); #2
});
});
var client = makeClient(uuidv4(), p);
messageBuffer.newWaiter(client);
p.then(function(data){ #3
res.set('Content-Type', 'application/json');
res.json({'messages': data});
});
});Copy the code
Each message-Update client generates a Promise, and the Promise executes the Promise’s resolve #2 to inform the current client of the arrival of a message after the message arrival event newMessage #1 is triggered.
summary
The strategies for implementing asynchrony are different, with Golang being the easiest to understand and implement, thanks to Go’s innate support for Coroutine and powerful channel communication. The Python implementation in this paper is based on Python 2. In Python 3, the use of Coroutine is greatly improved, but it is still a fly in the face compared to Golang. Nodejs is a natural asynchronous back-end JavaScript, and it takes a lot of tricks to make full use of promises, but yield in ES6 and await/async in ES7 have improved asynchronous operations. Their approach is python-like (the ES6 draft is said to be implemented by a bunch of Python programmers).
Which language will you use next time you need asynchronous support for a project?
Python example from Tornado github.com/tornadoweb/…
Chat – address github.com/zhyq0826/ch app…
Promise Bluebird bluebirdjs.com/docs/gettin…
Tornado tornado. Readthedocs. IO/en/stable/g…
Golang channel tour.golang.org/concurrency…
Scan wecatch for the latest articles