关于java线程
int?iCounter=0;//全局變量
DOWRD?threadA(void*?pD)
{
for(int?i=0;i<100;i++)
{
int?iCopy=iCounter;
//Sleep(1000);
iCopy++;
//Sleep(1000);
iCounter=iCopy;
}
}
現在假設有兩個線程threadA1和threadA2在同時運行那么運行結束后iCounter的值會是多少,是200嗎?不是的,如果我們將Sleep(1000)前的注釋去掉后我們會很容易明白這個問題,因為在iCounter的值被正確修改前它可能已經被其他的線程修改了。這個例子是一個將機器代碼操作放大的例子,因為在CPU內部也會經歷數據讀/寫的過程,而在線程執行的過程中線程可能被中斷而讓其他線程執行。變量iCounter在被第一個線程修改后,寫回內存前如果它又被第二個線程讀取,然后才被第一個線程寫回,那么第二個線程讀取的其實是錯誤的數據,這種情況就稱為臟讀(dirty?read)。這個例子同樣可以推廣到對文件,資源的使用上。?
那么要如何才能避免這一問題呢,假設我們在使用iCounter前向其他線程詢問一下:有誰在用嗎?如果沒被使用則可以立即對該變量進行操作,否則等其他線程使用完后再使用,而且在自己得到該變量的控制權后其他線程將不能使用這一變量,直到自己也使用完并釋放為止。經過修改的偽代碼如下:?
int?iCounter=0;//全局變量
DOWRD?threadA(void*?pD)
{
for(int?i=0;i<100;i++)
{
ask?to?lock?iCounter
wait?other?thread?release?the?lock
lock?successful
{
int?iCopy=iCounter;
//Sleep(1000);
iCopy++;
}
iCounter=iCopy;
release?lock?of?iCounter
}
}
幸運的是OS提供了多種同步對象供我們使用,并且可以替我們管理同步對象的加鎖和解鎖。我們需要做的就是對每個需要同步使用的資源產生一個同步對象,在使用該資源前申請加鎖,在使用完成后解鎖。接下來我們介紹一些同步對象:?
臨界區:臨界區是一種最簡單的同步對象,它只可以在同一進程內部使用。它的作用是保證只有一個線程可以申請到該對象,例如上面的例子我們就可以使用臨界區來進行同步處理。幾個相關的API函數為:?
VOID?InitializeCriticalSection(LPCRITICAL_SECTION?lpCriticalSection?);產生臨界區?
VOID?DeleteCriticalSection(LPCRITICAL_SECTION?lpCriticalSection?);刪除臨界區?
VOID?EnterCriticalSection(LPCRITICAL_SECTION?lpCriticalSection?);進入臨界區,相當于申請加鎖,如果該臨界區正被其他線程使用則該函數會等待到其他線程釋放?
BOOL?TryEnterCriticalSection(LPCRITICAL_SECTION?lpCriticalSection?);進入臨界區,相當于申請加鎖,和EnterCriticalSection不同如果該臨界區正被其他線程使用則該函數會立即返回FALSE,而不會等待?
VOID?LeaveCriticalSection(LPCRITICAL_SECTION?lpCriticalSection?);退出臨界區,相當于申請解鎖?
下面的示范代碼演示了如何使用臨界區來進行數據同步處理:?
//全局變量
int?iCounter=0;
CRITICAL_SECTION?criCounter;
DWORD?threadA(void*?pD)
{
int?iID=(int)pD;
for(int?i=0;i<8;i++)
{
EnterCriticalSection(&criCounter);
int?iCopy=iCounter;
Sleep(100);
iCounter=iCopy+1;
printf("thread?%d?:?%d\n",iID,iCounter);
LeaveCriticalSection(&criCounter);
}
return?0;
}
//in?main?function
{
//創建臨界區
InitializeCriticalSection(&criCounter);
//創建線程
HANDLE?hThread[3];
CWinThread*?pT1=AfxBeginThread((AFX_THREADPROC)threadA,(void*)1);
CWinThread*?pT2=AfxBeginThread((AFX_THREADPROC)threadA,(void*)2);
CWinThread*?pT3=AfxBeginThread((AFX_THREADPROC)threadA,(void*)3);
hThread[0]=pT1->m_hThread;
hThread[1]=pT2->m_hThread;
hThread[2]=pT3->m_hThread;
//等待線程結束
//至于WaitForMultipleObjects的用法后面會講到。
WaitForMultipleObjects(3,hThread,TRUE,INFINITE);
//刪除臨界區
DeleteCriticalSection(&criCounter);
printf("\nover\n");
}
接下來要講互斥量與臨界區的作用非常相似,但互斥量是可以命名的,也就是說它可以跨越進程使用。所以創建互斥量需要的資源更多,所以如果只為了在進程內部是用的話使用臨界區會帶來速度上的優勢并能夠減少資源占用量。因為互斥量是跨進程的互斥量一旦被創建,就可以通過名字打開它。下面介紹可以用在互斥量上的API函數:?
創建互斥量:
HANDLE?CreateMutex(
??LPSECURITY_ATTRIBUTES?lpMutexAttributes,//?安全信息
??BOOL?bInitialOwner,??//?最初狀態,
??//如果設置為真,則表示創建它的線程直接擁有了該互斥量,而不需要再申請
??LPCTSTR?lpName???????//?名字,可以為NULL,但這樣一來就不能被其他線程/進程打開
);
打開一個存在的互斥量:
HANDLE?OpenMutex(
??DWORD?dwDesiredAccess,??//?存取方式
??BOOL?bInheritHandle,????//?是否可以被繼承
??LPCTSTR?lpName??????????//?名字
);
釋放互斥量的使用權,但要求調用該函數的線程擁有該互斥量的使用權:
BOOL?ReleaseMutex(//作用如同LeaveCriticalSection
??HANDLE?hMutex???//?句柄
);
關閉互斥量:
BOOL?CloseHandle(
??HANDLE?hObject???//?句柄
);
你會說為什么沒有名稱如同EnterMutex,功能如同EnterCriticalSection一樣的函數來獲得互斥量的使用權呢?的確沒有!獲取互斥量的使用權需要使用函數:?
DWORD?WaitForSingleObject(
??HANDLE?hHandle,????????//?等待的對象的句柄
??DWORD?dwMilliseconds???//?等待的時間,以ms為單位,如果為INFINITE表示無限期的等待
);
返回:
WAIT_ABANDONED?在等待的對象為互斥量時表明因為互斥量被關閉而變為有信號狀態
WAIT_OBJECT_0?得到使用權
WAIT_TIMEOUT?超過(dwMilliseconds)規定時間
在線程調用WaitForSingleObject后,如果一直無法得到控制權線程講被掛起,直到超過時間或是獲得控制權。?
講到這里我們必須更深入的講一下WaitForSingleObject函數中的對象(Object)的含義,這里的對象是一個具有信號狀態的對象,對象有兩種狀態:有信號/無信號。而等待的含義就在于等待對象變為有信號的狀態,對于互斥量來講如果正在被使用則為無信號狀態,被釋放后變為有信號狀態。當等待成功后WaitForSingleObject函數會將互斥量置為無信號狀態,這樣其他的線程就不能獲得使用權而需要繼續等待。WaitForSingleObject函數還進行排隊功能,保證先提出等待請求的線程先獲得對象的使用權,下面的代碼演示了如何使用互斥量來進行同步,代碼的功能還是進行全局變量遞增,通過輸出結果可以看出,先提出請求的線程先獲得了控制權:?
int?iCounter=0;
DWORD?threadA(void*?pD)
{
int?iID=(int)pD;
//在內部重新打開
HANDLE?hCounterIn=OpenMutex(MUTEX_ALL_ACCESS,FALSE,"sam?sp?44");
for(int?i=0;i<8;i++)
{
printf("%d?wait?for?object\n",iID);
WaitForSingleObject(hCounterIn,INFINITE);
int?iCopy=iCounter;
Sleep(100);
iCounter=iCopy+1;
printf("\t\tthread?%d?:?%d\n",iID,iCounter);
ReleaseMutex(hCounterIn);
}
CloseHandle(hCounterIn);
return?0;
}
//in?main?function
{
//創建互斥量
HANDLE?hCounter=NULL;
if(?(hCounter=OpenMutex(MUTEX_ALL_ACCESS,FALSE,"sam?sp?44"))==NULL)
{
//如果沒有其他進程創建這個互斥量,則重新創建
hCounter?=?CreateMutex(NULL,FALSE,"sam?sp?44");
}
//創建線程
HANDLE?hThread[3];
CWinThread*?pT1=AfxBeginThread((AFX_THREADPROC)threadA,(void*)1);
CWinThread*?pT2=AfxBeginThread((AFX_THREADPROC)threadA,(void*)2);
CWinThread*?pT3=AfxBeginThread((AFX_THREADPROC)threadA,(void*)3);
hThread[0]=pT1->m_hThread;
hThread[1]=pT2->m_hThread;
hThread[2]=pT3->m_hThread;
//等待線程結束
WaitForMultipleObjects(3,hThread,TRUE,INFINITE);
//關閉句柄
CloseHandle(hCounter);
}
}
在這里我沒有使用全局變量來保存互斥量句柄,這并不是因為不能這樣做,而是為演示如何在其他的代碼段中通過名字來打開已經創建的互斥量。其實這個例子在邏輯上是有一點錯誤的,因為iCounter這個變量沒有跨進程使用,所以沒有必要使用互斥量,只需要使用臨界區就可以了。假設有一組進程在同時使用一個文件那么我們可以使用互斥量來保證該文件只同時被一個進程使用(如果只是利用OS的文件存取控制功能則需要添加更多的錯誤處理代碼),此外在調度程序中也可以使用互斥量來對資源的使用進行同步化。
現在我們回過頭來講WaitForSingleObject這個函數,從前面的例子中我們看到WaitForSingleObject這個函數將等待一個對象變為有信號狀態,那么具有信號狀態的對象有哪些呢?下面是一部分:?
Mutex?
Event?
Semaphore?
Job?
Process?
Thread?
Waitable?timer?
Console?input?
互斥量(Mutex),信號燈(Semaphore),事件(Event)都可以被跨越進程使用來進行同步數據操作,而其他的對象與數據同步操作無關,但對于進程和線程來講,如果進程和線程在運行狀態則為無信號狀態,在退出后為有信號狀態。所以我們可以使用WaitForSingleObject來等待進程和線程退出。(至于信號燈,事件的用法我們接下來會講)我們在前面的例子中使用了WaitForMultipleObjects函數,這個函數的作用與WaitForSingleObject類似但從名字上我們可以看出,WaitForMultipleObjects將用于等待多個對象變為有信號狀態,函數原型如下:?
DWORD?WaitForMultipleObjects(
??DWORD?nCount,?????????????//?等待的對象數量
??CONST?HANDLE?*lpHandles,??//?對象句柄數組指針
??BOOL?fWaitAll,????????????//?等待方式,
??//為TRUE表示等待全部對象都變為有信號狀態才返回,為FALSE表示任何一個對象變為有信號狀態則返回
??DWORD?dwMilliseconds??????//?超時設置,以ms為單位,如果為INFINITE表示無限期的等待
);
返回值意義:
WAIT_OBJECT_0?到?(WAIT_OBJECT_0?+?nCount?–?1):當fWaitAll為TRUE時表示所有對象變為有信號狀態,當fWaitAll為FALSE時使用返回值減去WAIT_OBJECT_0得到變為有信號狀態的對象在數組中的下標。
WAIT_ABANDONED_0?到?(WAIT_ABANDONED_0?+?nCount?–?1):當fWaitAll為TRUE時表示所有對象變為有信號狀態,當fWaitAll為FALSE時表示對象中有一個對象為互斥量,該互斥量因為被關閉而成為有信號狀態,使用返回值減去WAIT_OBJECT_0得到變為有信號狀態的對象在數組中的下標。
WAIT_TIMEOUT:表示超過規定時間。?
前面的例子中的如下代碼表示等待三個線程都變為有信號狀態,也就是說三個線程都結束。
HANDLE?hThread[3];
CWinThread*?pT1=AfxBeginThread((AFX_THREADPROC)threadA,(void*)1);
CWinThread*?pT2=AfxBeginThread((AFX_THREADPROC)threadA,(void*)2);
CWinThread*?pT3=AfxBeginThread((AFX_THREADPROC)threadA,(void*)3);
hThread[0]=pT1->m_hThread;
hThread[1]=pT2->m_hThread;
hThread[2]=pT3->m_hThread;
//等待線程結束
WaitForMultipleObjects(3,hThread,TRUE,INFINITE);
此外,在啟動和等待進程結束一文中就利用這個功能等待進程結束。
通過互斥量我們可以指定資源被獨占的方式使用,但如果有下面一種情況通過互斥量就無法處理,比如現在一位用戶購買了一份三個并發訪問許可的數據庫系統,你的老板會要求你根據用戶購買的訪問許可數量來決定有多少個線程/進程能同時進行數據庫操作,這時候如果利用互斥量就沒有辦法完成這個要求,信號燈對象可以說是一種資源計數器。對信號燈的操作偽代碼大致如下:?
Semaphore?sem=3;
dword?threadA(void*)
{
while(sem?<=?0)
{//?相當于?WaitForSingleObject
wait?...
}
//?sem?>?0
//?lock?the?Semaphore
sem?--?;
do?functions?...
//?release?Semaphore
sem?++?;
return?0;
}
這里信號燈有一個初始值,表示有多少進程/線程可以進入,當信號燈的值大于0時為有信號狀態,小于等于0時為無信號狀態,所以可以利用WaitForSingleObject進行等待,當WaitForSingleObject等待成功后信號燈的值會被減少1,直到釋放時信號燈會被增加1。用于信號燈操作的API函數有下面這些:?
創建信號燈:
HANDLE?CreateSemaphore(
??LPSECURITY_ATTRIBUTES?lpSemaphoreAttributes,//?安全屬性,NULL表示使用默認的安全描述
??LONG?lInitialCount,??//?初始值
??LONG?lMaximumCount,??//?最大值
??LPCTSTR?lpName???????//?名字
);
打開信號燈:
HANDLE?OpenSemaphore(
??DWORD?dwDesiredAccess,??//?存取方式
??BOOL?bInheritHandle,????//?是否能被繼承
??LPCTSTR?lpName??????????//?名字
);
釋放信號燈:
BOOL?ReleaseSemaphore(
??HANDLE?hSemaphore,???//?句柄
??LONG?lReleaseCount,??//?釋放數,讓信號燈值增加數
??LPLONG?lpPreviousCount???//?用來得到釋放前信號燈的值,可以為NULL
);
關閉信號燈:
BOOL?CloseHandle(
??HANDLE?hObject???//?句柄
);
可以看出來信號燈的使用方式和互斥量的使用方式非常相似,下面的代碼使用初始值為2的信號燈來保證只有兩個線程可以同時進行數據庫調用:?
DWORD?threadA(void*?pD)
{
int?iID=(int)pD;
//在內部重新打開
HANDLE?hCounterIn=OpenSemaphore(SEMAPHORE_ALL_ACCESS,FALSE,"sam?sp?44");
for(int?i=0;i<3;i++)
{
printf("%d?wait?for?object\n",iID);
WaitForSingleObject(hCounterIn,INFINITE);
printf("\t\tthread?%d?:?do?database?access?call\n",iID);
Sleep(100);
printf("\t\tthread?%d?:?do?database?access?call?end\n",iID);
ReleaseSemaphore(hCounterIn,1,NULL);
}
CloseHandle(hCounterIn);
return?0;
}
//in?main?function
{
//創建信號燈
HANDLE?hCounter=NULL;
if(?(hCounter=OpenSemaphore(SEMAPHORE_ALL_ACCESS,FALSE,"sam?sp?44"))==NULL)
{
//如果沒有其他進程創建這個信號燈,則重新創建
hCounter?=?CreateSemaphore(NULL,2,2,"sam?sp?44");
}
//創建線程
HANDLE?hThread[3];
CWinThread*?pT1=AfxBeginThread((AFX_THREADPROC)threadA,(void*)1);
CWinThread*?pT2=AfxBeginThread((AFX_THREADPROC)threadA,(void*)2);
CWinThread*?pT3=AfxBeginThread((AFX_THREADPROC)threadA,(void*)3);
hThread[0]=pT1->m_hThread;
hThread[1]=pT2->m_hThread;
hThread[2]=pT3->m_hThread;
//等待線程結束
WaitForMultipleObjects(3,hThread,TRUE,INFINITE);
//關閉句柄
CloseHandle(hCounter);
}
信號燈有時用來作為計數器使用,一般來講將其初始值設置為0,先調用ReleaseSemaphore來增加其計數,然后使用WaitForSingleObject來減小其計數,遺憾的是通常我們都不能得到信號燈的當前值,但是可以通過設置WaitForSingleObject的等待時間為0來檢查信號燈當前是否為0。?
接下來我們講最后一種同步對象:事件,前面講的信號燈和互斥量可以保證資源被正常的分配和使用,而事件是用來通知其他進程/線程某件操作已經完成。例如:現在有三個線程:threadA,threadB,threadC,現在要求他們中的部分功能要順序執行,也就是說threadA執行完一部分后threadB執行,threadB執行完一部分后threadC開始執行。也許你覺得下面的代碼可以滿足要求:?
要求:A1執行完后執行B2然后執行C3,再假設每個任務的執行時間都為1,而且允許并發操作。
方案一:
dword?threadA(void*)
{
do?something?A1;
create?threadB;
do?something?A2;
do?something?A3;
}
dword?threadB(void*)
{
do?something?B1;
do?something?B2;
create?threadC;
do?something?B3;
}
dword?threadC(void*)
{
do?something?C1;
do?something?C2;
do?something?C3;
}
方案二:
dword?threadA(void*)
{
do?something?A1;
do?something?A2;
do?something?A3;
}
dword?threadB(void*)
{
do?something?B1;
wait?for?threadA?end
do?something?B2;
do?something?B3;
}
dword?threadC(void*)
{
do?something?C1;
do?something?C2;
wait?for?threadB?end
do?something?C3;
}
main()
{
create?threadA;
create?threadB;
create?threadC;
}
方案三:
dword?threadA(void*)
{
do?something?A1;
release?event1;
do?something?A2;
do?something?A3;
}
dword?threadB(void*)
{
do?something?B1;
wait?for?envet1?be?released
do?something?B2;
release?event2;
do?something?B3;
}
dword?threadC(void*)
{
do?something?C1;
do?something?C2;
wait?for?event2?be?released
do?something?C3;
}
main()
{
create?threadA;
create?threadB;
create?threadC;
}
比較一下三種方案的執行時間:
?????????方案一????????????????????????方案二??????????????????????方案三
1?threadA??threadB??threadC????threadA??threadB??threadC???threadA??threadB??threadC
2????A1???????????????????????????A1????????B1???????C1???????A1???????B1???????C1
3????A2??????B1???????????????????A2?????????????????C2???????A2???????B2???????C2
4????A1??????B2???????????????????A3??????????????????????????A3???????B3???????C3
5????????????B3?????????C1??????????????????B2??????
6???????????????????????C2??????????????????B3????????
7???????????????????????C3???????????????????????????C3
8???????????????????????????????????????????????????
可以看出來方案三的執行時間是最短的,當然這個例子有些極端,但我們可以看出事件對象用于通知其他進程/線程某件操作已經完成方面的作用是很大的,而且如果有的任務要在進程尖進行協調采用等待其他進程中線程結束的方式是不可能實現的。此外我也希望通過這個例子講一點關于分析線程執行效率的方法。
一兩種方式創建,一種為自動重置,在其他線程使用WaitForSingleObject等待到事件對象變為有信號后該事件對象自動又變為無信號狀態,一種為人工重置在其他線程使用WaitForSingleObject等待到事件對象變為有信號后該事件對象狀態不變。例如有多個線程都在等待一個線程運行結束,我們就可以使用人工重置事件,在被等待的線程結束時設置該事件為有信號狀態,這樣其他的多個線程對該事件的等待都會成功(因為該事件的狀態不會被自動重置)。事件相關的API如下:?
創建事件對象:
HANDLE?CreateEvent(
??LPSECURITY_ATTRIBUTES?lpEventAttributes,//?安全屬性,NULL表示使用默認的安全描述
??BOOL?bManualReset,??//?是否為人工重置
??BOOL?bInitialState,?//?初始狀態是否為有信號狀態
??LPCTSTR?lpName??????//?名字
);
打開事件對象:
HANDLE?OpenEvent(
??DWORD?dwDesiredAccess,??//?存取方式
??BOOL?bInheritHandle,????//?是否能夠被繼承
??LPCTSTR?lpName??????????//?名字
);
設置事件為無信號狀態:
BOOL?ResetEvent(
??HANDLE?hEvent???//?句柄
);
設置事件有無信號狀態:
BOOL?SetEvent(
??HANDLE?hEvent???//?句柄
);
關閉事件對象:
BOOL?CloseHandle(
??HANDLE?hObject???//?句柄
);
下面的代碼演示了自動重置和人工重置事件在使用中的不同效果:?
DWORD?threadA(void*?pD)
{
int?iID=(int)pD;
//在內部重新打開
HANDLE?hCounterIn=OpenEvent(EVENT_ALL_ACCESS,FALSE,"sam?sp?44");
printf("\tthread?%d?begin\n",iID);
//設置成為有信號狀態
Sleep(1000);
SetEvent(hCounterIn);
Sleep(1000);
printf("\tthread?%d?end\n",iID);
CloseHandle(hCounterIn);
return?0;
}
DWORD?threadB(void*?pD)
{//等待threadA結束后在繼續執行
int?iID=(int)pD;
//在內部重新打開
HANDLE?hCounterIn=OpenEvent(EVENT_ALL_ACCESS,FALSE,"sam?sp?44");
if(WAIT_TIMEOUT?==?WaitForSingleObject(hCounterIn,10*1000))
{
printf("\t\tthread?%d?wait?time?out\n",iID);
}
else
{
printf("\t\tthread?%d?wait?ok\n",iID);
}
CloseHandle(hCounterIn);
return?0;
}
//in?main?function
{
HANDLE?hCounter=NULL;
if(?(hCounter=OpenEvent(EVENT_ALL_ACCESS,FALSE,"sam?sp?44"))==NULL)
{
//如果沒有其他進程創建這個事件,則重新創建,該事件為人工重置事件
hCounter?=?CreateEvent(NULL,TRUE,FALSE,"sam?sp?44");
}
//創建線程
HANDLE?hThread[3];
printf("test?of?manual?rest?event\n");
CWinThread*?pT1=AfxBeginThread((AFX_THREADPROC)threadA,(void*)1);
CWinThread*?pT2=AfxBeginThread((AFX_THREADPROC)threadB,(void*)2);
CWinThread*?pT3=AfxBeginThread((AFX_THREADPROC)threadB,(void*)3);
hThread[0]=pT1->m_hThread;
hThread[1]=pT2->m_hThread;
hThread[2]=pT3->m_hThread;
//等待線程結束
WaitForMultipleObjects(3,hThread,TRUE,INFINITE);
//關閉句柄
CloseHandle(hCounter);
if(?(hCounter=OpenEvent(EVENT_ALL_ACCESS,FALSE,"sam?sp?44"))==NULL)
{
//如果沒有其他進程創建這個事件,則重新創建,該事件為自動重置事件
hCounter?=?CreateEvent(NULL,FALSE,FALSE,"sam?sp?44");
}
//創建線程
printf("test?of?auto?rest?event\n");
pT1=AfxBeginThread((AFX_THREADPROC)threadA,(void*)1);
pT2=AfxBeginThread((AFX_THREADPROC)threadB,(void*)2);
pT3=AfxBeginThread((AFX_THREADPROC)threadB,(void*)3);
hThread[0]=pT1->m_hThread;
hThread[1]=pT2->m_hThread;
hThread[2]=pT3->m_hThread;
//等待線程結束
WaitForMultipleObjects(3,hThread,TRUE,INFINITE);
//關閉句柄
CloseHandle(hCounter);
}
從執行結果中我們可以看到在第二次執行時由于使用了自動重置事件threadB中只有一個線程能夠等待到threadA中釋放的事件對象。?
在處理多進程/線程的同步問題時必須要小心避免發生死鎖問題,比如說現在有兩個互斥量A和B,兩個線程tA和tB,他們在執行前都需要得到這兩個互斥量,但現在這種情況發生了,tA擁有了互斥量A,tB擁有了互斥量B,但它們同時都在等待擁有另一個互斥量,這時候顯然誰也不可能得到自己希望的資源。這種互相擁有對方所擁有的資源而且都在等待對方擁有的資源的情況就稱為死鎖。關于這個問題更詳細的介紹請參考其他參考書。?
在MFC中對于各種同步對象都提供了相對應的類
在這些類中封裝了上面介紹的對象創建,打開,控制,刪除功能。但是如果要使用等待功能則需要使用另外兩個類:CSingleLock和CMultiLock。這兩個類中封裝了WaitForSingleObject和WaitForMultipleObjects函數。如果大家覺的需要可以看看這些類的定義,我想通過上面的介紹可以很容易理解,但是在對象同步問題上我覺得使用API函數比使用MFC類更為直觀和方便。
總結
- 上一篇: 数据仓库的ETL、OLAP和BI应用
- 下一篇: 程序员真正的天赋是什么?