草庐IT

Android Timer(定时器)踩坑记

Magic旭 2023-03-28 原文

背景

由于网络需求需要通过发心跳来维持连接的建立,所以客户端需要通过计时器,每间隔一定事件发一次心跳请求到服务器,以此达到连接保活。我用了Timer来进行定时任务后,服务端童鞋找我说为啥同一秒会有重复的心跳请求发到服务器上呢?这就延伸出我们今天文章所要讲的内容了。

问题

业务场景是每隔10秒上报一次ping心跳,当09:50:33时候Timer执行了一次ping的上报任务后,下一次的上报的时间却是在09:50:54进行ping上报了(此次ping上报出现重复上报问题),中间间隔20几秒,在排查并非代码逻辑问题,把目光投向了定时器自身问题。

日志心跳某一秒内重复无用心跳

分析问题

结合自身日志和Timer的源码阅读,可以知道此问题是由于使用Timer进行定时任务上报,当你的app的cpu资源竞争非常激烈时候,你的Timer里面的Thread没有办法准时获取cpu资源来执行开发者需要做的定时任务,当获取到cpu资源时,Timer就会为了弥补之前漏执行的定时任务,会在同一时刻进行1-n次的定时任务。

前置知识

刚入门面试的我们,多多少少都会被面试官问到sleep和wait的区别,当初的我们涉世尚浅,并不是太多关注这两个的区别,以为并没有什么用处,但看完我这篇文章你就明白当初面试官为什么问你这个问题了。这里先大概讲下,wait是让当前线程让出系统资源,释放锁,处于线程队列中进行等待;sleep是不让出系统资源,当前线程挂起一定时间,不释放锁。Timer里面源码的实现就是用了wait实现。

源码解析
  1. 首先是从Timer的schedule函数开始看起来,大家对于这三个参数应该都有一定的认识,我这里就不展开细讲了。主要看的是scheduleAtFixedRate函数里的sched调用。注意sched第二个参数是当前系统时间+开发者所需的delay时间。

Timer().scheduleAtFixedRate(object : TimerTask() {
            override fun run() {
                ……
            }
        }, delayMills, periodMills)


public void scheduleAtFixedRate(TimerTask task, long delay, long period) {
        ……
        sched(task, System.currentTimeMillis()+delay, period);
 }
  1. sched方法主要是把Timer的启动时间和间隔存储到Task对象里,再把Task对象加到队列里,看完了Timer的构造,我们下面看下Timer是如何运行。
private void sched(TimerTask task, long time, long period) {
       ……
        synchronized(queue) {
          ……
            synchronized(task.lock) {
                if (task.state != TimerTask.VIRGIN)
                    throw new IllegalStateException(
                        "Task already scheduled or cancelled");
                task.nextExecutionTime = time;
                task.period = period;
                task.state = TimerTask.SCHEDULED;
            }

            queue.add(task);
            if (queue.getMin() == task)
                queue.notify();
        }
    }
  1. Timer内部有个TimerThread线程,Run内部实现为一个死循环,通过wait/wait(time)/notify 实现挂起/唤醒操作。在mainLoop里面有个逻辑缺陷就是,每次当前线程获取cpu资源时候,就会判断队列头部的Task是否到时间执行。如果未到时间,则wait剩余时间;如果到时间执行,则更新Task的下一次执行的时间(nextExecutionTime)。

注意:那么问题就出现了,假如你的定时器任务执行完后,wait了下一次间隔时间,但是那个时间段cpu资源竞争很激烈,TimerThread根本抢不到cpu资源去执行,当到达下下一次间隔时间获取到cpu的资源时候,你的死循环就因为currentTime - executionTime >= 2倍的间隔时间,所以会同一时刻执行两个Runnable的回调,自然你Runnable回调也会在同一时刻做出重复的行为。

class TimerThread extends Thread {
  public void run() {
        ……
        mainLoop();
        ……
    }
}

private void mainLoop() {
        while (true) {
            try {
                TimerTask task;
                boolean taskFired;
                synchronized(queue) {
                    // 当Task队列为空时候,挂起系统资源,等待notify的唤醒
                    while (queue.isEmpty() && newTasksMayBeScheduled)
                        queue.wait();
                    ……
                    // 从队列中取出头部Task
                    task = queue.getMin();
                    synchronized(task.lock) {
                       ……
                        currentTime = System.currentTimeMillis();
                        //Task的执行sched函数时的系统时间
                        executionTime = task.nextExecutionTime;
                        //taskFired:true 执行时间到了,false 执行时间未到
                        if (taskFired = (executionTime<=currentTime)) {
                            if (task.period == 0) { // Non-repeating, remove
                                queue.removeMin();
                                task.state = TimerTask.EXECUTED;
                            } else { // Repeating task, reschedule
                                //更新头部Task的nextExecutionTime时间
                                queue.rescheduleMin(
                                  task.period<0 ? currentTime   - task.period
                                                : executionTime + task.period);
                            }
                        }
                    }
                    if (!taskFired) // 任务还没有到时执行,挂起剩余的时间
                        queue.wait(executionTime - currentTime);
                }
                if (taskFired)  // 任务到时执行,回调Runnable
                    task.run();
            } catch(InterruptedException e) {
            }
        }
    }

总结

  1. Timer的设计者也考虑到多报的情况,所以设计了如果你传进来的period为负数,就用当前系统时间+你的period间隔时间,从而选择漏报而不是多报一次,但是好像还有bug,所以外面的schedulexxx只要period为负数就会抛异常。

  2. 所有跑线程的任务都会有资源竞争的问题,如果想要解决此类问题,应该规划线程优先级,业务的优先级最多到哪个等级,上报、crash等线程优先级比业务等级高。只有明确线程等级,才能保证你的线程能按时获取cpu资源执行任务。

  3. 一起努力搬砖?

有关Android Timer(定时器)踩坑记的更多相关文章

  1. ruby - Ruby 性能中的计时器 - 2

    我正在寻找一个用ruby​​演示计时器的在线示例,并发现了下面的代码。它按预期工作,但这个简单的程序使用30Mo内存(如Windows任务管理器中所示)和太多CPU有意义吗?非常感谢deftime_blockstart_time=Time.nowThread.new{yield}Time.now-start_timeenddefrepeat_every(seconds)whiletruedotime_spent=time_block{yield}#Tohandle-vesleepinteravalsleep(seconds-time_spent)iftime_spent

  2. springboot定时任务 - 2

    如果您希望在Spring中启用定时任务功能,则需要在主类上添加 @EnableScheduling 注解。这样Spring才会扫描 @Scheduled 注解并执行定时任务。在大多数情况下,只需要在主类上添加 @EnableScheduling 注解即可,不需要在Service层或其他类中再次添加。以下是一个示例,演示如何在SpringBoot中启用定时任务功能:@SpringBootApplication@EnableSchedulingpublicclassApplication{publicstaticvoidmain(String[]args){SpringApplication.ru

  3. C#初级_定时器 - 2

    文章目录一、引言二、Timers1.System.Threading.Timer1.1.简单使用1.2.注意点2.System.Timers.Timer2.1.概述🔺2.2.注意点三、总结一、引言在开发中,会遇到并行处理的需求。有时只需要使用task(底层是创建个线程)来处理一下就好了。而有时则在并行处理的基础上还有时间的要求,较常见的就是每隔一定时间处理一次。当然,这用task肯定可以实现,但是时间这块得自己控制,无疑增加了工作量和不确定性。.NET提供了叫做定时器(timer,也叫计时器)的类,它在并行处理的基础上,带了时间参数的设置,可以满足这一需求。其实本文标题与其叫C#定时器,不如叫

  4. mysql - 在为 RoR 应用程序在 MySQL 和 Amazon 的 SimpleDB 之间做出决定时,您需要考虑什么? - 2

    我刚刚开始研究使用Amazon的SimpleDB服务作为我计划构建的RoR应用程序的数据存储的可行性。我们将为Web服务器使用EC2,并计划将EC2用于MySQL服务器。但现在的问题是,为什么不使用SimpleDB?应用程序(如果成功)需要在支持的用户数量方面具有很强的可扩展性,需要维护简单高效的代码库,并且需要可靠。我很好奇SO社区对此有何看法。 最佳答案 RubySimpleDB库不如ActiveRecord(默认的RailsDB适配器)那么完整,因此您习惯的许多功能将不存在。从好的方面来说,它是无模式的、可扩展的并且可以很好地

  5. camille mumu 模拟器 frida 踩坑记录 - 2

    1.了解监管机构已经卷到需要监控进程了,为了跟上通报步伐查了下资料,打算浅试一下camille,依据原作的文档初步了解到需要python3、adb、frida、模拟器(木木-已成功、夜神)、root手机,开始逐个尝试,记录一下所遇到的情况。 原作祭上:camille/use.mdatmaster·zhengjim/camille·GitHubhttps://www.cnblogs.com/zhengjim/p/15508738.html2.PythonPython38、pip更新电脑中如果有多个python环境的,记得改好名哦,不然会报错,我是配置了环境变量然后让38的置顶pip如果久没用了也

  6. system.threading.Timer每天尝试同时制作计时器 - 2

    我在控制台服务应用中使用system.threading.timer,并尝试每天同时制作计时器。最初,如果我在时间之前启动该应用程序,我会很好。就像我的时间是10:05,我从10:00启动该应用程序,我们很好。但是,如果我从10:06开始,我就不知道如何告诉时间台下24小时。谢谢你的帮助!publicvoidSetUpTimer(TimeSpanalertTime){DateTimecurrent=DateTime.Now;TimeSpantimeToGo=alertTime-current.TimeOfDay;if(timeToGo{EventLog.WriteEntry("MhyApp",

  7. javascript - 重新加载页面时保持计时器 (setInterval) 运行 - 2

    加载网页后,我会通过控制台插入一些Javscript。我想知道我是否有可能使用Javascript或jQuery重新加载页面(而不是从缓存中),同时保持我正在运行的setInterval。我熟悉location.reload(),但这会终止它。 最佳答案 当您重新加载页面时,包括所有正在运行的JS在内的整个页面上下文将被完全破坏。重新加载其主机页面时,您不能保持setInterval()运行。您可以为新页面创建一个信号,以使用cookie、查询参数或本地存储值(查询参数可能是最合适的)再次开始间隔。如果采用这种方式,则需要对页面进行

  8. javascript - ionic 应用程序中的计时器(setInterval)在后台运行一段时间后进入休眠状态 - 2

    我的ionic应用程序有一个计时器(一个简单的setInterval,每秒滴答一次),当应用程序位于前台时,它工作得很好。然而,当应用程序进入后台并在10分钟后返回前台时,应用程序中显示的时间是错误的(时间比应该的少得多)。我试过将计时器添加到指令中并使用nativeDOM操作api(document.getElementById等)方法,但它们都不起作用。我认为当应用程序进入后台时,ionic框架正在对View和绑定(bind)做一些事情。有没有人遇到过这样的问题?如果遇到过,你们是如何解决的? 最佳答案 经过几个小时的寻找答案,

  9. javascript - 在一定时间后终止 Javascript 进程 - 2

    如果这是重复的,我深表歉意。假设我有一个JavaScript函数调用网络服务来提取一些数据。我使用某种移动图形让用户知道它正在工作。成功检索后,我将图形更改为复选标记。这是我的代码:getData:function(){$("#button").attr("disabled","true");varparams={doRefresh:false,method:'/GetData',onSuccess:newthis.getDataCallback(this).callback,onFailure:newthis.getDataFailed(this).callback,args:{te

  10. javascript - 跨多个客户端同步的 Firebase 倒数计时器 - 2

    我将尝试使用AngularJS为特定的利基市场构建一个便士拍卖网站。我正在尝试计划倒数计时器,并且我一直渴望尝试使用firebase。我昨天有一个想法,让每次拍卖都以某种方式在实际数据库中有一个倒计时,因为有了2种方式的数据绑定(bind),人们的客户端将始终保持更新。当firebase发生变化时,所有连接的客户端都会立即发生变化。所以我的问题是……如何在特定记录中进行服务器端倒计时。假设我有一个项目x的记录,它包含所有项目信息,并且数组键之一是“倒计时:59:01:00”。在服务器端从59:01:00到00:00:00倒计时的现实且可扩展的方法是什么。我在想也许是每1秒运行一次的cr

随机推荐