Escrevendo um método assíncrono

Este tópico descreve como implementar um método assíncrono no Microsoft Media Foundation.

Métodos assíncronos são onipresentes no pipeline da Media Foundation. Métodos assíncronos facilitam a distribuição do trabalho entre vários threads. É particularmente importante executar E/S de forma assíncrona, para que a leitura de um arquivo ou rede não bloqueie o restante do pipeline.

Se você estiver escrevendo uma fonte de mídia ou coletor de mídia, é crucial lidar com operações assíncronas corretamente, pois o desempenho do componente tem um impacto em todo o pipeline.

Observação

As MFTs (transformações do Media Foundation) usam métodos síncronos por padrão.

 

Filas de trabalho para operações assíncronas

No Media Foundation, há uma relação próxima entre métodos de retorno de chamada assíncronos e filas de trabalho. Uma fila de trabalho é uma abstração para mover o trabalho do thread do chamador para um thread de trabalho. Para executar o trabalho em uma fila de trabalho, faça o seguinte:

  1. Implemente a interface IMFAsyncCallback .

  2. Chame MFCreateAsyncResult para criar um objeto de resultado . O objeto de resultado expõe o IMFAsyncResult. O objeto de resultado contém três ponteiros:

    • Um ponteiro para a interface IMFAsyncCallback do chamador.
    • Um ponteiro opcional para um objeto de estado. Se especificado, o objeto state deve implementar IUnknown.
    • Um ponteiro opcional para um objeto privado. Se especificado, esse objeto também deve implementar IUnknown.

    Os dois últimos ponteiros podem ser NULL. Caso contrário, use-os para armazenar informações sobre a operação assíncrona.

  3. Chame MFPutWorkItemEx para fazer fila para o item de trabalho.

  4. O thread da fila de trabalho chama o método IMFAsyncCallback::Invoke .

  5. Faça o trabalho dentro do método Invoke . O parâmetro pAsyncResult desse método é o ponteiro IMFAsyncResult da etapa 2. Use este ponteiro para obter o objeto de estado e o objeto privado:

Como alternativa, você pode combinar as etapas 2 e 3 chamando a função MFPutWorkItem . Internamente, essa função chama MFCreateAsyncResult para criar o objeto de resultado.

O diagrama a seguir mostra as relações entre o chamador, o objeto de resultado, o objeto de estado e o objeto privado.

diagrama mostrando um objeto de resultado assíncrono

O diagrama de sequência a seguir mostra como um objeto enfileira um item de trabalho. Quando o thread da fila de trabalho chama Invoke, o objeto executa a operação assíncrona nesse thread.

diagrama mostrando como um objeto enfileira um item de trabalho

É importante lembrar que Invoke é chamado de um thread que pertence à fila de trabalho. Sua implementação de Invoke deve ser thread-safe. Além disso, se você usar a fila de trabalho da plataforma (MFASYNC_CALLBACK_QUEUE_STANDARD), é essencial que você nunca bloqueie o thread, pois isso pode impedir que todo o pipeline do Media Foundation processe dados. Se você precisar executar uma operação que bloqueará ou levará muito tempo para ser concluída, use uma fila de trabalho privada. Para criar uma fila de trabalho privada, chame MFAllocateWorkQueue. Qualquer componente de pipeline que executa operações de E/S deve evitar bloquear chamadas de E/S pelo mesmo motivo. A interface IMFByteStream fornece uma abstração útil para E/S de arquivo assíncrono.

Implementando o Begin.../End... Padrão

Conforme descrito em Chamando Métodos Assíncronos, métodos assíncronos no Media Foundation geralmente usam o Begin.../Final.... Padrão. Nesse padrão, uma operação assíncrona usa dois métodos com assinaturas semelhantes às seguintes:

// 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);

Para tornar o método realmente assíncrono, a implementação do BeginX deve executar o trabalho real em outro thread. É aí que as filas de trabalho entram em cena. Nas etapas a seguir, o chamador é o código que chama BeginX e EndX. Pode ser um aplicativo ou o pipeline do Media Foundation. O componente é o código que implementa BeginX e EndX.

  1. O chamador chama Begin..., passando um ponteiro para a interface IMFAsyncCallback do chamador.
  2. O componente cria um novo objeto de resultado assíncrono. Esse objeto armazena a interface de retorno de chamada e o objeto de estado do chamador. Normalmente, ele também armazena todas as informações de estado privado que o componente precisa para concluir a operação. O objeto de resultado desta etapa é rotulado como "Resultado 1" no próximo diagrama.
  3. O componente cria um segundo objeto de resultado. Esse objeto de resultado armazena dois ponteiros: o primeiro objeto de resultado e a interface de retorno de chamada do receptor. Esse objeto de resultado é rotulado como "Resultado 2" no próximo diagrama.
  4. O componente chama MFPutWorkItemEx para enfileirar um novo item de trabalho.
  5. No método Invoke , o componente faz o trabalho assíncrono.
  6. O componente chama MFInvokeCallback para invocar o método de retorno de chamada do chamador.
  7. O chamador chama o método EndX .

diagrama mostrando como um objeto implementa o padrão begin/end

Exemplo de método assíncrono

Para ilustrar essa discussão, usaremos um exemplo inventado. Considere um método assíncrono para calcular uma raiz quadrada:

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

O parâmetro x de BeginSquareRoot é o valor cuja raiz quadrada será calculada. A raiz quadrada é retornada no parâmetro pVal de EndSquareRoot.

Esta é a declaração de uma classe que implementa estes dois métodos:

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

A SqrRoot classe implementa IMFAsyncCallback para que possa colocar a operação raiz quadrada em uma fila de trabalho. O DoCalculateSquareRoot método é o método de classe privada que calcula a raiz quadrada. Esse método será chamado do thread da fila de trabalho.

Primeiro, precisamos de uma maneira de armazenar o valor de x, para que ele possa ser recuperado quando o thread da fila de trabalho chamar SqrRoot::Invoke. Aqui está uma classe simples que armazena as informações:

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

Essa classe implementa o IUnknown para que ele possa ser armazenado em um objeto de resultado.

O código a seguir implementa o BeginSquareRoot método :

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

O código faz o seguinte:

  1. Cria uma nova instância da AsyncOp classe para manter o valor de x.
  2. Chama MFCreateAsyncResult para criar um objeto de resultado. Esse objeto contém vários ponteiros:
    • Um ponteiro para a interface IMFAsyncCallback do chamador.
    • Um ponteiro para o objeto de estado do chamador (pState).
    • Um ponteiro para o objeto AsyncOp.
  3. Chama MFPutWorkItem para enfileirar um novo item de trabalho. Essa chamada cria implicitamente um objeto de resultado externo, que contém os seguintes ponteiros:
    • Um ponteiro para a SqrRoot interface IMFAsyncCallback do objeto.
    • Um ponteiro para o objeto de resultado interno da etapa 2.

O código a seguir implementa o SqrRoot::Invoke método :

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

Esse método obtém o objeto de resultado interno e o AsyncOp objeto . Em seguida, ele passa o AsyncOp objeto para DoCalculateSquareRoot. Por fim, ele chama IMFAsyncResult::SetStatus para definir o código status e MFInvokeCallback para invocar o método de retorno de chamada do chamador.

O DoCalculateSquareRoot método faz exatamente o que você esperaria:

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

    return S_OK;
}

Quando o método de retorno de chamada do chamador é invocado, é responsabilidade do chamador chamar o método End... — nesse caso, EndSquareRoot. É EndSquareRoot como o chamador recupera o resultado da operação assíncrona, que neste exemplo é a raiz quadrada computada. Essas informações são armazenadas no objeto de resultado:

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

Filas de operações

Até agora, supõe-se implicitamente que uma operação assíncrona poderia ser feita a qualquer momento, independentemente do estado atual do objeto. Por exemplo, considere o que acontece se um aplicativo chamar BeginSquareRoot enquanto uma chamada anterior para o mesmo método ainda estiver pendente. A SqrRoot classe pode enfileirar o novo item de trabalho antes que o item de trabalho anterior seja concluído. No entanto, as filas de trabalho não têm garantia de serializar itens de trabalho. Lembre-se de que uma fila de trabalho pode usar mais de um thread para expedir itens de trabalho. Em um ambiente multithread, um item de trabalho pode ser invocado antes que o anterior seja concluído. Os itens de trabalho podem até mesmo ser invocados fora de ordem, se ocorrer uma opção de contexto pouco antes do retorno de chamada ser invocado.

Por esse motivo, é responsabilidade do objeto serializar operações em si mesmo, se necessário. Em outras palavras, se o objeto exigir que a operação A seja concluída antes que a operação B possa ser iniciada, o objeto não deverá enfileirar um item de trabalho para B até que a operação A seja concluída. Um objeto pode atender a esse requisito tendo sua própria fila de operações pendentes. Quando um método assíncrono é chamado no objeto , o objeto coloca a solicitação em sua própria fila. À medida que cada operação assíncrona é concluída, o objeto extrai a próxima solicitação da fila. O exemplo MPEG1Source mostra um exemplo de como implementar essa fila.

Um único método pode envolver várias operações assíncronas, especialmente quando chamadas de E/S são usadas. Ao implementar métodos assíncronos, pense cuidadosamente nos requisitos de serialização. Por exemplo, é válido que o objeto inicie uma nova operação enquanto uma solicitação de E/S anterior ainda está pendente? Se a nova operação alterar o estado interno do objeto, o que acontece quando uma solicitação de E/S anterior é concluída e retorna dados que agora podem estar obsoletos? Um bom diagrama de estado pode ajudar a identificar as transições de estado válidas.

Considerações entre threads e processos cruzados

As filas de trabalho não usam marshaling COM para realizar marshaling de ponteiros de interface entre limites de thread. Portanto, mesmo que um objeto seja registrado como apartment-threaded ou o thread de aplicativo tenha inserido um STA (apartment de thread único), os retornos de chamada IMFAsyncCallback serão invocados de um thread diferente. De qualquer forma, todos os componentes de pipeline do Media Foundation devem usar o modelo de threading "Ambos".

Algumas interfaces no Media Foundation definem versões remotas de alguns métodos assíncronos. Quando um desses métodos é chamado entre os limites do processo, a DLL de proxy/stub do Media Foundation chama a versão remota do método , que executa o marshaling personalizado dos parâmetros de método. No processo remoto, o stub converte a chamada de volta para o método local no objeto . Esse processo é transparente para o aplicativo e o objeto remoto. Esses métodos de marshaling personalizados são fornecidos principalmente para objetos carregados no PMP (caminho de mídia protegido). Para obter mais informações sobre o PMP, consulte Caminho da mídia protegida.

Métodos de retorno de chamada assíncronos

Filas de Trabalho