撰寫非同步方法
本主題描述如何在 Microsoft Media Foundation 中實作非同步方法。
非同步方法在媒體基礎管線中很普遍。 非同步方法可讓您更輕鬆地將工作分散到數個執行緒。 請務必以非同步方式執行 I/O,以便從檔案或網路讀取不會封鎖管線的其餘部分。
如果您要撰寫媒體來源或媒體接收,請務必正確處理非同步作業,因為您的元件效能會對整個管線造成影響。
注意
媒體基礎會 (MFT) 預設使用同步方法。
非同步作業的工作佇列
在媒體基礎中, 非同步回呼方法 與 工作佇列之間有密切的關聯性。 工作佇列是將工作從呼叫端執行緒移至背景工作執行緒的抽象概念。 若要在工作佇列上執行工作,請執行下列動作:
實作 IMFAsyncCallback 介面。
呼叫 MFCreateAsyncResult 以建立 結果 物件。 結果物件會公開 IMFAsyncResult。 結果物件包含三個指標:
- 呼叫端 之 IMFAsyncCallback 介面的指標。
- 狀態物件的選擇性指標。 如果指定,狀態物件必須實作 IUnknown。
- 私用物件的選擇性指標。 如果指定,這個物件也必須實作 IUnknown。
最後兩個指標可以是 Null。 否則,請使用它們來保存非同步作業的相關資訊。
呼叫 MFPutWorkItemEx 將佇列排入工作專案。
工作佇列執行緒會呼叫 您的 IMFAsyncCallback::Invoke 方法。
在 Invoke 方法內執行工作。 這個方法的 pAsyncResult 參數是步驟 2 中的 IMFAsyncResult 指標。 使用此指標來取得狀態物件和私用物件:
- 若要取得狀態物件,請呼叫 IMFAsyncResult::GetState。
- 若要取得私人物件,請呼叫 IMFAsyncResult::GetObject。
或者,您可以藉由呼叫 MFPutWorkItem 函式來結合步驟 2 和 3。 在內部,此函式會呼叫 MFCreateAsyncResult 來建立結果物件。
下圖顯示呼叫端、結果物件、狀態物件和私用物件之間的關聯性。
下列順序圖顯示物件如何將工作專案排入佇列。 當工作佇列執行緒呼叫 Invoke時,物件會在該執行緒上執行非同步作業。
請務必記住 ,叫 用是從工作佇列所擁有的執行緒呼叫。 您的 Invoke 實作必須是安全線程。 此外,如果您使用平臺工作佇列 (MFASYNC_CALLBACK_QUEUE_STANDARD) ,請務必不要封鎖執行緒,因為這樣可能會封鎖整個 Media Foundation 管線來處理資料。 如果您需要執行將會封鎖或花費很長的時間才能完成的作業,請使用私人工作佇列。 若要建立私人工作佇列,請呼叫 MFAllocateWorkQueue。 執行 I/O 作業的任何管線元件都應該避免因為相同原因而封鎖 I/O 呼叫。 IMFByteStream介面提供非同步檔案 I/O 的實用抽象概念。
正在實作 Begin.../End...模式
如呼叫非同步方法中所述,Media Foundation 中的非同步方法通常會使用Begin.../結束。。。。模式。 在此模式中,非同步作業會使用兩個簽章類似下列的方法:
// Starts the asynchronous operation.
HRESULT BeginX(IMFAsyncCallback *pCallback, IUnknown *punkState);
// Completes the asynchronous operation.
// Call this method from inside the caller's Invoke method.
HRESULT EndX(IMFAsyncResult *pResult);
若要讓方法真正非同步, BeginX 的實作必須在另一個執行緒上執行實際工作。 這是工作佇列進入圖片的位置。 在後續步驟中, 呼叫端 是呼叫 BeginX 和 EndX的程式碼。 這可能是應用程式或媒體基礎管線。 元件是實作BeginX和EndX的程式碼。
- 呼叫端會呼叫 Begin...,撥入電話端 之 IMFAsyncCallback 介面的指標。
- 元件會建立新的非同步結果物件。 此物件會儲存呼叫端的回呼介面和狀態物件。 一般而言,它也會儲存元件完成作業所需的任何私用狀態資訊。 此步驟的結果物件在下一張圖表中標示為「結果 1」。
- 元件會建立第二個結果物件。 此結果物件會儲存兩個指標:第一個結果物件,以及被呼叫端的回呼介面。 下圖中,此結果物件會標示為 「結果 2」。
- 元件會呼叫 MFPutWorkItemEx ,將新的工作專案排入佇列。
- 在 Invoke 方法中,元件會執行非同步工作。
- 元件會呼叫 MFInvokeCallback 以叫用呼叫端的回呼方法。
- 呼叫端會呼叫 EndX 方法。
非同步方法範例
為了說明此討論,我們將使用已討論的範例。 請考慮計算平方根的非同步方法:
HRESULT BeginSquareRoot(double x, IMFAsyncCallback *pCB, IUnknown *pState);
HRESULT EndSquareRoot(IMFAsyncResult *pResult, double *pVal);
的 BeginSquareRoot
x參數是將計算其平方根的值。 平方根會在 的 EndSquareRoot
pVal參數中傳回。
以下是實作這兩種方法之類別的宣告:
class SqrRoot : public IMFAsyncCallback
{
LONG m_cRef;
double m_sqrt;
HRESULT DoCalculateSquareRoot(AsyncOp *pOp);
public:
SqrRoot() : m_cRef(1)
{
}
HRESULT BeginSquareRoot(double x, IMFAsyncCallback *pCB, IUnknown *pState);
HRESULT EndSquareRoot(IMFAsyncResult *pResult, double *pVal);
// IUnknown methods.
STDMETHODIMP QueryInterface(REFIID riid, void **ppv)
{
static const QITAB qit[] =
{
QITABENT(SqrRoot, IMFAsyncCallback),
{ 0 }
};
return QISearch(this, qit, riid, ppv);
}
STDMETHODIMP_(ULONG) AddRef()
{
return InterlockedIncrement(&m_cRef);
}
STDMETHODIMP_(ULONG) Release()
{
LONG cRef = InterlockedDecrement(&m_cRef);
if (cRef == 0)
{
delete this;
}
return cRef;
}
// IMFAsyncCallback methods.
STDMETHODIMP GetParameters(DWORD* pdwFlags, DWORD* pdwQueue)
{
// Implementation of this method is optional.
return E_NOTIMPL;
}
// Invoke is where the work is performed.
STDMETHODIMP Invoke(IMFAsyncResult* pResult);
};
類別 SqrRoot
會實作 IMFAsyncCallback ,讓它可以將平方根作業放在工作佇列上。 方法是 DoCalculateSquareRoot
計算平方根的私用類別方法。 這個方法會從工作佇列執行緒呼叫。
首先,我們需要一種方式來儲存 x的值,以便在工作佇列執行緒呼叫 SqrRoot::Invoke
時擷取它。 以下是儲存資訊的簡單類別:
class AsyncOp : public IUnknown
{
LONG m_cRef;
public:
double m_value;
AsyncOp(double val) : m_cRef(1), m_value(val) { }
STDMETHODIMP QueryInterface(REFIID riid, void **ppv)
{
static const QITAB qit[] =
{
QITABENT(AsyncOp, IUnknown),
{ 0 }
};
return QISearch(this, qit, riid, ppv);
}
STDMETHODIMP_(ULONG) AddRef()
{
return InterlockedIncrement(&m_cRef);
}
STDMETHODIMP_(ULONG) Release()
{
LONG cRef = InterlockedDecrement(&m_cRef);
if (cRef == 0)
{
delete this;
}
return cRef;
}
};
這個類別會實作 IUnknown ,使其可以儲存在結果物件中。
下列程式碼會實作 BeginSquareRoot
方法:
HRESULT SqrRoot::BeginSquareRoot(double x, IMFAsyncCallback *pCB, IUnknown *pState)
{
AsyncOp *pOp = new (std::nothrow) AsyncOp(x);
if (pOp == NULL)
{
return E_OUTOFMEMORY;
}
IMFAsyncResult *pResult = NULL;
// Create the inner result object. This object contains pointers to:
//
// 1. The caller's callback interface and state object.
// 2. The AsyncOp object, which contains the operation data.
//
HRESULT hr = MFCreateAsyncResult(pOp, pCB, pState, &pResult);
if (SUCCEEDED(hr))
{
// Queue a work item. The work item contains pointers to:
//
// 1. The callback interface of the SqrRoot object.
// 2. The inner result object.
hr = MFPutWorkItem(MFASYNC_CALLBACK_QUEUE_STANDARD, this, pResult);
pResult->Release();
}
return hr;
}
此程式碼會執行以下動作:
- 建立 類別的新實例
AsyncOp
,以保存 x的值。 - 呼叫 MFCreateAsyncResult 來建立結果物件。 此物件會保存數個指標:
- 呼叫端 之 IMFAsyncCallback 介面的指標。
- 呼叫端狀態物件的指標, (pState) 。
-
AsyncOp
物件的指標。
- 呼叫 MFPutWorkItem 以將新的工作專案排入佇列。 此呼叫會隱含地建立外部結果物件,其中包含下列指標:
- 物件之 IMFAsyncCallback介面的
SqrRoot
指標。 - 步驟 2 中內部結果物件的指標。
- 物件之 IMFAsyncCallback介面的
下列程式碼會實作 SqrRoot::Invoke
方法:
// Invoke is called by the work queue. This is where the object performs the
// asynchronous operation.
STDMETHODIMP SqrRoot::Invoke(IMFAsyncResult* pResult)
{
HRESULT hr = S_OK;
IUnknown *pState = NULL;
IUnknown *pUnk = NULL;
IMFAsyncResult *pCallerResult = NULL;
AsyncOp *pOp = NULL;
// Get the asynchronous result object for the application callback.
hr = pResult->GetState(&pState);
if (FAILED(hr))
{
goto done;
}
hr = pState->QueryInterface(IID_PPV_ARGS(&pCallerResult));
if (FAILED(hr))
{
goto done;
}
// Get the object that holds the state information for the asynchronous method.
hr = pCallerResult->GetObject(&pUnk);
if (FAILED(hr))
{
goto done;
}
pOp = static_cast<AsyncOp*>(pUnk);
// Do the work.
hr = DoCalculateSquareRoot(pOp);
done:
// Signal the application.
if (pCallerResult)
{
pCallerResult->SetStatus(hr);
MFInvokeCallback(pCallerResult);
}
SafeRelease(&pState);
SafeRelease(&pUnk);
SafeRelease(&pCallerResult);
return S_OK;
}
這個方法會取得內部結果物件和 AsyncOp
物件。 然後將 物件傳遞 AsyncOp
至 DoCalculateSquareRoot
。 最後,它會呼叫 IMFAsyncResult::SetStatus 來設定狀態碼和 MFInvokeCallback 以叫用呼叫端的回呼方法。
方法 DoCalculateSquareRoot
會確實執行您預期的結果:
HRESULT SqrRoot::DoCalculateSquareRoot(AsyncOp *pOp)
{
pOp->m_value = sqrt(pOp->m_value);
return S_OK;
}
叫用呼叫端的回呼方法時,呼叫端必須負責呼叫 End... 方法,在此案例中為 EndSquareRoot
。
EndSquareRoot
是呼叫端擷取非同步作業結果的方式,在此範例中為計算平方根。 此資訊會儲存在結果物件中:
HRESULT SqrRoot::EndSquareRoot(IMFAsyncResult *pResult, double *pVal)
{
*pVal = 0;
IUnknown *pUnk = NULL;
HRESULT hr = pResult->GetStatus();
if (FAILED(hr))
{
goto done;
}
hr = pResult->GetObject(&pUnk);
if (FAILED(hr))
{
goto done;
}
AsyncOp *pOp = static_cast<AsyncOp*>(pUnk);
// Get the result.
*pVal = pOp->m_value;
done:
SafeRelease(&pUnk);
return hr;
}
作業佇列
到目前為止,無論物件的目前狀態為何,都已隱性地假設隨時可以完成非同步作業。 例如,假設應用程式在先前呼叫 BeginSquareRoot
相同方法時仍擱置時,會發生什麼情況。 類別 SqrRoot
可能會在上一個工作專案完成之前,將新的工作專案排入佇列。 不過,不保證工作佇列會序列化工作專案。 回想一下,工作佇列可以使用一個以上的執行緒來分派工作專案。 在多執行緒環境中,在上一個工作專案完成之前,可能會叫用工作專案。 工作專案甚至可以依序叫用,如果內容切換剛好在叫用回呼之前發生。
因此,如有需要,物件必須負責序列化本身的作業。 換句話說,如果物件需要作業A才能開始作業B,則物件在作業A完成之前,不得將工作專案排入佇列給B。 物件可以藉由有自己的擱置作業佇列來符合此需求。 在 物件上呼叫非同步方法時,物件會將要求放在自己的佇列上。 當每個非同步作業完成時,物件會從佇列提取下一個要求。 MPEG1Source 範例示範如何實作這類佇列的範例。
單一方法可能會牽涉到數個非同步作業,特別是使用 I/O 呼叫時。 當您實作非同步方法時,請仔細思考序列化需求。 例如,在先前的 I/O 要求仍在擱置中時,物件是否能夠啟動新的作業? 如果新的作業變更物件的內部狀態,當先前的 I/O 要求完成並傳回目前可能過時的資料時,會發生什麼情況? 良好的狀態圖表有助於識別有效的狀態轉換。
跨執行緒和跨進程考慮
工作佇列不會使用 COM 封送處理,跨執行緒界限封送處理介面指標。 因此,即使物件註冊為 Apartment 執行緒,或應用程式執行緒已進入單一執行緒 Apartment (STA) ,也會從不同的執行緒叫用 IMFAsyncCallback 回呼。 在任何情況下,所有媒體基礎管線元件都應該使用「兩者」執行緒模型。
Media Foundation 中的某些介面會定義某些非同步方法的遠端版本。 當跨進程界限呼叫其中一種方法時,Media Foundation Proxy/存根 DLL 會呼叫方法的遠端版本,以執行方法參數的自訂封送處理。 在遠端進程中,存根會將呼叫轉譯回 物件上的本機方法。 此程式對應用程式和遠端物件而言都是透明的。 這些自訂封送處理方法主要是針對載入受保護媒體路徑中的物件, (PMP) 。 如需 PMP 的詳細資訊,請參閱 受保護的媒體路徑。
相關主題