OkHttp Knowledge Carding (2) - Asynchronous Request for Source Parsing of OkHttp-Thread Scheduling

Keywords: OkHttp network less Android

OkHttp Knowledge Carding Series

OkHttp Knowledge Carding (1) - Introduction to OkHttp Source Parsing
OkHttp Knowledge Carding (2) - Asynchronous Request for Source Parsing of OkHttp-Thread Scheduling

I. Preface

stay OkHttp Knowledge Carding (1) - Introduction to OkHttp Source Parsing In this paper, we will study the internal implementation principle of asynchronous request and thread scheduling together.

First, let's review how asynchronous requests are implemented:

    private void startAsyncRequest() {
        //The following three steps are the same as synchronizing requests.
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder().url(URL).build();
        Call call = client.newCall(request);
        //The difference is the way you handle RealCall objects.
        call.enqueue(new Callback() {

            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                String result = response.body().string();
                //Returns the result to the main thread.
                Message message = mMainHandler.obtainMessage(MSG_UPDATE_UI, result);
                mMainHandler.sendMessage(message);
            }
        });
    }

As you can see, for asynchronous requests, the first three synchronous requests are the same. The difference is that when a request is initiated, the synchronous request uses call.execute(), which returns from the. execute() function only when the entire request is completed.

For asynchronous requests, the. enqueue(Callback) method returns as soon as it is invoked. When the network request returns, the onResponse/onFailure method of Callback is called back, and the two callback methods are executed in the sub-thread, which is the main difference between asynchronous requests and synchronous requests.

Now let's analyze the internal implementation logic of asynchronous requests.

2. Source code parsing for asynchronous requests

For the first three internal implementations, we will not repeat the instructions, you can see OkHttp Knowledge Carding (1) - Introduction to OkHttp Source Parsing In the analysis. Eventually we will get an instance of RealCall, which represents an execution task. Next, look at what's going on inside enqueue.

    public void enqueue(Callback responseCallback) {
        //The first step is to determine whether the object has ever been executed.
        synchronized(this) {
            if(this.executed) {
                throw new IllegalStateException("Already Executed");
            }

            this.executed = true;
        }
        //Capture stack information.
        this.captureCallStackTrace();
        //Notify the listener that the request has started.
        this.eventListener.callStart(this);
        //Call the enqueue method of the scheduler.
        this.client.dispatcher().enqueue(new RealCall.AsyncCall(responseCallback));
    }

Here we see the familiar dispatcher() class, which enqueue implements as follows:

    private int maxRequests = 64;
    private int maxRequestsPerHost = 5;
    
    //Thread pool for task execution.
    private ExecutorService executorService;

    //Waiting for an asynchronous request task queue to be executed.
    private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque();
    //Asynchronous request task queue being executed.
    private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque();
    
    public synchronized ExecutorService executorService() {
        if(this.executorService == null) {
            this.executorService = new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue(), Util.threadFactory("OkHttp Dispatcher", false));
        }
        return this.executorService;
    }

    synchronized void enqueue(AsyncCall call) {
        //If the current number of requests is less than 64 and the number of requests for the same host is less than 5, the request is initiated.
        if(this.runningAsyncCalls.size() < this.maxRequests && this.runningCallsForHost(call) < this.maxRequestsPerHost) {
            //Add the task to the queue being requested.
            this.runningAsyncCalls.add(call); 
            //Tasks are performed through thread pools.
            this.executorService().execute(call);
        //Otherwise, join the waiting queue.
        } else {
            this.readyAsyncCalls.add(call);
        }
    }

Dispatcher's enqueue first judges that if the current number of requests is less than 64 and the number of requests for the same host is less than 5, the request is initiated. RealCall is added to the running AsyncCalls queue before the request is initiated and executed through ThreadPool Executor.

2.1 Executing tasks through thread pools

ThreadPool Executor is a thread pool provided by Java, in which Multithread Knowledge Carding (6) - ThreadPool Executor of Thread Pool Quartet We have already introduced it. According to its parameter configuration, it corresponds to CachedThreadPool, which is characterized by unbounded thread pool size and is suitable for programs that perform many short-term asynchronous tasks or servers with lighter load.


Its concrete realization way is:

  • SynchonousQueue is used for waiting queues, and each insertion must wait for the removal of another thread. For thread pools, that is to say, when adding tasks to waiting queues, there must be an idle thread trying to get tasks from waiting queues before adding them successfully.
  • Therefore, when a task is added to the thread pool, there are two situations:
    • If an idle thread is currently trying to retrieve a task from a waiting queue, the task will be handed over to the idle thread for processing.
    • If there are currently no idle threads trying to retrieve tasks from the waiting queue, a new thread will be created to perform the task.
  • Because of the set waiting time-out, a thread can not get new tasks within 60 seconds and will be destroyed.

The execute function of the thread pool receives Runnable's interface implementation class as a parameter. When the task is executed, its run() method will be called. The same is true for AsyncCall above. It inherits the abstract class of NamedRunnable, and NamedRunnable implements the Runnable interface. When NamedRunnable's run() method is called back, AsyncCall's exee () method will be called.

final class AsyncCall extends NamedRunnable {
    private final Callback responseCallback;

    //The responseCallback is the callback passed in when the call.enqueue method is called.
    AsyncCall(Callback responseCallback) {
        super("OkHttp %s", new Object[]{RealCall.this.redactedUrl()});
        this.responseCallback = responseCallback;
    }
        
    //This function is executed in a sub-thread.
    protected void execute() {
        boolean signalledCallback = false;

        try {
            //The same logic as synchronous requests.
            Response response = RealCall.this.getResponseWithInterceptorChain();
            if(RealCall.this.retryAndFollowUpInterceptor.isCanceled()) {
                signalledCallback = true;
                this.responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
            } else {
                signalledCallback = true;
                this.responseCallback.onResponse(RealCall.this, response);
            }
        } catch (IOException var6) {
            if(signalledCallback) {
                Platform.get().log(4, "Callback failure for " + RealCall.this.toLoggableString(), var6);
            } else {
                RealCall.this.eventListener.callFailed(RealCall.this, var6);
                this.responseCallback.onFailure(RealCall.this, var6);
            }
        } finally {
                        //Call the finished method of Dispatcher
            RealCall.this.client.dispatcher().finished(this);
        }

    }
}

public abstract class NamedRunnable implements Runnable {
    protected final String name;

    public NamedRunnable(String format, Object... args) {
        this.name = Util.format(format, args);
    }

    public final void run() {
        String oldName = Thread.currentThread().getName();
        Thread.currentThread().setName(this.name);

        try {
            //Call the execute() method of the subclass.
            this.execute();
        } finally {
            Thread.currentThread().setName(oldName);
        }

    }
    protected abstract void execute();
}

The execute() method is ultimately executed in sub-threads, and here we see a familiar sentence:

Response response = RealCall.this.getResponseWithInterceptorChain();

Here's the core logic of making requests. We're OkHttp Knowledge Carding (1) - Introduction to OkHttp Source Parsing As described in Section 3.4, retry requests, cache processing, and network requests are processed through a series of interceptors. Finally, the returned Response is obtained, and the onResponse/onFailure method of the initial Callback is called back according to the situation.

2.2 Processing after Task Execution

When the callback is complete, Dispatcher's finished method is finally called:

    void finished(AsyncCall call) {
        //If it is an asynchronous request, the last parameter is true.
        this.finished(this.runningAsyncCalls, call, true);
    }

    void finished(RealCall call) {
        //If the request is synchronous, the last parameter is false.
        this.finished(this.runningSyncCalls, call, false);
    }

    private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
        int runningCallsCount;
        Runnable idleCallback;
        synchronized(this) {
            //Remove it from the list of tasks currently being performed.
            if (!calls.remove(call)) {
                throw new AssertionError("Call wasn't in-flight!");
            }
            //Look for tasks that meet the criteria in the waiting queue to execute.
            if (promoteCalls) {
                this.promoteCalls();
            }
            runningCallsCount = this.runningCallsCount();
            idleCallback = this.idleCallback;
        }
        if (runningCallsCount == 0 && idleCallback != null) {
            idleCallback.run();
        }
    }

    private void promoteCalls() {
        if (this.runningAsyncCalls.size() < this.maxRequests) {
            if (!this.readyAsyncCalls.isEmpty()) {
                Iterator i = this.readyAsyncCalls.iterator();
                do {
                    if(!i.hasNext()) {
                        return;
                    }
                    AsyncCall call = (AsyncCall)i.next();
                    if (this.runningCallsForHost(call) < this.maxRequestsPerHost) {
                        i.remove();
                        //Find a task that meets the execution criteria in the waiting queue, and then execute it.
                        this.runningAsyncCalls.add(call);
                        this.executorService().execute(call);
                    }
                } while(this.runningAsyncCalls.size() < this.maxRequests);

            }
        }
    }

Here, the same as synchronized requests, will go to the finished method. The difference is that the last parameter is true and the synchronized request is false. That is to say, it will be called to the promoteCalls method. The function of promoteCalls is: at the beginning, if the execution condition is not satisfied, then the task will be added to the waiting queue readyAsyncCalls, then when a task is completed. After that, you need to wait for the task in the queue to find the qualified task and add it to the task queue to execute, the logic is the same as before.

The promoteCalls function is called after an asynchronous request has been executed. It also triggers the lookup process when we change the maximum number of requests and the maximum number of requests for the same host.

    //Changed the maximum number of requests.
    public synchronized void setMaxRequests(int maxRequests) {
        if(maxRequests < 1) {
            throw new IllegalArgumentException("max < 1: " + maxRequests);
        } else {
            this.maxRequests = maxRequests;
            this.promoteCalls();
        }
    }
    //Changed the maximum number of requests for the same Host.
    public synchronized void setMaxRequestsPerHost(int maxRequestsPerHost) {
        if(maxRequestsPerHost < 1) {
            throw new IllegalArgumentException("max < 1: " + maxRequestsPerHost);
        } else {
            this.maxRequestsPerHost = maxRequestsPerHost;
            this.promoteCalls();
        }
    }

Three, summary

The above is the source code analysis of asynchronous requests, from which we can summarize OkHttp's scheduling method for asynchronous requests:

  • Asynchronous request tasks are managed through two lists. Running AsyncCalls stores a list of tasks being executed, while readyAsyncCalls stores tasks waiting to be executed.
  • When a task is completed, it goes to readyAsyncCalls to find the next task that can be executed.
  • Task execution is accomplished through thread pool ThreadPool Executor in sub-threads, which is responsible for task scheduling. The internal implementation principles are as follows. Multithread Knowledge Carding (6) - ThreadPool Executor of Thread Pool Quartet Analyzed.
  • The core code for real network requests is in the execute() function of AsyncCall, where a series of interceptors are used to process the retry requests, cache processing and network requests. Finally, the returned Response is obtained, and the onResponse/onFailure method of the initial Callback is called back according to the situation.
  • For asynchronous requests, Callback's onResponse/onFailed is executed in a sub-thread, so if you want to update the UI in it, you need to notify the main thread to update it.

For more articles, please visit my Android Knowledge Carding Series:

Posted by kristo5747 on Sun, 19 May 2019 16:34:18 -0700