问题分析
在程序设计中,我们经常遇到这样的问题:
有一个模块,外部多线程访问,其内部也有多个工作线程;外部调用和内部工作都可能产生一系列事件(包括状态变化),这些都需要“有序”并且“实时”的通知到外部。
总结一下基本需求:
- 多路,事件来源于多个线程
- 有序,事件通知要严格保持其产生的顺序
- 实时,外部收到状态变化(一种事件)通知时,可以假定模块仍然处在该状态
- 串行,外部不需要在回调方法中显示同步,“有序”其实隐含串行的要求
考虑一下实时的意义。比如模块是用来串行完成一个个任务的,每个任务执行中会通知外部任务状态的变化。外部在收到某个状态变化时,会去获取当前任务的部分中间结果。如果通知不是实时的,也可能从通知发出到收到这之间,模块已经执行的下一个任务了,那么外部也可能获取的是一个任务的中间结果,从而导致错误的处理。
上面这个问题可以有其他方式解决,比如获取中间结果时带上任务ID,但是本文还是想纯粹从“事件通知”这个问题角度做一些探讨。
为此我们设计这样一种通知机制:
- 通知队列,多线程产生的事件,需要在队列中排序,满足“有序”的需求
- 当即通知,每个方法(包括内部线程和外部调用的方法)在退出前,都触发事件的分派(即针对每个事件,调用回调方法)
- 独占分派,当一个线程正在分派一个事件时(调用回调方法),其他线程不能分派事件,满足“串行”需求
针对没有抢到分派权时,如何处理,有两种策略:
- 退出分派
因为肯定有其他线程会继续分派剩余事情,所以不需要担心当前线程产生的事情滞留在队列中(假设此后没有触发新的分派,可能导致滞留);但是损失了部分实时性
- 继续等待
等待并尝试分派后续事件,这种策略能够提高实时性
其他设计:
- 不在加锁状态下分派事件:考虑外部会在事件回调中,调用模块的方法,可能导致死锁
- 检测分派重入情形:还是上面的情况,当前线程再次进入模块事件分派方法,可以检测并忽略(直接退出分派方法)
- 内部锁递归检测:考虑模块内部方法A加锁调用公开方法B,B需要在退出前分派事件,这就会在加锁状态下分派事件,所以需要检测内部锁是否lock
队列的实现
内部变量包括:
- 一个先入先出的队列
- 当前在分派的线程
- 一个互斥锁
- 一个条件变量
方法:
push:加入事件,多线程调用,需要互斥锁保护;加入事件不通知分派,因为没有固定的分派线程
poll:分派事件,步骤:
- lock
- 检查分派线程是否为NULL,非空退出(也可以不退出,等待策略;但是如果等于当前线程,仍然退出)
- 等待分派线程变为NULL,通过条件变量循环等待
- 如果事件队列为空,退出
- 取出一个事件,设置分派线程为当前线程
- unlock
- 调用回调
- lock
- 设置分派线程为NULL
- 唤醒条件变量
- unlock
- 循环到第1步(这里其他分派线程可以进入,抢夺分派权)
附Java实现类
package xxx;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.List;
public class NotifyQueue<Listener> {
private static final String TAG = "NotifyQueue";
private List<Object> mPendingNotifies = new ArrayList<Object>();
private Thread mNotifyThread;
private Listener mNoopListener;
private List<Listener> mListeners = new ArrayList<Listener>();
private Listener[] mListenerCopy;
private int mListenerCopyCount = 0;
private Object mLock = new Object();
@SuppressWarnings("unchecked")
NotifyQueue(Listener noop) {
mNoopListener = noop;
mListenerCopy = (Listener[]) Array.newInstance(noop.getClass().getInterfaces()[0], 4);
}
public void addListener(Listener listener) {
synchronized (mLock) {
if (!mListeners.contains(listener))
mListeners.add(listener);
}
}
public void removeListener(Listener listener) {
removeListener(listener, true);
}
public void removeListener(Listener listener, boolean wait) {
synchronized (mLock) {
if (!mListeners.remove(listener))
return;
if (mListenerCopyCount == 0) { // mNotifyThread is not null when locked
return;
}
// make sure that the listener is not using
if (mNotifyThread == Thread.currentThread()) {
for (int i = 0; i < mListenerCopyCount; ++i) {
if (mListenerCopy[i] == listener)
mListenerCopy[i] = mNoopListener;
}
} else if (wait) {
while (mNotifyThread != null) {
try {
mLock.wait();
} catch (InterruptedException e) {
Log.w(TAG, "removeTaskStateChangeListener", e);
}
}
}
}
}
public void push(Object notify) {
synchronized (mLock) {
mPendingNotifies.add(notify);
}
}
/*
* try to dispatch notifies,
* exit soon if another thread is dispatching
*/
public void poll() {
poll(false);
}
/*
* wait for all notifies to be dispatched,
* not matter dispatch by self or another threads
*/
public void sync() {
poll(true);
}
protected void poll(boolean sync) {
Object notify = null;
while (true) {
synchronized (mLock) {
if (mNotifyThread != null) {
if (!sync) return;
while (mNotifyThread != null) {
try {
mLock.wait();
} catch (InterruptedException e) {
Log.w(TAG, "poll", e);
}
}
}
if (mPendingNotifies.isEmpty())
return;
mNotifyThread = Thread.currentThread();
mListenerCopy = mListeners.toArray(mListenerCopy);
mListenerCopyCount = mListeners.size();
notify = mPendingNotifies.remove(0);
}
invoke(mListenerCopy, mListenerCopyCount, notify);
synchronized (mLock) {
// not clear mListenerCopy for optimize
mListenerCopyCount = 0;
mNotifyThread = null;
mLock.notifyAll();
}
}
}
protected boolean lock(Thread lockThread) {
Log.d(TAG, "lock");
synchronized (mLock) {
if (mNotifyThread == null) {
mNotifyThread = lockThread;
return true;
} else {
return false;
}
}
}
protected boolean unlock(Thread lockThread) {
Log.d(TAG, "unlock");
synchronized (mLock) {
if (mNotifyThread == lockThread) {
mNotifyThread = null;
mLock.notifyAll();
} else {
return false;
}
}
poll();
return true;
}
protected void invoke(Listener[] listeners, int count, Object notify) {
if (notify instanceof Runnable) {
Runnable runnable = (Runnable) notify;
runnable.run();
}
}
}