17.4 多线程的替代方案

如果线程使用的复杂性让你感到气馁,也许你可以尝试一些简单的替代方案,比如使用定时器,空闲时间处理或者两者一起使用.

使用wxTimer

wxTimer类让你的程序可以周期性的收到提示,或者在某个特定的时间间隔收到提醒.如果你要使用线程处理的事情可以分成小的时间片,每隔几个毫秒处理一次,以便你的应用程序可以有足够的时间响应用户的输入,你就可以使用wxTimer来代替多线程.

你可以自己选择提醒的通知方式,如果你更喜欢使用虚函数,就实现一个wxTimer的派生类,然后重载其Notify函数,如果你更倾向使用事件机制,就给你的wxTimer构造函数指定一个wxEvtHandler指针(或者使用SetOwner)函数,然后使用EVT_TIMER(id, func)事件映射宏来将事件映射到对应的处理函数.

你可以给wxTimer的构造函数或者SetOwner函数传递一个可选的定时器标识符,这个标识符可以用在EVT_TIMER事件映射宏中.这在你需要使用多个定时器的时候比较有用.

使用Start函数启动定时器,需要传递的参数包括一个毫秒为单位的定时器时长和可选的wxTIMER_ONE_SHOT指示(如果你只希望收到一次提示).Stop函数用来终止某个定时器,IsRunning函数用来检测当前定时器是否出于运行状态.

下面的代码演示了怎样通过事件的方式使用定时器:

#define TIMER_ID 1000
class MyFrame : public wxFrame
{
public:
    ...
    void OnTimer(wxTimerEvent& event);
private:
    wxTimer m_timer;
};
BEGIN_EVENT_TABLE(MyFrame, wxFrame)
    EVT_TIMER(TIMER_ID, MyFrame::OnTimer)
END_EVENT_TABLE()
MyFrame::MyFrame()
       : m_timer(this, TIMER_ID)
{
    // 1秒的间隔
    m_timer.Start(1000);   
}
void MyFrame::OnTimer(wxTimerEvent& event)
{
    // 你可以在这里作任何你希望1秒执行一次的动作
}

注意这里的时间间隔很难作到精确,实际的间隔时长要看定时器时间处理函数执行之前发生的事情的忙闲状况而定.

当我们想精确的测量时长的时候,wxStopWatch是一个很有用的类,它的构造函数开始记录时间,你可以暂停和恢复它以便获得某个特定时长的精确时间.

wxStopWatch sw;
SlowBoringFunction();
// 暂停监视
sw.Pause();
wxLogMessage("The slow boring function took %ldms to execute",
             sw.Time());
// 恢复监视
sw.Resume();
SlowBoringFunction();
wxLogMessage("And calling it twice took %ldms in all", sw.Time());

空闲时间处理

另外一种代替多线程处理的方法是使用空闲时间处理函数.应用程序类和所有的窗口类在所有其它的事件处理完以后都会收到系统空闲事件,你可以拦截这个事件并增加自己的处理函数.如果你希望收到更多的空闲事件,你可以调用wxIdleEvent::RequestMore函数,否则一次空闲事件处理完成以后,要等到下次用户界面事件处理周期结束才会再次收到系统空闲事件.另外,通常你还需要调用wxIdleEvent::Skip函数,以便别的对象的系统空闲事件处理函数可以被执行.

在下面的例子中,假想的函数FinishedIdleTask在每次空闲事件处理函数中处理一部分工作,这个函数在整个工作完成以后返回True.

class MyFrame : public wxFrame
{
public:
    ...
    void OnIdle(wxIdleEvent& event);
    // 作一小部分工作,如果做完则返回True
    bool FinishedIdleTask();
};
BEGIN_EVENT_TABLE(MyFrame, wxFrame)
    EVT_IDLE(MyFrame::OnIdle)
END_EVENT_TABLE()
void MyFrame::OnIdle(wxIdleEvent& event)
{
    // 进行空闲事件处理,如果没有处理完
    // 再次请求空闲事件
    if (!FinishedIdleTask())
        event.RequestMore();
    event.Skip();
}

虽然我们的例子中使用的是frame窗口,并不意味着空闲事件只能存在于顶层窗口,实际上,任何窗口都可以处理这个事件.比如,如果你想制作一个定制的图像显示控件,如果窗口大小发生了变化,这个控件只在空闲事件才进行重画以避免窗口大小改变时立即重画可能引发的闪铄.为了确定这个空闲处理不被应用程序的空闲事件处理函数打断,你可以在你的自定义控件中重载虚函数OnInternalIdle,并调用其基类的OnInternalIdle 函数.它的代码大致象下面的样子:

void wxImageCtrl::OnInternalIdle()
{
    wxControl::OnInternalIdle();

    if (m_needResize)
    {
        m_needResize = false;
        SizeContent();
    }
}
void wxImageCtrl::OnSize(wxSizeEvent& event)
{
    m_needResize = true;
}

有时候你可能想强制执行系统空闲处理函数,即使没有任何事件导致系统空闲事件被再次调用.你可以通过wxWakeUpIdle函数强制启动空闲事件处理.另外一个方法是启动一个不做任何事情的定时器,由于定时器会定时送出定时器事件,因此在定时器事件处理完以后(实际上什么事也没做),就会周期性的引发系统空闲事件处理,要强制立即处理所有的空闲事件,你可以直接调用wxApp::ProcessIdle函数,但是依平台的不同,这可能会影响到内部空闲事件处理(比如在GTK+平台上,窗口重绘都是在空闲时间完成的).

使用空闲事件进行用户界面更新的内容,我们已经在第9章,"创建定制的对话框"中介绍过,它用来另外一种形式的系统空闲事件wxUpdateUIEvent.

强制处理用户界面更新事件

当一个应用程序忙于处理一个很耗时的工作的时候,它可能来不及更新用户界面,这时候用户界面就好像被冻结一样,要避免这种情况,你可以在那个耗时的任务中周期性的调用wxApp::Yield函数(或者它的等价体wxYield).不过这种方法应该尽量少的使用,因为它可能会导致不可知的问题.比如,Yield可能会导致处理用户命令事件,这个事件可能会导致特定的任务被重新执行,尽管我们目前仍在执行这个任务.我们称之为重入.函数 wxSafeYield会首先禁用所有的窗口,然后Yield,然后再重新使能所有的窗口以尽可能避免重入.如果你给wxApp::Yield函数传递 True作为参数,那么如果正在进行Yield,第二次的Yield动作将放弃,这也是另外一种降低重入风险的办法.

如果你希望周期的更新特定显示,直接调用wxWindow::Update函数会比较好,因为这将只会导致未处理的重绘事件被处理.