DAO(Data Access Objects)在多執行緒環境下有嚴格限制:DAO 物件必須在建立它的執行緒中使用,跨執行緒直接呼叫會導致未定義行為或崩潰。本文提出一種以 GUI 執行緒封送 為核心的解決方案,封裝 CMultiDaoRecordSet 基類,透過視窗訊息將背景執行緒的 DAO 操作請求轉發至主執行緒執行,確保執行緒親和性並簡化上層邏輯。
CDaoRecordset 會觸發 DAO 3609 錯誤或存取違規。CDaoDatabase/CDaoRecordset 無內建跨執行緒封送機制。sequenceDiagram
participant Worker as 背景執行緒
participant Main as GUI 主執行緒
participant DAO as CDaoRecordset
Worker->>Main: PostMessage(WM_MULTIDAOMESSAGE, cmd, pRecordset)
Main->>Main: OnMultiDaoMessage() 接收
Main->>DAO: 執行實際 DAO 操作 (Open/Edit/Update/Delete)
DAO-->>Main: 回傳結果/錯誤碼
Main->>Worker: PostThreadMessage / 事件通知完成
// MultiDaoRecordset.h
#define WM_MULTIDAOMESSAGE RegisterWindowMessage(_T("WM_MULTIDAOMESSAGE"))
enum MultiDaoCmd {
DAO_CMD_OPEN,
DAO_CMD_CLOSE,
DAO_CMD_MOVE_NEXT,
DAO_CMD_MOVE_PREV,
DAO_CMD_EDIT,
DAO_CMD_UPDATE,
DAO_CMD_DELETE,
DAO_CMD_ADDNEW,
DAO_CMD_FIND,
DAO_CMD_REQUERY
};
struct MultiDaoParam {
MultiDaoCmd cmd;
CMultiDaoRecordSet* pSet;
COleVariant var1, var2; // 通用參數
LONG lResult; // 執行結果
CString strError; // 錯誤訊息
HANDLE hEvent; // 同步事件(可選)
};
class CMultiDaoRecordSet : public CDaoRecordset
{
DECLARE_DYNAMIC(CMultiDaoRecordSet)
public:
CMultiDaoRecordSet(CDaoDatabase* pdb = NULL) : CDaoRecordset(pdb) {}
// 覆寫關鍵虛擬函式,攔截呼叫並轉發至 GUI 執行緒
virtual BOOL Open(int nOpenType = dbOpenDynaset, LPCTSTR lpszSQL = NULL, int nOptions = 0) {
return MarshalCall(DAO_CMD_OPEN, (LPARAM)lpszSQL, nOptions);
}
virtual void Close() { MarshalCall(DAO_CMD_CLOSE); }
virtual BOOL MoveNext() { return (BOOL)MarshalCall(DAO_CMD_MOVE_NEXT); }
virtual BOOL MovePrev() { return (BOOL)MarshalCall(DAO_CMD_MOVE_PREV); }
virtual void Edit() { MarshalCall(DAO_CMD_EDIT); }
virtual void Update() { MarshalCall(DAO_CMD_UPDATE); }
virtual void Delete() { MarshalCall(DAO_CMD_DELETE); }
virtual void AddNew() { MarshalCall(DAO_CMD_ADDNEW); }
protected:
LONG MarshalCall(MultiDaoCmd cmd, LPARAM param1 = 0, LPARAM param2 = 0) {
// 檢查是否已在 GUI 執行緒
if (AfxGetThread() == AfxGetApp()->m_pMainWnd->GetWindowThreadProcessId()) {
// 直接執行基類實作
return DirectCall(cmd, param1, param2);
}
// 封裝參數
MultiDaoParam params;
params.cmd = cmd;
params.pSet = this;
params.var1 = param1; params.var2 = param2;
params.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
// 發送至主視窗
::PostMessage(AfxGetMainWnd()->GetSafeHwnd(), WM_MULTIDAOMESSAGE, 0, (LPARAM)¶ms);
// 等待完成(可選:非同步模式則不等待,改用回調)
WaitForSingleObject(params.hEvent, INFINITE);
CloseHandle(params.hEvent);
if (!params.strError.IsEmpty())
AfxThrowDaoException(params.lResult, params.strError);
return params.lResult;
}
LONG DirectCall(MultiDaoCmd cmd, LPARAM p1, LPARAM p2) {
switch (cmd) {
case DAO_CMD_OPEN: return CDaoRecordset::Open((int)p1, (LPCTSTR)p2);
case DAO_CMD_CLOSE: CDaoRecordset::Close(); return 0;
case DAO_CMD_MOVE_NEXT: return CDaoRecordset::MoveNext();
// ... 其他命令
}
return 0;
}
};
IMPLEMENT_DYNAMIC(CMultiDaoRecordSet, CDaoRecordset)
// MainFrm.h
#define MAINFRAME_CPP
#include "MultiDaoRecordset.h"
UINT WM_MULTIDAOMESSAGE = RegisterWindowMessage(_T("WM_MULTIDAOMESSAGE"));
class CMainFrame : public CMDIFrameWnd {
// ...
afx_msg LONG OnMultiDaoMessage(UINT uParam, LONG lParam);
};
// MainFrm.cpp
BEGIN_MESSAGE_MAP(CMainFrame, CMDIFrameWnd)
ON_REGISTERED_MESSAGE(WM_MULTIDAOMESSAGE, OnMultiDaoMessage)
END_MESSAGE_MAP()
LONG CMainFrame::OnMultiDaoMessage(UINT uParam, LONG lParam)
{
MultiDaoParam* pParams = (MultiDaoParam*)lParam;
CMultiDaoRecordSet* pSet = pParams->pSet;
LONG lRet = 0;
CString strErr;
try {
switch (pParams->cmd) {
case DAO_CMD_OPEN:
lRet = pSet->CDaoRecordset::Open((int)pParams->var1.lVal,
(LPCTSTR)pParams->var1.pbstrVal,
(int)pParams->var2.lVal);
break;
case DAO_CMD_CLOSE:
pSet->CDaoRecordset::Close(); break;
case DAO_CMD_MOVE_NEXT:
lRet = pSet->CDaoRecordset::MoveNext(); break;
case DAO_CMD_MOVE_PREV:
lRet = pSet->CDaoRecordset::MovePrev(); break;
case DAO_CMD_EDIT:
pSet->CDaoRecordset::Edit(); break;
case DAO_CMD_UPDATE:
pSet->CDaoRecordset::Update(); break;
case DAO_CMD_DELETE:
pSet->CDaoRecordset::Delete(); break;
case DAO_CMD_ADDNEW:
pSet->CDaoRecordset::AddNew(); break;
// ... 其他命令
}
} catch (CDaoException* e) {
lRet = e->m_nAfxDaoError;
strErr = e->m_strError;
e->Delete();
} catch (CException* e) {
TCHAR buf[256]; e->GetErrorMessage(buf, 256);
strErr = buf; lRet = -1;
e->Delete();
}
pParams->lResult = lRet;
pParams->strError = strErr;
SetEvent(pParams->hEvent); // 通知呼叫執行緒完成
return 0;
}
// MySet.h
class CMySet : public CMultiDaoRecordSet {
DECLARE_DYNAMIC(CMySet)
public:
CMySet(CDaoDatabase* pdb = NULL) : CMultiDaoRecordSet(pdb) {}
// 宣告欄位...
};
// MySet.cpp
IMPLEMENT_DYNAMIC(CMySet, CMultiDaoRecordSet)
CMySet::CMySet(CDaoDatabase* pdb) : CMultiDaoRecordSet(pdb) {}
// 背景執行緒中安全使用
UINT WorkerThread(LPVOID pParam) {
CMySet set(&db);
set.Open(); // 自動封送至 GUI 執行緒
while (!set.IsEOF()) {
// 讀取欄位...
set.MoveNext(); // 自動封送
}
set.Close();
return 0;
}
同步等待 WaitForSingleObject 會阻塞背景執行緒。若需高併發,可改為:
PostMessage 後立即返回,不等待。PostThreadMessage 通知背景執行緒(需背景執行緒有訊息迴圈)。MultiDaoParam,主執行緒直接呼叫。MoveNext)效能差。建議批次讀取至記憶體結構,再由背景執行緒處理。CMultiDaoRecordSet* 必須確保生命週期覆蓋操作期間,避免懸空指標。_RecordsetPtr 支援自由執行緒)或 ODBC,避開 DAO 執行緒限制。CMultiDaoRecordSet 透過 RegisterWindowMessage 訊息封送,將 DAO 的單執行緒限制轉化為透明的跨執行緒調用。此模式適用於維護既有 DAO 專案、需在背景執行緒存取資料庫的情境。對於新開發,優先選擇 ADO/ODBC 可從架構層面消除此類問題。