Build Your Own HandlerThread Part Finale

至此,我们整个系列宣告完结。

现在回过头往前看,是不是觉得其实这些底层的东西也没这么难?有时候只是需要一些基础和解决问题的方法罢了。

Rant:做业务才真的难呢,框架和流程都给你定死了,一坨又一坨用了几年的不明觉厉的封装,如果内部文档哪个不详,又没有熟悉的人带着你,基本就净在里面绕圈子了。

Read More

Build Your Own HandlerThread Part 4

有了前面的铺垫我们终于可以开始实现我们的主角 ActiveThread 了,虽然它登场的有点晚。

这个系列的开头我们提到 ActiveThread 有两个鲜明的特点:

  • 它是线程,可以运行,代表一个单独的执行上下文(execution unit/context)
  • 每个 ActiveThread 内部运行一个 message-loop,方便持续的执行我们提交的任务

这两点和 Android 原生提供的 HandlerThread 是一模一样的。

不过和 HandlerThread 不同,我们这里不打算采用继承 Thread 的方式,而是采用 composition。原因之一是我个人非常反感传统的 OO 继承手法;并且 Java 8 开始正式支持 lambda 之后,不用继承我们的工作也可以做得很好。

不适用继承同时有个好处,我们可以只暴露我们需要的接口,避免误用(比如经典的用 run() 而不是 start()

Read More

Build Your Own HandlerThread Part 3

前面我们实现了 MessageLoop,现在该轮到 TaskRunner 了。

在我们的设计中,TaskRunner 的定位是类似 Handler,用来驱动/使用 MessageLoop,将 MessageLoop 隐藏在日常使用中。

Read More

Build Your Own HandlerThread Part 2

有了 MessagePumpDefault 之后,我们就可以开始着手实现最核心的 MessageLoop 了。

不过在此之前,我们还首先需要实现一个辅助设施:PendingTask

PendingTask

一个 PendingTask instance 表示一个等待被执行的 task,并且这个 task 可能是一个 delayed task。

因此 PendingTask 需要能够表示时序上的顺序,这个可以利用 Instant 类型的一个时间戳,结合一个 long 类型的 sequence-number。因为有可能两个 delayed tasks 的时间戳相同,此时就必须要用 seq-num 来区分先后顺序。

另外,PendingTask 为了能够表示 task 语义,他必须可以被执行。这可以通过内部存储一个 Runnable 或者 Callable 成员做到。

我们的实现选择 Runnable,因为

  • 我们当前的实现不考虑返回值,因为实现返回值和我们的主题没有直接关系。
    CSP 的并发模型上如果要考虑使用某个任务的返回值,一般会使用类似于 PostTaskAndReplyWithResult() 来组合两个函数;或者更一般的,使用 continuation 来 lifting restrictions
  • 懒得处理异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class PendingTask {
private static AtomicLong s_seqNumGenerator = new AtomicLong(0);

private final long _seqNum;
private final Instant _endTime;

private Runnable _task;

PendingTask(Runnable task, Instant endTime) {
_seqNum = s_seqNumGenerator.getAndIncrement();
_endTime = endTime;

_task = task;
}

void run() {
_task.run();
}

Instant endTime() {
return _endTime;
}

long seqNum() {
return _seqNum;
}
}

我们使用了 AtomicLong 来存储当前的 sequence-number,因为我们不知道某个 PendingTask 会在哪个线程上被创建。

Read More

Build Your Own HandlerThread Part 1

MessagePump

对于我们的 ActiveThread 来说,核心仍然是持续不断运行的 message-loop;但是在实现 message-loop 前,我们需要首先来实现它的“引擎”:MessagePump

大体上,message-loop 要维持运转,就要能够源源不断地从某个地方获取消息/事件,而获取消息/事件的这个行为,就是由 message-pump 来完成,正如他的名字一样。

这里可能会产生一个疑问,为什么要单独抽出一个 message-pump,而不是直接把这部分实现做到 message-loop 里呢?

原因是:一个复杂的系统可能会有多个消息/事件来源,每个来源会有专门的获取、分发的方式。

例如:

  • 界面消息可能是从系统的 GUI 消息系统中获得。一个典型的例子是 Win32 GUI 程序所使用的 GetMessage()
  • 网络活动事件通常从 native 系统提供的 I/O multiplexor 中获得。例子包括 POSIX 标准的 select(2), poll(2);Linux 独有的 epoll;以及 Windows 提供的真-异步I/O机制 IO Completion Port。

注:GUI 系统中多使用 messages(消息)作为术语;而网络 I/O 等倾向于使用 events(事件)作为使用术语。因为此系列文章不涉及网络 I/O,因此以后均采用_消息_作为我们的使用术语。

注1:对于 Android 系统,他的 GUI 事件实质上是通过 epoll 监听关联到 device 抽象的 pipe fd 来完成的;Linux 的各类 X-Window 实现大多也是类似做法。

所以为了解耦具体的消息来源,同时让我们的实现看上去更加有逼格,我们采取分离实现 message-pump 和 message-loop。

前面说到可能有多种 pump,所以这里我们的 MessagePump 应该是一个 interface,规定了一个 message-pump 应该具有哪些基本的行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.time.Instant;

interface MessagePump {
interface Delegate {
boolean doWork();
boolean doDelayedWork();
Instant nextDelayedWorkExpiration();
}

void run(Delegate delegate);

// Quit the pump.
// This method can only be called on the thread that called run().
void quit();

// Thread-safe
void wakeup();
}

同时我们定义了一个 Delegate interface,message-loop 需要实现这个 interface,以便让 message-pump 可以通过这个 interface 来按照自己的实现来操作 message-loop。

什么都不做的 MessagePumpDefault

我们的 ActiveThread 运行的纯 Java 环境,不需要像 Android 那样能够获取设备输入消息(Frankly,你想做还做不了呢,我哪来的设备信息获取啊),有点类似 worker HandlerThread.

Read More

Build Your Own HandlerThread Part 0

立一个 Flag

HandlerThread 是 Android runtime 提供的一种线程基础设施;和 Java Thread 相比,HandlerThread 最大的优点是自带消息循环(message-loop),这使得用户可以很方便地基于 HandlerThread 实现 CSP (Communicating Sequential Process) 模型,极大降低多线程编程的复杂度。

Read More

Monthly Read Posts in Nov 2018

Algorithms

Shuffling

Read More