QTimer與事件迴圈和多執行緒
定時器的原始碼分析
startTimer
返回定時器的ID,在定時時間到了後,收到一個QTimerEvent
,並覆蓋虛擬函式timerEvent
進行處理,該QTimerEvent包括了定時器ID
看QTimer的原始碼就明白了:
QObject::startTimer()
{
if (Q_UNLIKELY(!d->threadData->eventDispatcher.load())) {
qWarning("QObject::startTimer: Timers can only be used with threads started with QThread" );
return 0;
}
if (Q_UNLIKELY(thread() != QThread::currentThread())) {
qWarning("QObject::startTimer: Timers cannot be started from another thread");
return 0;
}
// 呼叫物件關聯執行緒的eventDispatcher來註冊定時器,killTimer中是unregisterTimer
int timerId = d->threadData->eventDispatcher. load()->registerTimer(interval, timerType, this);
if (!d->extraData)
d->extraData = new QObjectPrivate::ExtraData;
d->extraData->runningTimers.append(timerId);
return timerId;
}
event dispatcher維護每個QObject物件關聯的定時器的列表,再看registerTimer的原始碼:
void QEventDispatcherWin32::registerTimer (int timerId, int interval, Qt::TimerType timerType, QObject *object)
{
if (timerId < 1 || interval < 0 || !object) {
qWarning("QEventDispatcherWin32::registerTimer: invalid arguments");
return;
} else if (object->thread() != thread() || thread() != QThread::currentThread()) {
// 又判斷是不是跟event dispatcher同一執行緒
qWarning("QEventDispatcherWin32::registerTimer: timers cannot be started from another thread");
return;
}
Q_D(QEventDispatcherWin32);
// exiting ... do not register new timers (QCoreApplication::closingDown() is set too late to be used here)
if (d->closingDown)
return;
// 分配計時器ID,間隔,型別
WinTimerInfo *t = new WinTimerInfo;
t->dispatcher = this;
t->timerId = timerId;
t->interval = interval;
t->timerType = timerType;
t->obj = object;
t->inTimerEvent = false;
t->fastTimerId = 0;
if (d->internalHwnd)
d->registerTimer(t); // 進內部實現類的同名函式
d->timerVec.append(t); // store in timer vector
d->timerDict.insert(t->timerId, t); // store timers in dict
}
QAbstractEventDispatcher::registeredTimers 可被用來查詢QObject物件所關聯的定時器的列表。即QList<TimerInfo> list;
,其中的結構體TimerInfo除了一個行內函數,成員如下:
int timerId;
int interval;
Qt::TimerType timerType;
enum TimerType { //下面的函式會根據不同型別做不同處理
PreciseTimer,
CoarseTimer,
VeryCoarseTimer
};
接著看內部實現類的同名函式,這次有點複雜:
void QEventDispatcherWin32Private::registerTimer(WinTimerInfo *t)
{
//引數是 internal window handle used for socketnotifiers/timers/etc
Q_ASSERT(internalHwnd);
Q_Q(QEventDispatcherWin32);
bool ok = false;
calculateNextTimeout(t, qt_msectime());
uint interval = t->interval;
if (interval == 0u) {
//對於時間間隔為0的timer,不啟動系統的計時器,非同步傳送QZeroTimerEvent 事件到自身來處理
QCoreApplication::postEvent(q, new QZeroTimerEvent(t->timerId));
ok = true;
} else if (interval < 20u || t->timerType == Qt::PreciseTimer) {
//間隔小於20ms或者型別時Precise,首先用多媒體定時器(fast timer)
t->fastTimerId = timeSetEvent(interval, 1, qt_fast_timer_proc, DWORD_PTR(t),
TIME_CALLBACK_FUNCTION | TIME_PERIODIC | TIME_KILL_SYNCHRONOUS );
ok = t->fastTimerId;
}
if (!ok) {
// 如果多媒體定時器不可用,或者是(Very)CoarseTimers型別,使用SetTimer傳送
WM_TIMER訊息到回撥函式中並作為QEvent::Timer傳送到QObject。
ok = SetTimer(internalHwnd, t->timerId, interval, 0);
}
if (!ok) // 再失敗就報錯
qErrnoWarning("QEventDispatcherWin32::registerTimer: Failed to create a timer");
}
程式碼根據時間間隔給出了三種處理方式。
1. 間隔為0,不再解釋
2. timeSetEvent
,它接受一個回撥函式qt_fast_timer_proc
,到期時它會被呼叫並在一個獨立的執行緒中被呼叫。回撥函式 post 一個訊息到派發器,派發器將獲得該訊息並作為QEvent::Timer
傳送,看原始碼:
void WINAPI QT_WIN_CALLBACK qt_fast_timer_proc(uint timerId, uint /*reserved*/, DWORD_PTR user, DWORD_PTR /*reserved*/, DWORD_PTR /*reserved*/)
{
if (!timerId) // sanity check
return;
WinTimerInfo *t = (WinTimerInfo*)user;
Q_ASSERT(t);
QCoreApplication::postEvent(t->dispatcher, new QTimerEvent(t->timerId));
}
- 時間間隔大於20毫秒,它使用 SetTimer傳送WM_TIMER訊息到回撥函式
qt_internal_proc
,並作為QEvent::Timer傳送到QObject。回撥函式中的部分程式碼:
else if (message == WM_TIMER) {
Q_ASSERT(d != 0);
d->sendTimerEvent(wp);
return 0;
}
接著找:
QEventDispatcherWin32Private::sendTimerEvent
{
WinTimerInfo *t = timerDict.value(timerId);
if (t && !t->inTimerEvent) {
// send event, but don't allow it to recurse
t->inTimerEvent = true;
QTimerEvent e(t->timerId);
//同步派發了QTimerEvent事件
QCoreApplication::sendEvent(t->obj, &e);
. . . . . .
}
}
以上傳送的QEvent::Timer
事件,處理都在QObject::timerEvent
當中。
timerEvent() 中發射 timeout()
訊號:
void QTimer::timerEvent(QTimerEvent *e)
{
if (e->timerId() == id) {
if (single)
stop();
emit timeout(QPrivateSignal());
}
}
定時器ID
定時器ID是按某分配演算法得到的,一定是唯一的(甚至在跨執行緒的情況下)。當一個QObject從一個執行緒移動到另一個時(moveToThread
),它的定時器也會隨它一起移動。移動定時器是一個簡單從舊執行緒的派發器登出計時器並在在新執行緒的派發器中註冊的問題。如果定時器的ID是不是唯一的,定時器ID可能會與新執行緒中現有的定時器發生衝突。
moveToThread的三大任務中的第三條是解除在當前執行緒中的timer註冊,在目標執行緒中重新註冊。原因是第1條:函式通過sendEvent()派發 QEvent::ThreadChange
事件,在QObject::event中處理。看event函式中的部分原始碼:
case QEvent::ThreadChange: {
Q_D(QObject);
QThreadData *threadData = d->threadData;
QAbstractEventDispatcher *eventDispatcher = threadData->eventDispatcher.load();
if (eventDispatcher) {
QList<QAbstractEventDispatcher::TimerInfo> timers = eventDispatcher->registeredTimers(this);
if (!timers.isEmpty()) {
// do not to release our timer ids back to the pool (since the timer ids are moving to a new thread).
// 解除註冊
eventDispatcher->unregisterTimers(this);
QMetaObject::invokeMethod(this, "_q_reregisterTimers", Qt::QueuedConnection,
Q_ARG(void*, (new QList<QAbstractEventDispatcher::TimerInfo>(timers)))); } }
break;
}
又非同步呼叫了函式_q_reregisterTimers
,其中eventDispatcher->registerTimer(ti.timerId, ti.interval, ti.timerType, q);
實現了重新註冊。
多執行緒中使用定時器
從上面的分析可以看出,多執行緒中使用定時器時,必須在同一個執行緒裡開始和停止定時器,也就是隻有在建立定時器的執行緒裡才能接受到timeout()訊號,一般是次執行緒的run
函式。否則就會在startTimer和registerTimer中報錯。
另一種方法,是將定時器和工作類都移到某個子執行緒。
現在看這樣的程式,在次執行緒開啟定時器,每秒產生一個隨機數,然後在主執行緒的文字框中新增一個個的隨機數。
class MyObj : public QObject
{
Q_OBJECT
public:
MyObj();
signals:
void toLine(QString line);
private slots:
void doWork();
void testTimer();
private:
QTimer* timer;
};
void MyObj::doWork()
{
qDebug()<<"timer thread:"<<QThread::currentThread();
timer = new QTimer();
connect(timer,SIGNAL(timeout()),this,SLOT(testTimer()));
timer->start(2000);
}
void MyObj::testTimer()
{
QString str = QString::number(qrand()%100);
qDebug()<<"test timer:"<<str;
emit toLine(str);
}
在次執行緒中建立和開啟了timer
主執行緒部分的程式碼:
t = new QThread();
obj = new MyObj();
obj->moveToThread(t);
qDebug()<<"main thread:"<<QThread::currentThread();
connect(t,SIGNAL(started()), obj, SLOT(doWork()), Qt::QueuedConnection);
connect(obj,SIGNAL(toLine(QString)),this,SLOT(appendText(QString) ),Qt::QueuedConnection );
connect(t,SIGNAL(finished()), obj, SLOT(deleteLater()) );
t->start();
與前一篇的程式碼幾乎相同,不再解釋。