Since gRPC’s asynchronous call code is rather convoluted, this article is mainly used to document gRPC’s asynchronous calls.
Note that gRPC uses the CompletionQueue binding for RPC calls in order to implement asynchronous calls, which can feel strange when writing the actual code. The response, because it is asynchronous, calls
CompletionQueue::Next to wait for the packet return operation. Leave an impression here first, it will be clearer when talking about the process below.
Compile and install
To complete our demo, you can install gRPC directly using the following command if you don’t have it installed.
The examples presented below all use the official: https://github.com/grpc/grpc/blob/master/examples/cpp/helloworld/.
For a synchronous client, calling a remote method blocks the current thread, but asynchronous allows multiple requests to be sent at the same time without blocking.
So when we use gRPC’s asynchronous API, we need to do the following.
- since we are making asynchronous non-blocking requests, we must not wait for packets to be returned when we send the request.
- all packets are processed asynchronously by another thread to avoid blocking of the main process.
- the data of the return packet is passed through some medium. gRPC uses a CompletionQueue to do this.
The overall process can be seen from the above diagram as follows.
- start the client and start a bypass thread to loop through the CompletionQueue data.
- send an asynchronous call to the server;
- if the server returns the packet, it will put the data into the CompletionQueue.
- the asynchronous thread gets the CompletionQueue data and returns it.
Let’s look at the following example.
The first step is to create the client to, and then asynchronously thread the server’s packet return, as it will block while processing the packet return.
Here the request will be sent by calling SayHello.
Since the packet return logic is not here, you can return directly after calling Finish without waiting. After calling this method, you can wait for the server to return the packet, and the server will stuff the packet data into cq_.
Asynchronous processing of packet returns
The logic for asynchronous packet processing is called in a loop inside the thread we created at the beginning.
Here will keep calling the Next method to process the server’s response, if there is no packet return will keep blocking, so here we need to start a new thread to avoid blocking the main process.
Generally, when we write the Server side, if it is a synchronous operation, the request will be processed immediately after it is received, and then the packet will be returned to the client, and the process of returning the packet needs to wait until the whole RPC request is finished, so there is a blocking waiting process.
However, when understanding the gRPC asynchronous API, it can still feel very awkward to understand the example code at first.
First of all, gRPC makes you prepare a CallData object as a container like a pipeline operation, and then gRPC sends various events to the CallData object through ServerCompletionQueue and lets the object process them according to its own state.
Then after processing the current event, you need to manually create another CallData object, which is prepared for the next Client request, the whole process is like a pipeline.
The above asynchronous process has a little state machine in it, all reversed by the CallData object.
- When the CallData object is first created, it is reversed from the CREATE state to the PROCESS state, indicating that it is waiting to receive the request.
- After the request comes in, a CallData object is first created, then reversed to the FINISH state after processing, waiting for the end of the packet return to the Client.
- The CallData object itself will be deleted after the packet return is completed.
After it is clear what this CallData object is used to do, let’s look at the entire Server process as follows:
- the Server is started, registered, and a CallData object is created, which is used to prepare for the next Client request.
- the created CallData object will be hosted by gRPC, and when an event comes, the event will be put into the CallData object pair, and then notified with a ServerCompletionQueue object.
- wait for the Client request to come…
- unpacking the data from the ServerCompletionQueue object when an event comes, turning it into a CallData object to call the Proceed method, then performing business logic processing and re-creating the CallData object to prepare for the next Client request.
- wait for the packet return to the Client to finish.
- Continue to process the event events returned by ServerCompletionQueue and clean up its own CallData object.
Take a look at the code below.
Start the main process
The main flow here will create CallData object and then continuously loop through the events from the cq object, which is a waiting queue and will keep blocking when there are no events coming. When there is an event, the tag will be taken from cq_ and converted into a CallData object to call the Proceed method.
Create CallData & Logical Processing & Completion
From here we can relate to the HandleRpcs method above.
- first the new CallData will call the Proceed method directly, this time going to the first branch of if, then writing itself this to cq_ and reversing the state to PROCESS.
- this time will continue to wait for events in the while loop of the HandleRpcs method.
- when a Client sends a request, it goes to the second if branch of Proceed to process the business logic.
- here first new CallData will be used for the next request;
- then the Client request parameters are obtained from request_ and processed;
- the packet return data is written to reply_ and finally the Finish call is made.
- this time will continue to HandleRpcs method in the while loop waiting for the response to the Client packet end.
- after receiving the reply packet, it will continue to call the if third branch of the Proceed method to delete the current object.
In fact, compared to go’s grpc asynchronous API, I have to say that cpp’s API design is very problematic, at first glance, I don’t know what new CallData is for, why it doesn’t do anything after new, and there is no delete operation, won’t memory overflow? And then into the constructor method to find the logic are in the constructor, such a way of writing code I have only seen here.
There are also a lot of people spit it out the design of this api: https://github.com/grpc/grpc/issues/7352 , the official also promised to modify, but a shake 6 years later also did not improve the meaning.