最近正在忙着开发一款三维CAD系统。BOSS希望该CAD系统具备文档自动保存功能,就像Office系列软件一样,在用户不知不觉的情况下将其所作修改进行保存,这样可以防止在意外宕机情况下用户的操作丢失。
其实添加自动保存功能并不是一件非常困难的事情,最简单的情况就是设置Timer计时器,捕捉系统消息WM_TIMER,并在Timer的响应函数里调用CDocument::OnSaveDocument()函数,使用主线程进行文档保存。不过,这样做是有弊端的。例如在我们所要做的CAD系统中,在实际使用过程中,可能会出现数据量非常庞大的零部件或组装结构。如果在这种情况下使用主线程来保存文档,文档的保存时间可能会较长。而在文档保存的过程中,系统会将光标设置为等待,因此,不仅无法再对文档进行操作,即使能操作,也会因为主线程正在忙碌状态而使系统无法响应用户操作,势必影响使用。
一个较好的方法就是使用辅助工作线程来进行文档的保存工作。由于工作线程只在后台运行,与主线程相对独立,因此,可以较好地解决用户操作与文档保存的并行问题。虽然在单核CPU情况下,所谓的多线程仍然存在CPU使用上的等待与交替过程,但由系统强制分配的CPU使用时间保证了两种操作形式上的并行,基本上解决了问题。
另外需要强调的是,由于MFC类库中的CDocument类在使用过程中,其内部函数与主线程存在线程依赖关系。具体是由一个Map影射造成的,因此如果使用传递给工作线程处理函数的CDocument类型指针直接调用CDocument::OnSaveDocument()则会造成程序断言失败而意外退出。下面是windows核心代码wincore.cpp内部包含的一个原函数。
void CWnd::AssertValid() const...{ if (m_hWnd == NULL) return; // null (unattached) windows are valid // check for special wnd??? values ASSERT(HWND_TOP == NULL); // same as desktop if (m_hWnd == HWND_BOTTOM) ASSERT(this == &CWnd::wndBottom); else if (m_hWnd == HWND_TOPMOST) ASSERT(this == &CWnd::wndTopMost); else if (m_hWnd == HWND_NOTOPMOST) ASSERT(this == &CWnd::wndNoTopMost); else ...{ // should be a normal window ASSERT(::IsWindow(m_hWnd)); // should also be in the permanent or temporary handle map CHandleMap* pMap = afxMapHWND(); ASSERT(pMap != NULL); CObject* p; ASSERT((p = pMap->LookupPermanent(m_hWnd)) != NULL || (p = pMap->LookupTemporary(m_hWnd)) != NULL); ASSERT((CWnd*)p == this); // must be us // Note: if either of the above asserts fire and you are // writing a multithreaded application, it is likely that // you have passed a C++ object from one thread to another // and have used that object in a way that was not intended. // (only simple inline wrapper functions should be used) // // In general, CWnd objects should be passed by HWND from // one thread to another. The receiving thread can wrap // the HWND with a CWnd object by using CWnd::FromHandle. // // It is dangerous to pass C++ objects from one thread to // another, unless the objects are designed to be used in // such a manner. }}注意看上述代码的注释部分。由于CDocument::OnSaveDocument()函数进行了较高层封装,与线程相关,因此,CDocument::OnSaveDocument()函数需要重写。在重写的过程中,不能再继承CDocument::OnSaveDocument()函数,而需要从更低层着手写保存操作。
关于辅助工作线程的使用方法,我会在稍后的时间里贴出程序代码和注释,敬请关注!
-----------------------------------FAQ---------------------------------
Feedback
# re: 多线程编程之使用工作线程实现文档自动保存(I) 2006-08-15 17:04 Ying-Shen
如果前台线程改文档状态后台线程保存文档,这样会不会出现数据不一致导致文件保存不正确的情况?
# re: 多线程编程之使用工作线程实现文档自动保存(I) 2006-08-15 17:56 antoniozhou
@Ying-Shen
我明白你所说的情况,就是用户在后台辅助工作线程保存文档的过程中又对文档进行了操作,会不会导致文档内容上的错乱问题。这一点我也考虑过。我们也曾做过类似的实验,通常情况下,辅助工作线程保存一个我们自己的经过压缩存储后2.5MB大小(释放到内存中在40MB大小左右)的CAD零件文件所耗费的时间小于1秒。该实验零件其实是一个厂房的立体图,数据量比较庞大,绝大多数情况下在一秒的时间里很难有太大的操作,所以,上述保存方式还是可以适用的。另外,该CAD系统使用的是ACIS建模核心。不管是ACIS还是Parasolid核心,都有一个存放操作的BulletinBoard结构,这种结构是顺序记录的。因此,基于该BulletinBoard的文件保存也是顺序的,从而避免了上述情况的发生。
# re: 多线程编程之使用工作线程实现文档自动保存(I) 2006-08-15 18:04 antoniozhou
@antoniozhou
另外,在开发这一CAD系统自动保存功能的时候,我们也借鉴了其它具备类似功能的软件,所以采取的是临时文件覆盖保存法。这种方法虽然没有增量保存法迅速和快捷,并且获得的临时文件体积较大(和原文件一样大),但实现起来却比较简单。自动保存过程中,临时文件在每一次保存时都被重写,而工作线程则不会去修改原文件,这样,就避免了多线程对同一文件进行操作而带来的同步问题。
# re: 多线程编程之使用工作线程实现文档自动保存(I) 2006-08-15 22:41 Ying-Shen
@antoniozhou
如果修改数据的不是用户呢?不知道你们的系统中除了UI线程和辅助保存文档的线程还有没有其它的工作线程会修改文档。或者说某个UI的功能使用了Timer来定时/延迟更新文档?
我不是做CAD系统的所以也不太了解你说的BulletinBoard结构顺序记录能解决这个同步问题的原因,你能说说吗?
还有我想如果建模核心里已经做到了线程安全的话,那就没问题了,最坏情况下我们应该可以得到一个格式正确但是数据稍有错误的文件。
# re: 多线程编程之使用工作线程实现文档自动保存(I) 2006-08-15 23:41 antoniozhou
@Ying-Shen
在我们的CAD系统中,对真实的文档,只有UI线程可以进行修改,即使是自动保存辅助线程也不能对该文档进行修改。辅助线程只是对临时备份文件进行不断的更新,它并不能在UI线程占用用户文档的读写权限的时候对该文档进行任何操作。原因是由产生文档的CDocument的派生类,即与UI线程相关联的文档类在其封装之初就已经与UI线程形成依赖关系,所以,该类中能对用户文档进行操作的OnSaveDocument()函数不能被辅助线程调用,因此,也就不能实现存储操作。
将临时备份文件更新到用户文件只是一个简单的覆盖过程,而这一过程的发生也是需要在UI线程释放对其读写权限时才能实现。即,必须在用户关闭某一View窗口时才发生覆盖,否则,便只能采用手动保存更新用户文档。
关于BulletinBoard,则就像是我们通常用的BBS(BulletinBoards)系统,所有顺序创建的对象都被顺序记录在这一类结构对象中,这些对象都有一个指针。要使用哪个对象,就将其对象指针设为可用;反之,将其对象指针设为空。
是的,并不排除有这样的情况发生。