Share via


撰寫非同步方法

本主題描述如何在 Microsoft Media Foundation 中實作非同步方法。

非同步方法在媒體基礎管線中很普遍。 非同步方法可讓您更輕鬆地將工作分散到數個執行緒。 請務必以非同步方式執行 I/O,以便從檔案或網路讀取不會封鎖管線的其餘部分。

如果您要撰寫媒體來源或媒體接收,請務必正確處理非同步作業,因為您的元件效能會對整個管線造成影響。

注意

媒體基礎會 (MFT) 預設使用同步方法。

 

非同步作業的工作佇列

在媒體基礎中, 非同步回呼方法工作佇列之間有密切的關聯性。 工作佇列是將工作從呼叫端執行緒移至背景工作執行緒的抽象概念。 若要在工作佇列上執行工作,請執行下列動作:

  1. 實作 IMFAsyncCallback 介面。

  2. 呼叫 MFCreateAsyncResult 以建立 結果 物件。 結果物件會公開 IMFAsyncResult。 結果物件包含三個指標:

    • 呼叫端 之 IMFAsyncCallback 介面的指標。
    • 狀態物件的選擇性指標。 如果指定,狀態物件必須實作 IUnknown
    • 私用物件的選擇性指標。 如果指定,這個物件也必須實作 IUnknown

    最後兩個指標可以是 Null。 否則,請使用它們來保存非同步作業的相關資訊。

  3. 呼叫 MFPutWorkItemEx 將佇列排入工作專案。

  4. 工作佇列執行緒會呼叫 您的 IMFAsyncCallback::Invoke 方法。

  5. Invoke 方法內執行工作。 這個方法的 pAsyncResult 參數是步驟 2 中的 IMFAsyncResult 指標。 使用此指標來取得狀態物件和私用物件:

或者,您可以藉由呼叫 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 的實作必須在另一個執行緒上執行實際工作。 這是工作佇列進入圖片的位置。 在後續步驟中, 呼叫端 是呼叫 BeginXEndX的程式碼。 這可能是應用程式或媒體基礎管線。 元件是實作BeginXEndX的程式碼。

  1. 呼叫端會呼叫 Begin...,撥入電話端 之 IMFAsyncCallback 介面的指標。
  2. 元件會建立新的非同步結果物件。 此物件會儲存呼叫端的回呼介面和狀態物件。 一般而言,它也會儲存元件完成作業所需的任何私用狀態資訊。 此步驟的結果物件在下一張圖表中標示為「結果 1」。
  3. 元件會建立第二個結果物件。 此結果物件會儲存兩個指標:第一個結果物件,以及被呼叫端的回呼介面。 下圖中,此結果物件會標示為 「結果 2」。
  4. 元件會呼叫 MFPutWorkItemEx ,將新的工作專案排入佇列。
  5. Invoke 方法中,元件會執行非同步工作。
  6. 元件會呼叫 MFInvokeCallback 以叫用呼叫端的回呼方法。
  7. 呼叫端會呼叫 EndX 方法。

顯示物件如何實作開始/結束模式的圖表

非同步方法範例

為了說明此討論,我們將使用已討論的範例。 請考慮計算平方根的非同步方法:

    HRESULT BeginSquareRoot(double x, IMFAsyncCallback *pCB, IUnknown *pState);
    HRESULT EndSquareRoot(IMFAsyncResult *pResult, double *pVal);

BeginSquareRootx參數是將計算其平方根的值。 平方根會在 的 EndSquareRootpVal參數中傳回。

以下是實作這兩種方法之類別的宣告:

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;
}

此程式碼會執行以下動作:

  1. 建立 類別的新實例 AsyncOp ,以保存 x的值。
  2. 呼叫 MFCreateAsyncResult 來建立結果物件。 此物件會保存數個指標:
    • 呼叫端 之 IMFAsyncCallback 介面的指標。
    • 呼叫端狀態物件的指標, (pState) 。
    • AsyncOp 物件的指標。
  3. 呼叫 MFPutWorkItem 以將新的工作專案排入佇列。 此呼叫會隱含地建立外部結果物件,其中包含下列指標:

下列程式碼會實作 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 物件。 然後將 物件傳遞 AsyncOpDoCalculateSquareRoot 。 最後,它會呼叫 IMFAsyncResult::SetStatus 來設定狀態碼和 MFInvokeCallback 以叫用呼叫端的回呼方法。

方法 DoCalculateSquareRoot 會確實執行您預期的結果:

HRESULT SqrRoot::DoCalculateSquareRoot(AsyncOp *pOp)
{
    pOp->m_value = sqrt(pOp->m_value);

    return S_OK;
}

叫用呼叫端的回呼方法時,呼叫端必須負責呼叫 End... 方法,在此案例中為 EndSquareRootEndSquareRoot是呼叫端擷取非同步作業結果的方式,在此範例中為計算平方根。 此資訊會儲存在結果物件中:

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 的詳細資訊,請參閱 受保護的媒體路徑

非同步回呼方法

工作佇列