起因

最近接手一个项目,要把其中的阻塞任务队列,重构成非阻塞。在客户端很少有机会直接处理任务队列。项目完成需要总结经验

阻塞的发生

我这里先说明我遇到的阻塞问题,我这里的阻塞不是多线程访问的阻塞,概念上是任务执行的阻塞。具体是:

  • 任务开始客户端准备数据,通过socket向服务器发送数据。
  • 阻塞等待服务器socket的ack回应。
  • 得到服务器的socket回应完成任务,取出队列的后续任务继续执行。

这样的阻塞队列优点就是:

  • 代码看起来非常简洁且聚集,开始代码对应结束代码。
  • 逻辑上可以保证任务的完成,因为如果没有完成,就会阻塞直到任务的完成。

但是致命的缺点也是阻塞等待,因为直接的socket通信使用是不保证送达,如果服务器一直没有回应,客户端的任务队列就一直阻塞在队头。除非通过其他方式强制终止任务队列。

非阻塞的队列

确定了问题的发生的原因,就可以一步步的解决问题。 首先阻塞就是因为在等待回应,只有回应后才能完成任务。任务以本地客户端开启,以服务器回应结束,期间阻塞。构成一个任务的概念。

拆任务

其实客户端不必执着等待回应,只要把任务拆分成

  1. 发送任务
  2. 回应任务

而期间不再阻塞,只要回应任务能够找到对应的发送任务,客户端就可以确定该任务的完成。

HandlerThread实现任务处理队列

这里socket的通信肯定是发生在子线程的,而子线程想要维护任务处理队列,最好的方式就是直接使用HandlerThread,它封装在子线程中Handler的配置,而Handler本身就是的任务处理队列。

package com.example.licola.myandroiddemo.java;import android.os.Handler;
import android.os.HandlerThread;
import java.util.HashSet;/*** Created by LiCola on 2018/4/10.* 简化版非阻塞任务队列*/
public class Dispatcher {private static final String THREAD_NAME="dispatcher-worker";private Handler mHandler;private HandlerThread handlerThread;private HashSet<String> tasks = new HashSet<>();//任务集合public void run(){handlerThread = new HandlerThread(THREAD_NAME);handlerThread.start();mHandler = new Handler(handlerThread.getLooper());}public void postSendTask(String id,String data){mHandler.post(new Runnable() {@Overridepublic void run() {//发送任务的操作 如准备数据等tasks.add(id);}});}public void postAckTask(final String id){mHandler.post(new Runnable() {@Overridepublic void run() {//回应任务的操作 如解析回应等tasks.remove(id);}});}
}复制代码

上面的代码已经非常简化,不涉及具体的任务处理,只有关键代码。实现了前文的拆任务的理念。

但是拆任务也带来了一个很严重的问题,任务怎样保证完成。因为不阻塞,发送任务只管发送,发送完成迎来的可能是下一个发送任务,而对应的回应任务却一直没有到来。概念上这个任务始终没有完成。代码上就是tasks堆积越来越多等待回应的任务。

超时机制

为了应对可能堆积的tasks任务集合,就需要引入超时机制,就是给一个任务设定最长等待时间,如果超过这个时间还没有完成就重试。有了前面的代码基础加入超时检测处理是很容易的。

  • 首先想到的就是在运行过程中加入定期循环执行的检测代码。
  • 给发送任务加入时间变量,用于检测超时。
  • 任务集合保存任务id和对应的发送数据,用于重试。

超时重试机制的任务队列

package com.example.licola.myandroiddemo.java;import android.os.Handler;
import android.os.HandlerThread;
import android.util.Pair;
import com.example.licola.myandroiddemo.utils.Logger;
import java.util.HashMap;
import java.util.Map.Entry;/*** Created by LiCola on 2018/4/10.* 支持超时重试机制版非阻塞任务队列*/
public class Dispatcher {private static final String THREAD_NAME = "dispatcher-worker";//超时检测时间private static final long CHECK_ACK_TIME_OUT = 10 * 1000;//任务限定等待时间,即任务超时时间private static final long ACK_TIME_OUT = 4 * 1000;private Handler mHandler;private HandlerThread handlerThread;private HashMap<String, Pair<Long, String>> tasks=new HashMap<>();//任务集合public void run() {handlerThread = new HandlerThread(THREAD_NAME);handlerThread.start();mHandler = new Handler(handlerThread.getLooper());//开启循环检测mHandler.postDelayed(checkTimeOutTask(), CHECK_ACK_TIME_OUT);}public void postSendTask(final String id, final String data) {mHandler.post(new Runnable() {@Overridepublic void run() {//发送任务的操作 如准备数据等Logger.d("开始发送任务");tasks.put(id, new Pair<>(System.currentTimeMillis(), data));}});}public void postAckTask(final String id) {mHandler.post(new Runnable() {@Overridepublic void run() {//回应任务的操作 如解析回应等Logger.d("开始回应任务");tasks.remove(id);}});}public Runnable checkTimeOutTask() {return new Runnable() {@Overridepublic void run() {int count = 0;long curTime = System.currentTimeMillis();if (!tasks.isEmpty()) {for (Entry<String, Pair<Long, String>> entry : tasks.entrySet()) {String id = entry.getKey();Pair<Long, String> pair = entry.getValue();Long time = pair.first;String data = pair.second;if (curTime - time >= ACK_TIME_OUT) {postSendTask(id, data);count++;}}}if (count > 0) {Logger.d(String.format("检测到超时任务%d", count));}//循环检测mHandler.postDelayed(checkTimeOutTask(), CHECK_ACK_TIME_OUT);}};}
}复制代码

上面的代码已经实现超时重试机制。仔细想想这段代码的运行情况。还是问题和有优化空间的。

检测时机

仔细想想定期检测的时间和限定的超时时间,两者的关系。

  //超时检测时间private static final long CHECK_ACK_TIME_OUT = 10 * 1000;//任务限定等待时间,即任务超时时间private static final long ACK_TIME_OUT = 4 * 1000;
复制代码

为了检测尽可能的高效,且不影响整个任务队列处理性能。让检测时间间隔比较大,且大于任务超时时间。 实际的运行情况很可能如下图所示:

我们以时间点check为基准分析:

  1. check-1时间点之前:开始任务task-1、task-2。
  2. check-1时间点:检测开始,发现任务集合中有2个等待任务,但是它们都没有超时,没有任何处理。
  3. check-2时间点之前:task-1正常完成,任务集合中删除它。
  4. check-2时间定:检测开始,发现任务集合中有1个等待任务,且已经超时。task-2任务重试,task-2的计时重置到当前时间点。

这是一种假设运行情况,但是还是暴露出了两个问题:

  • 不够高效:虽然检测时间间隔足够大,一个间隔内能够完成整个发送回应的正常任务,但是检测并没有很高效,还是在check-1时间点中观察到了两个不应该被观察到的任务。其中task-1:它刚开始且可以正常完成的。
  • 不精确的超时:在check-2之前任务task-2它已经超时了,但是在超时一段时间后才发现。

这两个问题其实不严重,根据实际情况选择。 如果任务的超时小概率发生,且不要求精确的超时检测。超时重试机制的任务处理队列-非精确控制时间,还是足够满足开发需求的。

精确的控制超时时间

怎样做到精确的控制超时时间,且让检测更高效。在Android开发中有没有遇到精确控制任务时间的情况,而其他工程师们怎样实现高效处理的。虽然我们日常开发中没有感知,但是这个情况其实非常非常的普遍存在。把这个问题换个角度:

怎样精确的控制任务时间?

再想想你开发的各种系统处理:

  • 长按点击事件的监听
  • ANR(Application Not Response)的检测和发生

这两个系统处理本质上就是精确控制任务时间的处理。

源码的智慧

确定了上面这两个源码目标,我们来看看系统是怎样实现的。

长按点击事件

一个点击的事件序列由ACTION_DOWN开始,后续的事件action不确定。

开始:

任务的开始就是在View.onTouchEvent(MotionEvent event)的action事件处理cast:MotionEvent.ACTION_DOWN中的方法checkForLongClick(0, x, y) 核心代码就一行:

private void checkForLongClick(int delayOffset, float x, float y) {if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {//发送延迟任务postDelayed(mPendingCheckForLongPress,ViewConfiguration.getLongPressTimeout() - delayOffset);}}
复制代码

结束:

点击任务处理已经开始,而典型点击任务结束就是ACTION_UP事件,同样在代码中cast:MotionEvent.ACTION_UP中的方法removeLongPressCallback()

    private void removeLongPressCallback() {if (mPendingCheckForLongPress != null) {removeCallbacks(mPendingCheckForLongPress);}}
复制代码

超时:

因为在开始就已经确定固定时间点后执行超时处理,在这个时间点之前没有其他action操作来及时remove掉超时处理。从而超时处理得到执行,具体就是执行长按事件。

private final class CheckForLongPress implements Runnable {@Overridepublic void run() {if ((mOriginalPressedState == isPressed()) && (mParent != null)&& mOriginalWindowAttachCount == mWindowAttachCount) {if (performLongClick(mX, mY)) {mHasPerformedLongPress = true;}}}}
复制代码

ANR的检查与发生

总所周知ANR的发生有很多种,这里就挑Service的创建超时来举例说明

Service Timeout:比如前台服务在20s内未执行完成。

这里参考理解Android ANR的触发原理的分析流程。作者很形象的总结整个ANR检测的理念:

埋炸弹-拆炸弹

因为ANR的处理比较复杂,我们省略自动写日志和进程通信等流程。

开始:埋炸弹

ActiveServices源码部分

private final void realStartServiceLocked(ServiceRecord r, ProcessRecord app, boolean execInFg) throws RemoteException {...//发送delay消息(SERVICE_TIMEOUT_MSG)bumpServiceExecutingLocked(r, execInFg, "create");try {...//最终执行服务的onCreate()方法app.thread.scheduleCreateService(r, r.serviceInfo,mAm.compatibilityInfoForPackageLocked(r.serviceInfo.applicationInfo),app.repProcState);} catch (DeadObjectException e) {mAm.appDiedLocked(app);throw e;} finally {...}
}
复制代码
private final void bumpServiceExecutingLocked(ServiceRecord r, boolean fg, String why) {... scheduleServiceTimeoutLocked(r.app);
}void scheduleServiceTimeoutLocked(ProcessRecord proc) {if (proc.executingServices.size() == 0 || proc.thread == null) {return;}long now = SystemClock.uptimeMillis();Message msg = mAm.mHandler.obtainMessage(ActivityManagerService.SERVICE_TIMEOUT_MSG);msg.obj = proc;//当超时后仍没有remove该SERVICE_TIMEOUT_MSG消息,则执行service Timeout流程mAm.mHandler.sendMessageAtTime(msg,proc.execServicesFg ? (now+SERVICE_TIMEOUT) : (now+ SERVICE_BACKGROUND_TIMEOUT));
}
复制代码

结束:拆炸弹

在Service的启动前,已经埋下了炸弹,那就在启动完成后拆掉炸弹。 ActiveServices源码部分

private void serviceDoneExecutingLocked(ServiceRecord r, boolean inDestroying, boolean finishing) {...if (r.executeNesting <= 0) {if (r.app != null) {r.app.execServicesFg = false;r.app.executingServices.remove(r);if (r.app.executingServices.size() == 0) {//当前服务所在进程中没有正在执行的servicemAm.mHandler.removeMessages(ActivityManagerService.SERVICE_TIMEOUT_MSG, r.app);...}...
}
复制代码

超时:炸弹爆炸

如果Service没有限定时间内完成启动,拆掉炸弹,炸弹就会爆炸,就是超时任务执行。 就是ActiveService的serviceTimeout方法执行,写下日志发出ANR弹框。

总结

我们从精确控制任务超时时间这角度,分析了长按事件和ANR的发生原理。最终发现他们都是基于同样的设计方式:埋炸弹-拆炸弹 在任务开始时设置定时任务,及时完成remove掉定时任务,否则任务超时就会执行超时处理,而定时任务精确的时间执行就保证了超时任务精确控制。这个方式完全不同于我前文实现的间隔检测-非精确时间控制。

超时重试机制的任务队列-精确控制时间

有对源码的理解和总结,稍微修改代码就可以得到如下

package com.example.licola.myandroiddemo.java;import android.os.Handler;
import android.os.HandlerThread;
import com.example.licola.myandroiddemo.utils.Logger;
import java.util.HashMap;/*** Created by LiCola on 2018/4/10.* 支持超时重试机制版非阻塞任务队列*/
public class DispatcherTime {private static final String THREAD_NAME = "dispatcher-worker";//任务限定等待时间,即任务超时时间private static final long ACK_TIME_OUT = 2 * 1000;private Handler mHandler;private HandlerThread handlerThread;private HashMap<String, Runnable> timeoutTask = new HashMap<>();//超时集合public void run() {handlerThread = new HandlerThread(THREAD_NAME);handlerThread.start();mHandler = new Handler(handlerThread.getLooper());}public void postSendTask(final String id, final String data) {mHandler.post(new Runnable() {@Overridepublic void run() {//发送任务的操作 如准备数据等Logger.d("开始发送任务",data);Runnable checkTimeOutTask = checkTimeOutTask(id, data);timeoutTask.put(id, checkTimeOutTask);mHandler.postDelayed(checkTimeOutTask,ACK_TIME_OUT);}});}public void postAckTask(final String id) {mHandler.post(new Runnable() {@Overridepublic void run() {//回应任务的操作 如解析回应等Logger.d("开始回应任务",id);Runnable runnable = timeoutTask.remove(id);mHandler.removeCallbacks(runnable);}});}public Runnable checkTimeOutTask(final String id, final String data) {return new Runnable() {@Overridepublic void run() {Logger.d("超时任务执行 ",id,data);postSendTask(id, data);}};}
}
复制代码

上面实现了每次任务发送都会埋下一个延迟任务,如果没有及时得到回应就会重试。 这个实现的缺点如果要说的就是:

  • 每个发送任务都会创建一个对应的延迟任务,如果发送任务数量较大,且只有小概率任务超时,就会产生大量创建的任务而又短期存在且没有机会执行的任务。

当然如果要优化就是使用Handler.handleMessage(Message msg)方法处理超时任务,而不是每次postDelayed都创建Runnable对象。这里只留下思路就不用代码了。

总结

  • 其实源码的理解不是很难,只要找到切入点,从你关心的点出发,就能够理解源码并应用它。我们从超时任务的处理为切入点就很容易理解长按事件的原理和ANR的发生机制。
  • 当我们了解到一个新的解决方案,不要急于去应用它,要分析新方案的利弊,和我们实际项目的匹配程度,才能很好的应用它和改造它。

参考

  • 理解Android ANR的触发原理

怎样实现一个非阻塞的超时重试任务队列相关推荐

  1. 阻塞、非阻塞、超时(同步与异步)

    今天看RXW文档,看到这个概念,不知啥意思... 1.阻塞: 阻塞==同步->例如:发送消息 直到 发送成功 才能发送下一条消息 接收消息 这条消息接收完毕 才能接收下条消息 2.非阻塞 非阻塞 ...

  2. 【基础】利用thrift实现一个非阻塞带有回调机制的客户端

    假设读者对thrift有一定了解. 客户端有时需要非阻塞的去发送请求,给定服务端一个请求,要求其返回一个计算结果.但是客户端不想等待服务端处理完,而是想发送完这个指令后自己去做其他事情,当结果返回时自 ...

  3. C++ TCP socket 非阻塞连接超时设定方式

    由于网上大部分教程为阻塞方式连接,当项目需要大量连接从机的情况下,阻塞式连接socket会导致长时间卡顿.因此需要用非阻塞式的方式连接,并设置超时是时间,再设置会阻塞.下属C++编辑代码实测可用,代码 ...

  4. 【linux】串口编程(二)——非阻塞接收

    项目中很少会使用阻塞接收,一般都是select+read监听模式来实现非阻塞接收. 使用selece时,需要处理一些异常情况的返回,比如:系统中断产生EINTR错误:超时错误ETIMEDOUT. 使用 ...

  5. Java 理论与实践: 非阻塞算法简介——看吧,没有锁定!(转载)

    简介: Java™ 5.0 第一次让使用 Java 语言开发非阻塞算法成为可能,java.util.concurrent 包充分地利用了这个功能.非阻塞算法属于并发算法,它们可以安全地派生它们的线程, ...

  6. Java 理论与实践: 非阻塞算法简介

    在不只一个线程访问一个互斥的变量时,所有线程都必须使用同步,否则就可能会发生一些非常糟糕的事情.Java 语言中主要的同步手段就是 synchronized 关键字(也称为内在锁),它强制实行互斥,确 ...

  7. iphone开发之轻松搞定原生socket 编程,阻塞与非阻塞,收发自如

    iphone socket 开发 在iphone的平台下,要进行socket开发其实有很多种的方法,开源的库Asyncsocket,官方的CFSocket,还有BSD的socket. 这里要做一个简单 ...

  8. Java 理论与实践: 非阻塞算法简介--转载

    在不只一个线程访问一个互斥的变量时,所有线程都必须使用同步,否则就可能会发生一些非常糟糕的事情.Java 语言中主要的同步手段就是synchronized 关键字(也称为内在锁),它强制实行互斥,确保 ...

  9. 200行自定义异步非阻塞Web框架

    Python的Web框架中Tornado以异步非阻塞而闻名.本篇将使用200行代码完成一个微型异步非阻塞Web框架:Snow. 一.源码 本文基于非阻塞的Socket以及IO多路复用从而实现异步非阻塞 ...

最新文章

  1. 会话保持之iRule脚本
  2. python中json模块读写数据
  3. 如何利用Excel计算有多少种组合?
  4. @EqualsAndHashCode()注解详解
  5. 【DOS】dos命令大全
  6. 【人工智能】【深度学习】初学者如何选出最适合自己深度学习框架?
  7. Vue默认插槽、具名插槽、作用域插槽及使用作用域插槽删除列表项
  8. javafx项目_爬虫系列(5):JavaFx界面
  9. android自定义sufaceview,Android自定义SurfaceView实现画板功能
  10. 【Zotero同步管理】【Zotero 6.0 + 坚果云 + iPad官方zotero APP】最新版教程
  11. CC2500的CCA
  12. mysql广告投放查询_广告投放数据分析
  13. Unity—背包系统(思路总括)
  14. 在 Windows 中保存和恢复桌面图标布局
  15. 永洪科技CEO何春涛:PASO模型构建企业大数据能力
  16. ucsd计算机排名,加州大学圣地亚哥分校专业排名一览及最强专业推荐(QS世界大学排名)...
  17. 利用ChatGPT学习生物信息数据分析
  18. python正则表达式介绍
  19. 刘慈欣回应《流浪地球》热点问题:承认有些设定有bug...
  20. vscode html tab键补全插件_打造舒适的 VS Code 开发环境

热门文章

  1. mvp模式 php,Hyper-V - 增强会话模式
  2. python必须下载到c盘吗_python为什么要安装到c盘
  3. php mysql百万级数据_PHP+MySQL百万级数据插入的优化
  4. python123第6周答案_python123 测验6: 组合数据类型 (第6周)
  5. java 消费者 生产者 队列_用Java写一个生产者-消费者队列
  6. java查找字符串是否有单词_java – 用于搜索单词/字符串是否包含在实...
  7. 营业税计提及企业所得税的相关计算公式
  8. Java案例:Java版生命游戏
  9. Java实训项目2:GUI学生信息管理系统 - 系统概述
  10. Java讲课笔记25:缓冲流、字符流与转换流