Écriture d’une méthode asynchrone

Cette rubrique explique comment implémenter une méthode asynchrone dans Microsoft Media Foundation.

Les méthodes asynchrones sont omniprésentes dans le pipeline Media Foundation. Les méthodes asynchrones facilitent la répartition du travail entre plusieurs threads. Il est particulièrement important d’effectuer des E/S de manière asynchrone, afin que la lecture à partir d’un fichier ou d’un réseau ne bloque pas le reste du pipeline.

Si vous écrivez une source multimédia ou un récepteur multimédia, il est essentiel de gérer correctement les opérations asynchrones, car les performances de votre composant ont un impact sur l’ensemble du pipeline.

Notes

Les transformations Media Foundation (MFT) utilisent des méthodes synchrones par défaut.

 

Files d’attente de travail pour les opérations asynchrones

Dans Media Foundation, il existe une relation étroite entre les méthodes de rappel asynchrones et les files d’attente de travail. Une file d’attente de travail est une abstraction permettant de déplacer le travail du thread de l’appelant vers un thread worker. Pour effectuer un travail sur une file d’attente de travail, procédez comme suit :

  1. Implémentez l’interface IMFAsyncCallback .

  2. Appelez MFCreateAsyncResult pour créer un objet de résultat . L’objet result expose l’OBJET IMFAsyncResult. L’objet result contient trois pointeurs :

    • Pointeur vers l’interface IMFAsyncCallback de l’appelant.
    • Pointeur facultatif vers un objet d’état. S’il est spécifié, l’objet state doit implémenter IUnknown.
    • Pointeur facultatif vers un objet privé. S’il est spécifié, cet objet doit également implémenter IUnknown.

    Les deux derniers pointeurs peuvent être NULL. Sinon, utilisez-les pour stocker des informations sur l’opération asynchrone.

  3. Appelez MFPutWorkItemEx pour mettre en file d’attente l’élément de travail.

  4. Le thread de file d’attente de travail appelle votre méthode IMFAsyncCallback::Invoke .

  5. Effectuez le travail à l’intérieur de votre méthode Invoke . Le paramètre pAsyncResult de cette méthode est le pointeur IMFAsyncResult de l’étape 2. Utilisez ce pointeur pour obtenir l’objet d’état et l’objet privé :

Vous pouvez également combiner les étapes 2 et 3 en appelant la fonction MFPutWorkItem . En interne, cette fonction appelle MFCreateAsyncResult pour créer l’objet de résultat.

Le diagramme suivant montre les relations entre l’appelant, l’objet de résultat, l’objet d’état et l’objet privé.

diagramme montrant un objet de résultat asynchrone

Le diagramme de séquence suivant montre comment un objet met en file d’attente un élément de travail. Lorsque le thread de file d’attente de travail appelle Invoke, l’objet effectue l’opération asynchrone sur ce thread.

diagramme montrant comment un objet met en file d’attente un élément de travail

Il est important de se rappeler qu’Invoke est appelé à partir d’un thread appartenant à la file d’attente de travail. Votre implémentation d’Invoke doit être thread-safe. En outre, si vous utilisez la file d’attente de travail de la plateforme (MFASYNC_CALLBACK_QUEUE_STANDARD), il est essentiel de ne jamais bloquer le thread, car cela peut empêcher tout le pipeline Media Foundation de traiter les données. Si vous devez effectuer une opération qui bloquera ou prendra beaucoup de temps, utilisez une file d’attente de travail privée. Pour créer une file d’attente de travail privée, appelez MFAllocateWorkQueue. Tout composant de pipeline qui effectue des opérations d’E/S doit éviter de bloquer les appels d’E/S pour la même raison. L’interface IMFByteStream fournit une abstraction utile pour les E/S de fichiers asynchrones.

Implémentation du paramètre Begin.../End... Modèle

Comme décrit dans Appel de méthodes asynchrones, les méthodes asynchrones dans Media Foundation utilisent souvent le.../Fin.... Modèle. Dans ce modèle, une opération asynchrone utilise deux méthodes avec des signatures similaires à celles-ci :

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

Pour rendre la méthode véritablement asynchrone, l’implémentation de BeginX doit effectuer le travail réel sur un autre thread. C’est là que les files d’attente de travail entrent dans l’image. Dans les étapes suivantes, l’appelant est le code qui appelle BeginX et EndX. Il peut s’agir d’une application ou du pipeline Media Foundation. Le composant est le code qui implémente BeginX et EndX.

  1. L’appelant appelle Begin..., en passant un pointeur vers l’interface IMFAsyncCallback de l’appelant.
  2. Le composant crée un nouvel objet de résultat asynchrone. Cet objet stocke l’interface de rappel et l’objet d’état de l’appelant. En règle générale, il stocke également toutes les informations d’état privé dont le composant a besoin pour terminer l’opération. L’objet de résultat de cette étape est intitulé « Result 1 » dans le diagramme suivant.
  3. Le composant crée un deuxième objet de résultat. Cet objet de résultat stocke deux pointeurs : le premier objet de résultat et l’interface de rappel de l’appelé. Cet objet de résultat est intitulé « Result 2 » dans le diagramme suivant.
  4. Le composant appelle MFPutWorkItemEx pour mettre en file d’attente un nouvel élément de travail.
  5. Dans la méthode Invoke , le composant effectue le travail asynchrone.
  6. Le composant appelle MFInvokeCallback pour appeler la méthode de rappel de l’appelant.
  7. L’appelant appelle la méthode EndX .

diagramme montrant comment un objet implémente le modèle de début/fin

Exemple de méthode asynchrone

Pour illustrer cette discussion, nous allons utiliser un exemple artificiel. Envisagez une méthode asynchrone pour le calcul d’une racine carrée :

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

Le paramètre x de BeginSquareRoot est la valeur dont la racine carrée sera calculée. La racine carrée est retournée dans le paramètre pVal de EndSquareRoot.

Voici la déclaration d’une classe qui implémente ces deux méthodes :

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

La SqrRoot classe implémente IMFAsyncCallback afin qu’elle puisse placer l’opération racine carrée sur une file d’attente de travail. La DoCalculateSquareRoot méthode est la méthode de classe privée qui calcule la racine carrée. Cette méthode sera appelée à partir du thread de file d’attente de travail.

Tout d’abord, nous avons besoin d’un moyen de stocker la valeur de x, afin qu’elle puisse être récupérée lorsque le thread de file d’attente de travail appelle SqrRoot::Invoke. Voici une classe simple qui stocke les informations :

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

Cette classe implémente IUnknown afin qu’il puisse être stocké dans un objet de résultat.

Le code suivant implémente la BeginSquareRoot méthode :

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

Ce code effectue les actions suivantes :

  1. Crée une instance de la AsyncOp classe pour contenir la valeur x.
  2. Appelle MFCreateAsyncResult pour créer un objet de résultat. Cet objet contient plusieurs pointeurs :
    • Pointeur vers l’interface IMFAsyncCallback de l’appelant.
    • Pointeur vers l’objet d’état de l’appelant (pState).
    • Pointeur vers l'objet AsyncOp.
  3. Appelle MFPutWorkItem pour mettre en file d’attente un nouvel élément de travail. Cet appel crée implicitement un objet de résultat externe, qui contient les pointeurs suivants :
    • Pointeur vers l’interface SqrRootIMFAsyncCallback de l’objet.
    • Pointeur vers l’objet résultat interne de l’étape 2.

Le code suivant implémente la SqrRoot::Invoke méthode :

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

Cette méthode obtient l’objet de résultat interne et l’objet AsyncOp . Ensuite, il passe l’objet AsyncOp à DoCalculateSquareRoot. Enfin, il appelle IMFAsyncResult::SetStatus pour définir le code status et MFInvokeCallback pour appeler la méthode de rappel de l’appelant.

La DoCalculateSquareRoot méthode fait exactement ce que vous attendez :

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

    return S_OK;
}

Lorsque la méthode de rappel de l’appelant est appelée, il incombe à l’appelant d’appeler la méthode End... (dans ce cas, EndSquareRoot. Est EndSquareRoot la façon dont l’appelant récupère le résultat de l’opération asynchrone, qui dans cet exemple est la racine carrée calculée. Ces informations sont stockées dans l’objet résultat :

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

Files d’attente d’opération

Jusqu’à présent, on a supposé implicitement qu’une opération asynchrone pouvait être effectuée à tout moment, quel que soit l’état actuel de l’objet. Par exemple, considérez ce qui se passe si une application appelle BeginSquareRoot alors qu’un appel antérieur à la même méthode est toujours en attente. La SqrRoot classe peut mettre en file d’attente le nouvel élément de travail avant que l’élément de travail précédent soit terminé. Toutefois, les files d’attente de travail ne sont pas garanties pour sérialiser les éléments de travail. Rappelez-vous qu’une file d’attente de travail peut utiliser plusieurs threads pour distribuer des éléments de travail. Dans un environnement multithread, un élément de travail peut être appelé avant la fin du précédent. Les éléments de travail peuvent même être appelés dans le désordre si un changement de contexte se produit juste avant l’appel du rappel.

Pour cette raison, il incombe à l’objet de sérialiser les opérations sur lui-même, si nécessaire. En d’autres termes, si l’objet nécessite la fin de l’opération A avant le début de l’opération B , l’objet ne doit pas mettre en file d’attente un élément de travail pour B tant que l’opération A n’est pas terminée. Un objet peut répondre à cette exigence en ayant sa propre file d’attente d’opérations en attente. Lorsqu’une méthode asynchrone est appelée sur l’objet, l’objet place la requête sur sa propre file d’attente. Lorsque chaque opération asynchrone est terminée, l’objet extrait la requête suivante de la file d’attente. L’exemple MPEG1Source montre un exemple d’implémentation d’une telle file d’attente.

Une seule méthode peut impliquer plusieurs opérations asynchrones, en particulier lorsque des appels d’E/S sont utilisés. Lorsque vous implémentez des méthodes asynchrones, réfléchissez attentivement aux exigences de sérialisation. Par exemple, est-il valide pour que l’objet démarre une nouvelle opération alors qu’une demande d’E/S précédente est toujours en attente ? Si la nouvelle opération modifie l’état interne de l’objet, que se passe-t-il lorsqu’une demande d’E/S précédente se termine et retourne des données qui peuvent maintenant être obsolètes ? Un diagramme d’état correct peut aider à identifier les transitions d’état valides.

Considérations sur les threads croisés et les processus

Les files d’attente de travail n’utilisent pas le marshaling COM pour marshaler les pointeurs d’interface au-delà des limites des threads. Par conséquent, même si un objet est inscrit en tant que thread d’appartement ou si le thread d’application a entré un appartement à thread unique (STA), les rappels IMFAsyncCallback sont appelés à partir d’un autre thread. Dans tous les cas, tous les composants de pipeline Media Foundation doivent utiliser le modèle de threading « Both ».

Certaines interfaces dans Media Foundation définissent des versions distantes de certaines méthodes asynchrones. Quand l’une de ces méthodes est appelée au-delà des limites du processus, la DLL proxy/stub Media Foundation appelle la version distante de la méthode, qui effectue un marshaling personnalisé des paramètres de méthode. Dans le processus distant, le stub convertit le rappel en méthode locale sur l’objet. Ce processus est transparent pour l’application et l’objet distant. Ces méthodes de marshaling personnalisées sont principalement fournies pour les objets chargés dans le chemin d’accès multimédia protégé (PMP). Pour plus d’informations sur le PMP, consultez Chemin du média protégé.

Méthodes de rappel asynchrones

Files d’attente de travail