MFC多線程編程注意事項

來源:互聯網
上載者:User

------ 轉自 http://hi.baidu.com/snownight/blog/item/dc4c0a5522bd24cdb645ae48.html

MFC多線程編程注意事項2009-08-24 09:56
 [轉帖]MFC多線程編程注意事項收藏

1.      表現——錯誤樣本
關於啟動線程時傳輸視窗對象(指標?控制代碼?)的問題:   

在選擇菜單中的開始線程後:   
void   cmainframe::onmenu_start()   
{   
...   
afxbeginthread(mythread,   this);   
...   
}   

線程函數如下:   
uint   mythread(lpvoid   pparam)   
{   
cmainframe*   pmainfrm   =   (cmainframe   *)pparam;   
...   
}   

問題一:   
這樣的代碼是不是有問題?   
(文檔中說線程間不能直接傳輸mfc對象的指標,應該通過傳輸控制代碼實現)   

問題二:   
這樣使用開始好像沒有問題,直接通過pmainfrm訪問視窗中的view都正常。   
但發現訪問狀態條時:   
pmainfrm->m_wndstatusbar.setpanetext(2,   "test);   
出現debug   assertion   failed!(在視窗線程中沒有問題)   
位置是wincore.cpp中的   
assert((p   =   pmap->lookuppermanent(m_hwnd))   !=   null   ||   
(p   =   pmap->lookuptemporary(m_hwnd))   !=   null);   
為什麼訪問view能正常,但訪問狀態條時不可以呢?   

問題三:   
如果通過傳輸控制代碼實現,怎樣做呢?   
我用下面的代碼執行時有問題:   
void   cmainframe::onmenu_start()   
{   
...   
hwnd   hwnd   =   getsafehwnd();   
afxbeginthread(mythread,   hwnd);   
...   
}   

uint   mythread(lpvoid   pparam)   
{   
cmainframe*   pmainfrm   =   (cmainframe   *)(cwnd::fromhandle((hwnd)pparam));   
...   
}   
執行時通過線程中得到pmainfrm,訪問其成員時不正常。 

網友:hewwatt
大致原因解釋如下:   
1.   mfc的大多數類不是安全執行緒的,cwnd及其訊息路由是其中之最   
2.   mfc介面類的大多數方法,最後都是通過sendmessage實現的,而訊息處理的   
過程中會引發其他訊息的發送及處理。如果訊息處理函數本身不是安全執行緒的   
你從背景工作執行緒中調用這些方法遲早會同你介面線程的使用者訊息響應發生衝突   
3.   cxxxx::fromhandle會根據調用者所線上程查表,如果查不到使用者建立的cxxxx   
對應對象,它會建立一個臨時對象出來。由於你在背景工作執行緒中調用該方法,當然   
不可能查到介面主線程中你所建立起來的那個對象了。這時mfc會你建立一個臨時   
對象並返回給你,你根本不可能期望它的成員變數會是有意義的。   所以要用   
也只能用cwnd::fromhandle,因為它只包含一個m_hwnd成員。   不過,要記住   
跨線程直接或間接地調用::sendmessage,通常都是行為不可預測的。   

2.        原因分析
MFC介面封裝類(多線程時成員函數調用的宣告失敗)
日期:2006-9-17 18:06:00     [Host01.Com] 
MFC介面封裝類
——多線程時成員函數調用的宣告失敗
經常在論壇上看到如下的問題:
DWORD WINAPI ThreadProc( void *pData ) // 線程函數(比如用於從COM口擷取資料)
{
// 資料擷取迴圈
// 資料獲得後放在變數i中
CAbcDialog *pDialog = reinterpret_cast< CAbcDialog* >( pData );
ASSERT( pDialog ); // 此處如果ASSERT_VALID( pDialog )將宣告失敗
pDialog-&gt;m_Data = i;
pDialog-&gt;UpdateData( FALSE ); // UpdateData內部ASSERT_VALID( this )宣告失敗

}
BOOL CAbcDialog::OnInitDialog()
{
CDialog::OnInitDialog();
// 其他初始化代碼
CreateThread( NULL, 0, ThreadProc, this, 0, NULL ); // 建立線程
return TRUE;
}
注意上面注釋中的兩處宣告失敗,本文從MFC底層的實現來解釋為什麼會宣告失敗,並說明MFC為什麼要這樣實現及相應的處理辦法。
在說明MFC介面封裝類的底層實現之前,由於其和視窗有關,故先講解視窗類別這個基礎知識以為後面做鋪墊。

視窗類別
視窗類別是一個結構,其一個執行個體代表著一個視窗類別型,與C++中的類的概念非常相近(雖然其表現形式完全不同,C++的類只不過是記憶體布局和其上的操作這個概念的類型),故被稱作為視窗類別。
視窗是具有裝置操作能力的邏輯概念,即一種能操作裝置(通常是顯示器)的東西。由於視窗是視窗類別的執行個體,就象C++中的一個類的執行個體,是可以具有成員函數 的(雖然表現形式不同),但一定要明確視窗的目的——操作裝置(這點也可以從Microsoft針對視窗所制訂的API的功能看出,主要出於對裝置操作的 方便)。因此不應因為其具有成員函數的功能而將視窗用於功能對象的建立,這雖然不錯,但是嚴重違反了語義的需要(關於語義,可參考我的另一篇文章——《語 義的需要》),是不提倡的,但卻由於MFC介面封裝類的加入導致大多數程式員經常將邏輯混入介面。
視窗類別是個結構,其中的大部分成員都沒什麼重要意義,只是Microsoft一相情願制訂的,如果不想使用介面API(Windows User Interface API),可以不管那些成員。其中只有一個成員是重要的——lpfnWndProc,訊息處理函數。
外界(使用視窗的代碼)只能通過訊息作業視窗,這就如同C++中編寫的具有良好的物件導向風格的類的執行個體只能通過其公用成員函數對其進行操作。因此訊息處 理函數就代表了一個視窗的一切(忽略視窗類別中其他成員的作用)。很容易發現,視窗這個執行個體只具有成員函數(訊息處理函數),不具有成員變數,即沒有一塊特 定記憶體和一特定的視窗相關聯,則視窗將不能具有狀態(Windows還是提供了Window Properties API來緩和這種狀況)。這也正是上面問題發生的根源。
為了處理視窗不能具有狀態的問題(這其實正是Windows靈活的表現),可以有很多種方法,而MFC出於能夠很容易的對已有視窗類別進行擴充,選擇了使用 一個映射將一個視窗控制代碼(視窗的唯一標示符)和一個記憶體塊進行綁定,而這塊記憶體塊就是我們熟知的MFC介面封裝類(從CWnd開始派生延續)的執行個體。

MFC狀態
狀態就是執行個體通過某種手段使得資訊可以跨時間段重現,C++的類的執行個體就是由外界通過公用成員函數改變執行個體的成員變數的值以實現具有狀態的效果。在MFC 中,具有三種狀態:模組狀態、進程狀態、線程狀態。分別為模組、進程和線程這三種執行個體的狀態。由於代碼是由線程運行,且和另外兩個的關係也很密切,因此也 被稱作本機資料。
模組本機資料
具有模組本地性的變數。模組指一個載入到進程虛擬記憶體空間中的PE檔案,即exe檔案本身和其載入的dll檔案。而模組本地性即同樣的指標,根據代碼從不 同的模組執行而訪問不同的記憶體空間。這其實只用每個模組都聲明一個全域變數,而前面的“代碼”就在MFC庫檔案中,然後通過一個切換的過程(將欲使用的模 塊的那個全域變數的地址賦給前述的指標)即可實現模組本地性。MFC中,這個過程是通過調用AfxSetModuleState來切換的,而通常都使用 AFX_MANAGE_STATE這個宏來處理,因此下面常見的語句就是用於模組狀態的切換的:
AFX_MANAGE_STATE( AfxGetStaticModuleState() );
MFC中定義了一個結構(AFX_MODULE_STATE),其執行個體具有模組本地性,記錄了此模組的全域應用程式物件指標、資源控制代碼等模組層級的全域變 量。其中有一個成員變數是執行緒區域資料,類型為AFX_MODULE_THREAD_STATE,其就是本文問題的關鍵。
進程本機資料
具有進程本地性的變數。與模組本地性相同,即同一個指標,在不同進程中指向不同的記憶體空間。這一點Windows本身的虛擬記憶體空間這個機制已經實現了, 不過在dll中定義的全域變數,如果dll支援Win32s,則其是共用其全域變數的,即不同的進程載入了同一dll將訪問同一記憶體。Win32s是為了 那些基於Win32的應用程式能在Windows 3.1上運行,由於Windows 3.1是16位作業系統,早已被淘汰,而現行的dll模型其本身就已經實現了進程本地性(不過還是可以通過共用節來實現Win32s中的dll的效果), 因此進程狀態其實就是一全域變數。
MFC中作為本機資料的結構有很多,如_AFX_WIN_STATE、_AFX_DEBUG_STATE、_AFX_DB_STATE等,都是MFC內部自己使用的具有進程本地性的全域變數。
執行緒區域資料
具有執行緒區域性的變數。如上,即同一個指標,不同的線程將會訪問不同的記憶體空間。這點MFC是通過執行緒區域儲存(TLS——Thread Local Storage,其使用方法由於與本文無關,在此不表)實現的。
MFC中定義了一個結構(_AFX_THREAD_STATE)以記錄某些線程級的全域變數,如最近一次的模組狀態指標,最近一次的訊息等。
模組線程狀態
MFC中定義的一個結構(AFX_MODULE_THREAD_STATE),其執行個體即具有執行緒區域性又具有模組本地性。也就是說不同的線程從同一模組中 和同一線程從不同模組中訪問MFC庫函數都將導致操作不同的記憶體空間。其應用在AFX_MODULE_STATE中,記錄一些線程相關但又模組層級的資料, 如本文的重點——視窗控制代碼映射。

封裝類對象和控制代碼映射
控制代碼映射——CHandleMap,MFC提供的一個底層輔助類,程式員是不應該直接使用它的。其有兩個重要的成員變數:CMapPtrToPtr m_permanentMap, m_temporaryMap;。分別記錄永久控制代碼綁定和臨時控制代碼綁定。前面說過,MFC使用一個映射將視窗控制代碼和其封裝類的執行個體綁定在一 起,m_permanentMap和m_temporaryMap就是這個映射,分別映射永久封裝類對象和臨時封裝類對象,而在前面提到過的 AFX_MODULE_THREAD_STATE中就有一個成員變數:CHandleMap* m_pmapHWND;(之所以是CHandleMap*是使用懶惰編程法,盡量節約資源)以專門完成HWND的綁定映射,除此以外還有如 m_pmapHDC、m_pmapHMENU等成員變數以分別實現HDC、HMENU的綁頂映射。而為什麼這些映射要放在模組線程狀態而不放線上程狀態或 模組狀態是很明顯的——這些封裝類封裝的控制代碼都是和線程相關的(如HWND只有建立它的線程才能接收其訊息)且這個模組中的封裝類對象可能不同於另一個模 塊的(如封裝類是某個DLL中專門派生的一個類,如a.dll中定義的CAButton的執行個體和b.dll中定義的CBButton的執行個體如果同時在一個 線程中。此時線程卸載了a.dll,然後CAButton的執行個體得到訊息並進行處理,將發生嚴重錯誤——類代碼已經被卸載掉了)。
封裝類存在的意 義有二:封裝對HWND的操作以加速代碼的編寫和提供視窗子類化(不是超類化)的效果以派生視窗類別。封裝類對象針對線程分為兩種:永久封裝類對象(以後簡 稱永久對象)和臨時封裝類對象(以後簡稱臨時對象)。臨時對象的意義僅僅只有封裝對HWND的操作以加速代碼編寫,不具有派生視窗類別的功能。永久對象則具 有前面說的封裝類的兩個意義。
在建立視窗時(即CWnd::CreateEx中),MFC通過鉤子提前(WM_CREATE和WM_NCCREATE之前)處理了通知,用 AfxWndProc子類化了建立的視窗並將對應的CWnd*加入當前線程的永久對象的映射中,而在AfxWndProc中,總是由 CWnd::FromHandlePermanent(獲得對應HWND的永久對象)得到當前線程中當前訊息所屬視窗控制代碼對應的永久對象,然後通過調用得 到的CWnd*的WindowProc成員函數來處理訊息以實現派生視窗類別的效果。這也就是說永久對象具有視窗子類化的意義,而不僅僅是封裝HWND的操 作。
要將一個HWND和一個已有的封裝類對象相關聯,調用CWnd::Attach將此封裝類對象和HWND映射成永久對象(但這種方法得到的永久對象不一定 具有子類化功能,很可能仍和臨時對象一樣,僅僅起封裝的目的)。如果想得到臨時對象,則通過CWnd::FromHandle這個靜態成員函數以獲得。臨 時對象之所以叫臨時,就是其是由MFC內部(CHandleMap::FromHandle)產生,其內部 (CHandleMap::DeleteTemp)銷毀(一般通過CWinThread::OnIdle中調用AfxUnlockTempMaps)。因 此程式員是永遠不應該試圖銷毀臨時對象的(即使臨時對象所屬線程沒有訊息迴圈,不能調用CwinThread::OnIdle,線上程結束 時,CHandleMap的析構仍然會銷毀臨時對象)。

原因
為什麼要分兩種封裝類對象?很好玩嗎?注意前面提過的視窗模型——只能通過訊息機制和視窗互動。注意,也就是說視窗是安全執行緒的執行個體。視窗過程的編寫中不 用考慮會有多個線程同時訪問視窗的狀態。如果不使用兩種封裝類對象,在視窗建立的鉤子中通過調用SetProp將建立的視窗控制代碼和對應的CWnd*綁定, 不一樣也可以實現前面說的視窗控制代碼和記憶體塊的綁定?
CWnd的衍生類別CA,具有一個成員變數m_BGColor以決定使用什麼顏色填充底背景。線程1建立了CA的一個執行個體a,將其指標傳進線程2,線程2設 置a.m_BGColor為紅色。這已經很明顯了,CA::m_BGColor不是安全執行緒的,如果不止一個線程2,那麼a.m_BGColor將會出現 線程存取違規。這嚴重違背視窗是安全執行緒的這個要求。因為使用了非訊息機制與視窗進行互動,所以失敗。
繼續,如果給CA一個公用成員函數SetBGColor,並在其中使用原子操作以保護m_BGColor,不就一切正常了?呵,在CA::OnPaint 中,會兩次使用m_BGColor進行繪圖,如果在兩次繪圖之間另一線程調用CA::SetBGColor改變了CA::m_BGColor,問題嚴重 了。也就是說不光是CA::m_BGColor的寫操作需要保護,讀操作亦需要保護,而這僅僅是一個成員變數。
那麼再繼續,完全按照視窗本身的定義,只使用訊息與它互動,也就是說自訂一個訊息,如AM_SETBGCOLOR,然後在CA::SetBGColor 中SendMessage這個訊息,並在其響應函數中修改CA::m_BGColor。完美了,這是即符合視窗概念又很好的設計,不過它要求每一個程式員 編寫每一個封裝類時都必須注意到這點,並且最重要的是,C++類的概念在這個設計中根本沒有發揮作用,嚴重地資源浪費。
因此,MFC決定要發揮C++類的概念的優勢,讓封裝類對象看起來就等同於視窗本身,因此使用了上面的兩種封裝類對象。讓封裝類對象隨線程的不同而不同可 以對封裝類對象進行線程保護,也就是說一個線程不可以也不應該訪問另一個線程中的封裝類對象(因為封裝類對象就相當於視窗,這是MFC的目標,並不是封裝 類本身不能被跨線程訪問),“不可以”就是通過在封裝類成員函數中的斷言宏實現的(在CWnd::AssertValid中),而“不應該”前面已經解釋 地很清楚了。因此本文開頭的宣告失敗的根本原因就是因為違反了“不可以”和“不應該”。
雖然封裝類對象不能跨線程訪問,但是視窗控制代碼卻可以跨線程訪問。因為封裝類對象不僅等同於視窗,還改變了視窗的互動方式(這也正是C++類的概念的應 用),使得不用非得使用訊息機制才能和視窗互動。注意前面提到的,如果跨線程訪問封裝類對象,而又使用C++類的概念操作它,則其必須進行線程保護,而“ 不能跨線程訪問”就消除了這個問題。因此臨時對象的產生就只是如前面所說,方便代碼的編寫而已,不提供子類化的效果,因為視窗控制代碼可以跨線程訪問。

解決辦法
已經瞭解失敗的原因,因此做如下修改:
DWORD WINAPI ThreadProc( void *pData ) // 線程函數(比如用於從COM口擷取資料)
{
// 資料擷取迴圈
// 資料獲得後放在變數i中
CAbcDialog *pDialog = static_cast< CAbcDialog* >(
CWnd::FromHandle( reinterpret_cast< HWND >( pData ) ) );
ASSERT_VALID( pDialog ); // 此處可能宣告失敗
pDialog-&gt;m_Data = i;      // 這是不好的設計,詳情可參看我的另一篇文章:《語義的需要》
pDialog-&gt;UpdateData( FALSE ); // UpdateData內部ASSERT_VALID( this )可能宣告失敗

}
BOOL CAbcDialog::OnInitDialog()
{
CDialog::OnInitDialog();
// 其他初始化代碼
CreateThread( NULL, 0, ThreadProc, m_hWnd, 0, NULL ); // 建立線程
return TRUE;
}
之所以是“可能”,因為這裡有個重點就是臨時對象是HWND操作的封裝,不是視窗類別的封裝。因此所有的HWND臨時對象都是CWnd的執行個體,即使上面強行 轉換為CAbcDialog*也依舊是CWnd*,所以在ASSERT_VALID裡調用CAbcDialog::AssertValid時,其定義了一 些附加檢查,則可能發現這是一個CWnd的執行個體而非一個CAbcDialog執行個體,導致宣告失敗。因此應將CAbcDialog全部換成CWnd,這下雖 然不宣告失敗了,但依舊錯誤(先不提pDialog-&gt;m_Data怎麼辦),因為臨時對象是HWND操作的封裝,而不幸的是 UpdateData 只是MFC自己提供的一個對話方塊資料交換的機制(DDX)的操作,其不是通過向HWND發送訊息來實現的,而是通過虛函數機制。因此在 UpdateData中調用執行個體的DoDataExchange將不能調用CAbcDialog::DoDataExchange,而是調用 CWnd::DoDataExchange,因此將不發生任何事。
因此合理(並不一定最好)的解決方案是向CAbcDialog的執行個體發送一個訊息,而通過一個中間變數(如一全域變數)來傳遞資料,而不是使用 CAbcDialog::m_Data。當然,如果資料少,比如本例,就應該將資料作為訊息參數進行傳遞,減少代碼的複雜性;資料多則應該通過全域變數傳 遞,減少了緩衝的管理費用。修改後如下:
#define AM_DATANOTIFY ( WM_USER + 1 )
static DWORD g_Data = 0;
DWORD WINAPI ThreadProc( void *pData ) // 線程函數(比如用於從COM口擷取資料)
{
// 資料擷取迴圈
// 資料獲得後放在變數i中
g_Data = i;
CWnd *pWnd = CWnd::FromHandle( reinterpret_cast< HWND >( pData ) );
ASSERT_VALID( pWnd ); // 本例應該直接調用平台SendMessage而不調用封裝類的,這裡只是示範
pWnd-&gt;SendMessage( AM_DATANOTIFY, 0, 0 );

}
BEGIN_MESSAGE_MAP( CAbcDialog, CDialog )

ON_MESSAGE( AM_DATANOTIFY, OnDataNotify )

END_MESSAGE_MAP()
BOOL CAbcDialog::OnInitDialog()
{
CDialog::OnInitDialog();
// 其他初始化代碼
CreateThread( NULL, 0, ThreadProc, m_hWnd, 0, NULL ); // 建立線程
return TRUE;
}
LRESULT CAbcDialog::OnDataNotify( WPARAM /* wParam */, LPARAM /* lParam */ )
{
UpdateData( FALSE );
return 0;
}
void CAbcDialog::DoDataExchange( CDataExchange *pDX )
{
CDialog::DoDataExchange( pDX );
DDX_Text( pDX, IDC_EDIT1, g_Data );
}

3.        注意事項
“安全執行緒”是一個什麼概念?
以 前常聽高手告誡MFC對象不要跨線程使用,因為MFC不是安全執行緒的。比如CWnd對象不要跨線程使用,可以用視窗控制代碼(HWND)代替。 CSocket/CAsyncSocket對象不要跨線程使用,用SOCKET控制代碼代替.那麼到底什麼是安全執行緒呢?什麼時候需要考慮?如果程式涉及到多 線程的話,就應該考慮安全執行緒問題。比如說設計的介面,將來需要在多線程環境中使用,或者需要跨線程使用某個對象時,這個就必須考慮了。關於安全執行緒也沒 什麼權威定義。在這裡我只說說我的理解:所提供的介面對於線程來說是原子操作或者多個線程之間的切換不會導致該介面的執行結果存在二義性,也就是說我們不 用考慮同步的問題。
一般而言“安全執行緒”由多線程對共用資源的訪問引起。如果調用某個介面時需要我們自己採取同步措施來保護該介面訪問的共用資源,則這樣的介面不是安全執行緒 的.MFC和STL都不是安全執行緒的. 怎樣才能設計出安全執行緒的類或者介面呢?如果介面中訪問的資料都屬於私人資料,那麼這樣的介面是安全執行緒的.或者幾個介面對共用資料都是唯讀操作,那麼這 樣的介面也是安全執行緒的.如果多個介面之間有共用資料,而且有讀有寫的話,如果設計者自己採取了同步措施,調用者不需要考慮資料同步問題,則這樣的介面是 安全執行緒的,否則不是安全執行緒的。

多線程的程式設計應該注意些什麼呢
1、盡量少的使用全域變數、static變數做共用 資料,盡量使用參數傳遞對象。被參數傳遞的對象,應該只包括必需的成員變數。所謂必需的成員變數,就是必定會被多線程操作的。很多人圖省事,會把 this指標(可能是任意一個對象指標)當作線程參數傳遞,致使線程內部有過多的操作許可權,對this中的參數任意妄為。整個程式由一個人完成,可能會非 常注意,不會出錯,但只要一轉手,程式就會面目全非。當兩個線程同時操作一個成員變數的時候,程式就開始崩潰了,更糟的是,這種錯誤很難被重現。(我就在 鬱悶這個問題,我們是幾個人,把程式編成debug版,經過數天使用,才找到錯誤。而找到錯誤只是開始,因為你要證明這個bug被修改成功了,也非常困 難。)其實,線程間資料互動大多是單向的,線上程回呼函數入口處,儘可能的將傳入的資料備份到局部變數中(當然,用於線程間通訊的變數不能這麼處理),以 後只對局部變數做處理,可以很好的解決這種問題。
2、在MFC中請慎用線程。因為MFC的架構假定你的訊息處理都是在主線程中完成的。首先視窗句 柄是屬於線程的,如果擁有視窗控制代碼的線程退出了,如果另一個線程處理這個視窗控制代碼,系統就會出現問題。而MFC為了避免這種情況的發生,使你在子線程中調 用訊息(視窗)處理函數時,就會不停的出Assert錯誤,煩都煩死你。典型的例子就時CSocket,因為CSocket是使用了一個隱藏視窗實現了假 阻塞,所以不可避免的使用了訊息處理函數,如果你在子線程中使用CSocket,你就可能看到assert的彈出了。
3、不要在不同的線程中同時 註冊COM組件。兩個線程,一個註冊1.ocx, 2.ocx, 3.ocx, 4.ocx; 而另一個則註冊5.ocx, 6.ocx, 7.ocx, 8.ocx,結果死結發生了,分別死在FreeLibrary和DllRegisterServer,因為這8個ocx是用MFC中做的,也可能是MFC 的Bug,但DllRegisterServer卻死在GetModuleFileName裡,而GetModuleFileName則是個API唉!如 果有過客看到,恰巧又知道其原因,請不吝賜教。
4、不要把線程搞的那麼複雜。很多初學者,恨不能用上線程相關的所有的函數,這裡互斥,那裡等待, 一會兒起線程,一會兒關線程的,比起goto語句有過之而無不及。好的多線程程式,應該是盡量少的使用線程。這句話怎麼理解呐,就是說盡量統一一塊資料共 享區存放資料隊列,工作子線程從隊列中取資料,處理,再放回資料,這樣才會模組化,對象化;而不是每個資料都起一個工作子線程處理,處理完了就關閉,寫的 時候雖然直接,等維護起來就累了。

轉自:http://hi.baidu.com/jumbo/blog/item/fd5d70f0e25130a7a40f52cd.html

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.