1.UE4源码剖析——异步与并行 中篇 之 Thread
2.ART 深入浅出 - 为何 Thread.getStackTrace() 会崩溃?
3.面试官:Thread启动线程的函数start方法能执行多次吗?
4.剖析Linux内核源码解读之《实现fork研究(二)》
5.一文读懂ThreadLocal的原理及使用场景
6.ThreadPoolExecutor简介&源码解析
UE4源码剖析——异步与并行 中篇 之 Thread
我们知道UE中的异步框架分为TaskGraph与Thread两种,上篇教程我们学习了TaskGraph,源码源码它擅长处理有依赖关系的分析短任务;本篇教程我们将学习Thread,它与TaskGraph相反,函数它更擅长于处理长任务。源码源码而下一篇文章,分析webrtc的源码我们则会承接Thread,函数去学习一下引擎中一些重要的源码源码线程。
Thread擅长处理长任务,分析从长任务生命周期这个层面来看,函数我们可以先把长任务分为两类:常驻型长任务与非常驻型长任务。源码源码
常驻型长任务侧重于并行,分析通常用于监听式服务,函数例如网络传输,源码源码使用单独的分析线程对网络进行监听,每当有网络数据包到达时,线程接收并处理后,不会立即结束,而是重置部分状态,继续监听,等待下一轮数据包。
非常驻型长任务侧重于异步,通常用于数据处理,例如主线程为了提高性能,避免卡顿,会将一些重负载的运算任务分发给分线程处理,可能分批给多条分线程,主线程继续运行其他逻辑。任务处理完成后,将结果返回给主线程,分线程可销毁。
接下来,我们通过两个例子学习Thread的使用。
计算由N到M(N和M为大数字)所有数字的和。使用Thread异步调用,将计算操作交由分线程执行,计算完成后再通知主线程结果,代码实现如下:
逻辑分为两部分:启动分线程计算数字和,使用Async函数,参数为EAsyncExecution::Thread,创建新线程执行。快餐系统 源码学习Async函数用法,该函数返回TFuture对象,代表未来状态,当前无法获取结果,但在未来某个时刻状态变为Ready,此时可通过TFuture获取结果。
主线程注册回调,等待分线程计算完成,使用TFuture的Then函数,完成时触发注册的回调,也可使用Wait系列函数等待计算完成。
接下来学习常驻型任务使用。
定义玩家血量上限点,当前点,当血量未满时,每0.2秒恢复1点血量。代码实现分为创建生命治疗仪FRunnable对象、重写Run函数、创建FRunnableThread线程、测试恢复功能和释放线程资源。
生命治疗仪创建与测试完整代码如下,可验证生命恢复功能和暂停与恢复。
UE4中的FRunnable与FRunnableThread提供创建常驻型任务所需接口。无论是常驻型还是非常驻型,底层实现相同,都是使用FRunnableThread线程。
FRunnableThread线程结构包含标识符、逻辑功能、效率与性能、辅助调试字段。线程创建与生命周期分为创建FRunnable类对象、创建FRunnableThread对象两步,通过FRunnable的生命周期管理实现线程运行与停止。
UE4线程管理流程包括继承并创建FRunnable类对象、创建FRunnableThread对象,生命治疗仪线程创建代码。
UE4中的几种异步方式底层使用线程实现,学习了线程类型、创建、生命周期、baytrail bios源码销毁方法,为下篇学习引擎特殊线程打下基础。
ART 深入浅出 - 为何 Thread.getStackTrace() 会崩溃?
前言
Thread 类的 getStackTrace() 方法是日常开发中常用的工具,特别是用于卡顿检测方案,如周期性调用 Thread.getStackTrace() 或 Thread.getAllStackTraces 获取主线程调用栈。然而,在频繁调用时,有时会引发崩溃现象。崩溃栈显示关键调用链路涉及 VMStack_getThreadStackTrace()、ThreadList::SuspendThreadByPeer()、ThreadSuspendByPeerWarning()、~LogMessage() 和 Runtime::Abort() 等。接下来,我们将逐步分析这一过程及其原因。
Thread.getStackTrace 源码分析
在 ART 源码版本 Android 中,核心调用在于 VMStack.cc 文件的 GetThreadStack 方法。关键步骤已用注释标记。GetThreadStack() 内部逻辑包括挂起线程、调用回调函数生成调用栈以及恢复线程。挂起线程的主要方法是 SuspendThreadByPeer(),该函数包含多步骤,但主要涉及初始化变量、循环检查目标线程状态、设置挂起标志位以及循环判断目标线程是否挂起,直至超时。
关键点之一在于,当超时时调用 ThreadSuspendByPeerWarning() 函数,其内部 LOG 调用会在严重级别为 FATAL 时直接触发 Abort。这就是文章开头提到的崩溃栈的原因。通常,为避免此崩溃,可以使用 ThreadList::SuspendThreadByThreadId() 函数,该函数在超时时仅产生 WARNING 级别的 LOG,并不会终止运行。
超时时间由 thread_suspend_timeout_ns_ 变量决定,此变量在 Runtime 初始化时传入 ThreadList,若未指定,则默认值在 thread_list.h 文件中。默认值为 秒,即时间单位为纳秒。exdui模块源码因此, 秒的默认超时时间是导致问题的原因之一。
另一个关键点涉及 ART 如何实际挂起线程。关键代码是 suspended_thread->ModifySuspendCount(),它设置挂起标志位。该函数的原理已通过注释解释。此外,从检查点的角度出发,Java 中的 Check Point 概念在解释执行和机器码执行过程中起到暂停当前指令执行的作用,从而挂起当前线程。检查点存在于 Java 指令执行过程中的特定位置,如 switch/case 语句。
总结
通过深入分析,我们知道 Java 层的 Thread.getStackTrace() 方法本质上是将目标线程设置为请求挂起的状态,然后循环判断线程是否挂起。这一过程依赖于各个检查点的执行,从而在调用栈生成过程中引发超时。因此,目标线程迟迟未能执行到检查点是 Thread.getStackTrace() 方法超时的根本原因。
面试官:Thread启动线程的start方法能执行多次吗?
在Java中,线程的创建与启动机制是通过Thread类中的start方法来实现的,而非直接调用run方法。这是基于线程状态管理的必要性。线程在其生命周期中会经历NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED等多个状态。start方法的作用是将线程从NEW状态转变为RUNNABLE状态,然后等待系统资源分配,一旦获得执行机会,便会执行run方法中的任务,实现真正的多线程工作。
直接调用run方法的情况不同,它会将run方法视为main线程中的普通函数执行,无法在新的线程中启动,因此不能用来启动线程。如果我们尝试多次调用start方法,只有第一次会成功启动线程,找巴士源码后续调用会抛出IllegalThreadStateException异常,因为线程的状态已经变为非初始状态,不能再调用start方法。
以下是start方法和run方法的源码理解:
start方法会检查线程状态,如果状态不是初始态,就会抛出异常。而run方法本身不触发线程的创建,仅在start方法调用后被执行。
总结来说,start方法是启动线程的关键,它确保了线程的生命周期管理和正确执行,而run方法则是线程实际执行的任务。理解这些原理对于正确使用和管理Java线程至关重要。
剖析Linux内核源码解读之《实现fork研究(二)》
本文深入剖析了Linux内核源码中fork实现的核心过程,重点在于copy_process函数的解析。在Linux系统中,应用层可以通过fork创建子进程或子线程,而内核并不区分两者,它们共享相同的task_struct结构,用于描述进程或线程的状态、资源等。task_struct包含了进程或线程所有关键数据结构,如内存描述符、文件描述符、信号处理等,是内核调度程序识别和管理进程的重要依据。
copy_process作为fork实现的关键,其主要任务是初始化task_struct结构,分配新进程的PID,并将其加入到运行队列。这个过程中,内核栈的初始化导致了fork()调用的两次返回值不同,这与copy_thread函数中父进程复制内核栈至子进程并清零寄存器值有关。这样,子进程返回0,而父进程继续执行copy_thread后续操作,最后返回子进程的PID。
对于线程的独有和共享资源,独有资源通常包括线程特定的数据结构和状态,而共享资源则涉及父进程与线程间的共享内存、文件描述符和信号处理等。这些资源的管理对于多线程程序的正确运行至关重要,需确保线程间资源的互斥访问和安全共享。
一文读懂ThreadLocal的原理及使用场景
ThreadLocal 类是用来提供线程内部的局部变量,即线程本地变量。这种变量在多线程环境下访问(通过get和set方法访问)时能够保证各个线程的变量相对独立于其他线程内的变量,不同线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。
ThreadLocal 表示线程的“本地变量”,即每个线程都拥有该变量副本,达到人手一份的效果,各用各的,这样就可以避免共享资源的竞争。
在高并发中会存在多个线程同时修改一个共享变量的场景,这就可能会出现线性安全问题。为了解决线性安全问题,可以通过加锁来实现,例如使用synchronized 或者Lock。但是加锁的方式可能会导致系统变慢。另外一种方式,可以使用ThreadLocal类访问共享变量,这样会在每个线程的本地,都保存一份共享变量的拷贝副本。这是一种“空间换时间”的方案,虽然会让内存占用大很多,但是由于不需要同步也就减少了线程可能存在的阻塞等待,从而提高时间效率。
接下来就让我们学习 ThreadLocal 的几个核心方法,来了解ThreadLocal 的实现原理。
set() 方法设置当前线程中 ThreadLocal 变量的值,该方法的源码为:通过源码我们知道 value 是存放在 ThreadLocalMap 里的,数据 value 是存放在 ThreadLocalMap 这个容器中的,并且是以当前 ThreadLocal 实例为 key 的。
ThreadLocalMap 是怎样来的?源码很清楚,是通过getMap(t)进行获取:该方法直接返回当前线程对象 t 的一个成员变量 ThreadLocals:也就是说ThreadLocalMap 的引用是作为 Thread 的一个成员变量的,被 Thread 进行维护的。
总结一下 set 方法:通过当前线程对象 thread 获取该 thread 所维护的 ThreadLocalMap,如果 ThreadLocalMap 不为 null,则以 ThreadLocal 实例为 key,值为 value 的键值对存入 ThreadLocalMap,若 ThreadLocalMap 为 null 的话,就新建 ThreadLocalMap,然后再以 ThreadLocal 为键,值为 value 的键值对存入即可。
get() 方法是获取当前线程中 ThreadLocal 变量的值,代码逻辑请看注释,另外,看下 setInitialValue 主要做了些什么事情?这段方法的逻辑和 set 方法几乎一致,关注的是 initialValue 方法:这个方法是 protected 修饰的,也就是说继承 ThreadLocal 的子类可重写该方法,实现赋值为其他的初始值。
总结一下 get 方法:通过当前线程 thread 实例获取到它所维护的 ThreadLocalMap,然后以当前 ThreadLocal 实例为 key 获取该 map 中的键值对(Entry),如果 Entry 不为 null 则返回 Entry 的 value。如果获取 ThreadLocalMap 为 null 或者 Entry 为 null 的话,就以当前 ThreadLocal 为 Key,value 为 null 存入 map 后,并返回 null。
remove() 方法实现了如何删数据的操作。删除数据当然是从 map 中删除数据,先获取与当前线程相关联的 ThreadLocalMap,然后从 map 中删除该 ThreadLocal 实例为 key 的键值对即可。
从上面的分析我们已经知道,数据其实都放在了 ThreadLocalMap 中,ThreadLocal 的 get、set 和 remove 方法实际上都是通过 ThreadLocalMap 的 getEntry、set 和 remove 方法实现的。ThreadLocalMap 是 ThreadLocal 一个静态内部类,内部维护了一个数组(Entry 类型的 table 数组),Entry 是一个以 ThreadLocal 为 key,Object 为 value 的键值对,这里的ThreadLocal 是弱引用。每个线程实例中都可以通过 ThreadLocals 获取到 ThreadLocalMap,而 ThreadLocalMap 实际上就是一个以 ThreadLocal 实例为 key,任意对象为 value 的 Entry 数组。当我们为 ThreadLocal 变量赋值时,实际上就是以当前 ThreadLocal 实例为 key,值为 value 的 Entry 往这个 ThreadLocalMap 中存放。需要注意的是,Entry 中的 key 是弱引用,当 ThreadLocal 外部强引用被置为 null(ThreadLocalInstance=null)时,系统 GC 的时候,根据可达性分析,这个 ThreadLocal 实例就没有任何一条链路能够引用到它,此时 ThreadLocal 势必会被回收,这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry,如果没有办法访问这些 key 为 null 的 Entry 的 value,如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 永远无法回收,造成内存泄漏。
当然,如果当前 thread 运行结束,ThreadLocal、ThreadLocalMap、Entry 没有引用链可达,在垃圾回收的时候都会被系统回收。在实际开发中,会使用线程池去维护线程的创建和复用,比如固定大小的线程池,线程为了复用是不会主动结束的。
本文主要讲解了ThreadLocal的作用及基本用法,以及ThreadLocal的实现原理和基础方法。线上环境中,ThreadLocal还有可能引起内存泄漏,这方面内容我们后续接着讲。
本文由 mdnice 多平台发布
ThreadPoolExecutor简介&源码解析
线程池是通过池化管理线程的高效工具,尤其在多核CPU时代,利用线程池进行并行处理任务有助于提升服务器性能。ThreadPoolExecutor是线程池的具体实现,它负责线程管理和任务管理,以及处理任务拒绝策略。这个类提供了多种功能,如通过Executors工厂方法配置,执行Runnable和Callable任务,维护任务队列,统计任务完成情况等。
创建线程池需要考虑关键参数,如核心线程数(任务开始执行时立即创建),最大线程数(任务过多时限制新线程生成),线程存活时间,任务队列大小,线程工厂以及拒绝策略。JDK提供了四种拒绝策略,如默认的AbortPolicy,当资源饱和时抛出异常。此外,线程池还提供了beforeExecute和afterExecute钩子函数,用于在任务执行前后执行自定义操作。
当任务提交到线程池时,会经历一系列处理流程,包括任务的执行和线程池状态的管理。例如,如果任务队列和线程池满,会根据拒绝策略处理新任务。使用线程池时,需注意线程池容量与状态的计算,以及线程池工作线程的动态调整。
示例中,自定义线程池并重写钩子函数,创建任务后向线程池提交,可以看到线程池如何根据配置动态调整资源。但要注意,如果任务过多且无法处理,可能会抛出异常。源码分析中,submit方法实际上是调用execute,而execute内部包含Worker类和runWorker方法的逻辑,包括任务的获取和执行。
线程池的容量上限并非Integer.MAX_VALUE,而是由ctl变量的低位决定。 Doug Lea的工具函数简化了ctl的操作,使得计算线程池状态和工作线程数更加便捷。通过深入了解ThreadPoolExecutor,开发者可以更有效地利用线程池提高应用性能。
Qt——QThread源码浅析
在探索Qt的多线程处理中,QThread类的实现源码历经变迁。在Qt4.0.1和Qt5.6.2版本中,尽管QThread类的声明相似,但run()函数的实现有所不同。从Qt4.4开始,QThread不再是抽象类,这标志着一些关键调整。
QThread::start()函数在不同版本中的核心代码保持基本一致,其中Q_D()宏定义是一个预处理宏,用于获取QThread的私有数据。_beginthreadex()函数则是创建线程的核心,调用QThreadPrivate::start(this),即执行run()函数并发出started()信号。
QThread::run()函数在Qt4.4后的版本中,不再强制要求重写,而是可以通过start启动事件循环。在Qt5.6.2版本中,run函数的定义更灵活,可以根据需要进行操作。
关于线程停止,QThread提供了quit()、exit()和terminate()三种方式。quit()和exit(0)等效,用于事件循环中停止线程,而terminate()则立即终止线程,但不推荐使用,因为它可能引发不稳定行为。
总结起来,QThread的核心功能包括线程的创建、run函数的执行以及线程的结束控制。从Qt4.4版本开始,QThread的使用变得更加灵活,可以根据需要选择是否重写run函数,以及如何正确地停止线程。不同版本间的细微差别需要开发者注意,以确保代码的兼容性和稳定性。