Programación asincrónica en C++/CX

Nota:

Este tema existe para ayudarte a mantener la aplicación C++/CX. Pero te recomendamos que uses C++/WinRT para las nuevas aplicaciones. C++/WinRT es una moderna proyección de lenguaje C++17 totalmente estándar para las API de Windows Runtime (WinRT), implementada como una biblioteca basada en archivos de encabezado y diseñada para darte acceso de primera clase a la API moderna de Windows.

En este artículo se describe la manera recomendada de consumir métodos asincrónicos en las extensiones de componentes de Visual C++ (C++/CX) usando la clase task que se define en el espacio de nombres concurrency en ppltasks.h.

Tipo asíncronos de Windows Runtime

Windows Runtime incluye un modelo bien definido para llamar a métodos asíncronos y proporciona los tipos que necesitas para consumir estos métodos. Si no estás familiarizado con el modelo asíncrono de Windows Runtime, lee Programación asincrónica antes de leer el resto de este artículo.

Aunque puedes consumir las API asíncronas de Windows Runtime directamente en C++, el enfoque preferido es usar la clase de tarea y sus tipos y funciones relacionados, que se encuentran en el espacio de nombres de simultaneidad y se definen en <ppltasks.h>. concurrency::task es un tipo de uso general, pero cuando se usa el conmutador del compilador /ZW, que es necesario para las aplicaciones y componentes de la Plataforma Universal de Windows (UWP), la clase de tarea encapsula los tipos asíncronos de Windows Runtime para que sea más fácil:

  • encadenar varias operaciones asíncronas y sincrónicas juntas

  • controlar excepciones en cadenas de tareas

  • realizar la cancelación en cadenas de tareas

  • asegúrate de que las tareas individuales se ejecutan en el contexto de subproceso o apartamento adecuados

En este artículo se proporcionan instrucciones básicas sobre cómo usar la clase de tarea con las API asíncronas de Windows Runtime. Para obtener documentación más completa sobre task y sus métodos relacionados, incluidos create_task, consulta Paralelismo de tareas (Runtime de simultaneidad).

Consumo de una operación asíncrona mediante una tarea

En el ejemplo siguiente se muestra cómo usar la clase de tarea para consumir un método asíncrono que devuelve una interfaz IAsyncOperation y cuya operación genera un valor. Estos son los pasos básicos:

  1. Llama al método create_task y pásale el objeto IAsyncOperation^.

  2. Llama a la función miembro task::then en la tarea y proporciona una expresión lambda que se invocará cuando se complete la operación asincrónica.

#include <ppltasks.h>
using namespace concurrency;
using namespace Windows::Devices::Enumeration;
...
void App::TestAsync()
{    
    //Call the *Async method that starts the operation.
    IAsyncOperation<DeviceInformationCollection^>^ deviceOp =
        DeviceInformation::FindAllAsync();

    // Explicit construction. (Not recommended)
    // Pass the IAsyncOperation to a task constructor.
    // task<DeviceInformationCollection^> deviceEnumTask(deviceOp);

    // Recommended:
    auto deviceEnumTask = create_task(deviceOp);

    // Call the task's .then member function, and provide
    // the lambda to be invoked when the async operation completes.
    deviceEnumTask.then( [this] (DeviceInformationCollection^ devices )
    {       
        for(int i = 0; i < devices->Size; i++)
        {
            DeviceInformation^ di = devices->GetAt(i);
            // Do something with di...          
        }       
    }); // end lambda
    // Continue doing work or return...
}

La tarea creada y devuelta por la función task::then se conoce como continuación. El argumento de entrada (en este caso) para la expresión lambda proporcionada por el usuario es el resultado que la operación de tarea genera cuando se completa. Es el mismo valor que se recuperaría llamando directamente a IAsyncOperation::GetResults si estuviera usando la interfaz IAsyncOperation directamente.

El método task::then devuelve inmediatamente y su delegado no se ejecuta hasta que el trabajo asíncrono se completa correctamente. En este ejemplo, si la operación asincrónica hace que se produzca una excepción o termine en el estado cancelado como resultado de una solicitud de cancelación, la continuación nunca se ejecutará. Más adelante, describiremos cómo escribir continuaciones que se ejecutan incluso si la tarea anterior se canceló o produjo un error.

Aunque declares la variable de tarea en la pila local, administra su duración para que no se elimine hasta que todas sus operaciones se completen y todas las referencias a ella salgan del ámbito, incluso si el método vuelve antes de que se completen las operaciones.

Creación de una cadena de tareas

En la programación asíncrona, es habitual definir una secuencia de operaciones, también conocidas como cadenas de tareas, en las que cada continuación solo se ejecuta cuando se completa la anterior. En algunos casos, la tarea anterior (o antecedente) genera un valor que la continuación acepta como entrada. Mediante el método task::then, puedes crear cadenas de tareas de forma intuitiva y sencilla; el método devuelve una tarea<T> donde T es el tipo de valor devuelto de la función lambda. Puedes componer varias continuaciones en una cadena de tareas: myTask.then(…).then(…).then(…);

Las cadenas de tareas son especialmente útiles cuando una continuación crea una nueva operación asincrónica; dicha tarea se conoce como una tarea asíncrona. En el ejemplo siguiente se muestra una cadena de tareas que tiene dos continuaciones. La tarea inicial adquiere el identificador de un archivo existente y, cuando se completa esa operación, la primera continuación inicia una nueva operación asincrónica para eliminar el archivo. Cuando se completa esa operación, la segunda continuación se ejecuta y genera un mensaje de confirmación.

#include <ppltasks.h>
using namespace concurrency;
...
void App::DeleteWithTasks(String^ fileName)
{    
    using namespace Windows::Storage;
    StorageFolder^ localFolder = ApplicationData::Current->LocalFolder;
    auto getFileTask = create_task(localFolder->GetFileAsync(fileName));

    getFileTask.then([](StorageFile^ storageFileSample) ->IAsyncAction^ {       
        return storageFileSample->DeleteAsync();
    }).then([](void) {
        OutputDebugString(L"File deleted.");
    });
}

En el ejemplo anterior se muestran cuatro puntos importantes:

  • La primera continuación convierte el objeto IAsyncAction^ en una task<void> y devuelve la tarea.

  • La segunda continuación no realiza ningún control de errores y, por tanto, toma void y no task<void> comoentrada. Es una continuación basada en valores.

  • La segunda continuación no se ejecuta hasta que se completa la operación DeleteAsync.

  • Dado que la segunda continuación está basada en valores, si la operación iniciada por la llamada a DeleteAsync produce una excepción, la segunda continuación no se ejecuta en absoluto.

Nota Crear una cadena de tareas es solo una de las formas de usar la clase task para crear operaciones asíncronas. También puedes componer operaciones mediante los operadores de combinación y elección && y ||. Para obtener más información, consulta Paralelismo de tareas (Runtime de simultaneidad).

Tipos de valor devueltos de función lambda y tipos de valor devueltos de tareas

En una continuación de la tarea, el tipo de valor devuelto de la función lambda se encapsula en un objeto task. Si la expresión lambda devuelve un valor doble, el tipo de la tarea de continuación es task<double>. Sin embargo, el objeto de tarea está diseñado para que no genere tipos de valor devueltos anidados innecesariamente. Si una expresión lambda devuelve una IAsyncOperation<SyndicationFeed^>^, la continuación devuelve una task<SyndicationFeed^>, no una task<task<SyndicationFeed^>> o una task<IAsyncOperation<SyndicationFeed^>^>^. Este proceso se conoce como desencapsulamiento asíncrono y también garantiza que la operación asincrónica dentro de la continuación se complete antes de que se invoque la siguiente continuación.

En el ejemplo anterior, observa que la tarea devuelve una task<void> aunque su lambda devolviera un objeto IAsyncInfo. En la tabla siguiente se resumen las conversiones de tipos que se producen entre una función lambda y la tarea envolvente:

tipo de valor devuelto lambda Tipo de valor devuelto .then
TResult task<TResult>
IAsyncOperation<TResult>^ task<TResult>
IAsyncOperationWithProgress<TResult, TProgress>^ task<TResult>
IAsyncAction^ task<void>
IAsyncActionWithProgress<TProgress>^ task<void>
task<TResult> task<TResult>

Cancelar tareas

A menudo es una buena idea dar al usuario la opción de cancelar una operación asincrónica. Y en algunos casos es posible que tenga que cancelar una operación mediante programación desde fuera de la cadena de tareas. Aunque cada tipo de valor devuelto *Async tiene un método Cancelar que hereda de IAsyncInfo, es incómodo exponerlo a métodos externos. La manera preferida de admitir la cancelación en una cadena de tareas es usar un cancellation_token_source para crear un cancellation_token y, a continuación, pasar el token al constructor de la tarea inicial. Se se crea una tarea asíncrona con un token de cancelación, y se llama a [cancellation_token_source::cancel](/cpp/parallel/concrt/reference/cancellation-token-source-class?view=vs-2017& -view=true), la tarea llama automáticamente a Cancelar en la operación IAsync* y pasa la solicitud de cancelación a su cadena de continuación. El pseudocódigo siguiente muestra el enfoque básico.

//Class member:
cancellation_token_source m_fileTaskTokenSource;

// Cancel button event handler:
m_fileTaskTokenSource.cancel();

// task chain
auto getFileTask2 = create_task(documentsFolder->GetFileAsync(fileName),
                                m_fileTaskTokenSource.get_token());
//getFileTask2.then ...

Cuando se cancela una tarea, se propaga una excepción task_canceled por la cadena de tareas. Las continuaciones basadas en valores simplemente no se ejecutarán, pero las continuaciones basadas en tareas harán que se produzca la excepción cuando se llame a task::get. Si tienes una continuación de control de errores, asegúrate de que detecta explícitamente la excepción task_canceled. (Esta excepción no se deriva de Platform::Exception.)

La cancelación es cooperativa. Si la continuación realiza algún trabajo de larga duración más allá de invocar un método para UWP, entonces es tu responsabilidad comprobar el estado del token de cancelación periódicamente y detener la ejecución si se cancela. Después de limpiar todos los recursos asignados en la continuación, llama a cancel_current_task para cancelar esa tarea y propagar la cancelación a cualquier continuación basada en valores que la siga. Este es otro ejemplo: puedes crear una cadena de tareas que represente el resultado de una operación FileSavePicker. Si el usuario elige el botón Cancelar, no se llama al método IAsyncInfo::Cancel. En su lugar, la operación se realiza correctamente, pero devuelve nullptr. La continuación puede probar el parámetro de entrada y llamar a cancel_current_task si la entrada es nullptr.

Para más información, consulta Cancelación en la PPL

Control de errores en una cadena de tareas

Si quieres que una continuación se ejecute incluso si el antecedente se canceló o produjo una excepción,, convierte la continuación en una continuación basada en tareas especificando la entrada a su función lambda como task<TResult> o task<void> si la expresión lambda de la tarea antecedente devuelve una IAsyncAction^.

Para controlar los errores y la cancelación en una cadena de tareas, no es necesario realizar todas las tareas de continuación basadas en tareas ni incluir todas las operaciones que puedan iniciarse dentro de un bloque try…catch. En su lugar, puedes agregar una continuación basada en tareas al final de la cadena y controlar todos los errores allí. Cualquier excepción (esto incluye una excepción task_canceled), propagará la cadena de tareas y omitirá las continuaciones basadas en valores, de modo que puedas controlarla en la continuación basada en tareas de control de errores. Podemos volver a escribir el ejemplo anterior para usar una continuación basada en tareas de control de errores:

#include <ppltasks.h>
void App::DeleteWithTasksHandleErrors(String^ fileName)
{    
    using namespace Windows::Storage;
    using namespace concurrency;

    StorageFolder^ documentsFolder = KnownFolders::DocumentsLibrary;
    auto getFileTask = create_task(documentsFolder->GetFileAsync(fileName));

    getFileTask.then([](StorageFile^ storageFileSample)
    {       
        return storageFileSample->DeleteAsync();
    })

    .then([](task<void> t)
    {

        try
        {
            t.get();
            // .get() didn' t throw, so we succeeded.
            OutputDebugString(L"File deleted.");
        }
        catch (Platform::COMException^ e)
        {
            //Example output: The system cannot find the specified file.
            OutputDebugString(e->Message->Data());
        }

    });
}

En una continuación basada en tareas, llamamos a la función miembro task::get para obtener los resultados de la tarea. Todavía tenemos que llamar a task::get incluso si la operación era una IAsyncAction que no genera ningún resultado porque task::get también obtiene las excepciones que se han transportado a la tarea. Si la tarea de entrada almacena una excepción, se produce en la llamada a task::get. Si no llama a task::get, no usa una continuación basada en tareas al final de la cadena o no detecta el tipo de excepción que se produjo, se produce una unobserved_task_exception cuando se han eliminado todas las referencias a la tarea.

Capturar solo las excepciones que puedas administrar. Si la aplicación encuentra un error que no se puede recuperar, es mejor permitir que la aplicación se bloquee que dejar que continúe ejecutándose en un estado desconocido. Además, en general, no intentes capturar la unobserved_task_exception en sí misma. Esta excepción está pensada principalmente para fines de diagnóstico. Cuando se inicia unobserved_task_exception, normalmente indica un error en el código. A menudo, la causa es una excepción que se debe controlar o una excepción irrecuperable causada por algún otro error en el código.

Administración del contexto del subproceso

La interfaz de usuario de una aplicación para UWP se ejecuta en un contenedor uniproceso (STA). Una tarea cuya expresión lambda devuelve IAsyncAction o IAsyncOperation es compatible con los contenedores. Si la tarea se crea en el STA, todas sus continuaciones se ejecutarán también de manera predeterminada, a menos que especifique lo contrario. En otras palabras, toda la cadena de tareas hereda el reconocimiento de contenedores de la tarea primaria. Este comportamiento ayuda a simplificar las interacciones con los controles de interfaz de usuario, a los que solo se puede acceder desde el STA.

Por ejemplo, en una aplicación para UWP, en la función miembro de cualquier clase que represente una página XAML, puedes rellenar un control ListBox desde un método task::then sin tener que usar el objeto Dispatcher.

#include <ppltasks.h>
void App::SetFeedText()
{    
    using namespace Windows::Web::Syndication;
    using namespace concurrency;
    String^ url = "http://windowsteamblog.com/windows_phone/b/wmdev/atom.aspx";
    SyndicationClient^ client = ref new SyndicationClient();
    auto feedOp = client->RetrieveFeedAsync(ref new Uri(url));

    create_task(feedOp).then([this]  (SyndicationFeed^ feed)
    {
        m_TextBlock1->Text = feed->Title->Text;
    });
}

Si una tarea no devuelve IAsyncAction o IAsyncOperation, no es compatible con el apartamento y, de forma predeterminada, sus continuaciones se ejecutan en el primer subproceso en segundo plano disponible.

Puede invalidar el contexto de subproceso predeterminado para cualquier tipo de tarea mediante la sobrecarga de task::then que toma un task_continuation_context. Por ejemplo, en algunos casos, podría ser conveniente programar la continuación de una tarea compatible con apartamentos en un subproceso en segundo plano. En tal caso, puedes pasar task_continuation_context::use_arbitrary para programar el trabajo de la tarea en el siguiente subproceso disponible en un apartamento multiproceso. Esto puede mejorar el rendimiento de la continuación porque su trabajo no tiene que sincronizarse con otro trabajo que se está produciendo en el subproceso de la interfaz de usuario.

En el ejemplo siguiente se muestra cuándo resulta útil especificar la opción task_continuation_context::use_arbitrary y también se muestra cómo resulta útil el contexto de continuación predeterminado para sincronizar operaciones simultáneas en colecciones no seguras para subprocesos. En este código, recorremos una lista de direcciones URL para fuentes RSS y, para cada dirección URL, iniciamos una operación asincrónica para recuperar los datos de feeds. No podemos controlar el orden en el que se recuperan los feeds y no nos importa realmente. Cuando se completa cada operación RetrieveFeedAsync, la primera continuación acepta el objeto SyndicationFeed^ y lo usa para inicializar un objeto FeedData^ definido por la aplicación. Dado que cada una de estas operaciones es independiente de las demás, podemos acelerar las cosas especificando el contexto de la continuación task_continuation_context::use_arbitrary. Sin embargo, después de inicializar cada objeto FeedData, tenemos que agregarlo a un vector, que no es una colección segura para subprocesos. Por lo tanto, creamos una continuación y especificamos [task_continuation_context::use_current](/cpp/parallel/concrt/reference/task-continuation-context-class?view=vs-2017& -view=true) para garantizar que todas las llamadas a Append se produzcan en el mismo contexto de contenedor uniproceso de aplicación (ASTA). Puesto que task_continuation_context::use_default es el contexto predeterminado, no es necesario especificarlo explícitamente, pero lo hacemos aquí por motivos de claridad.

#include <ppltasks.h>
void App::InitDataSource(Vector<Object^>^ feedList, vector<wstring> urls)
{
                using namespace concurrency;
    SyndicationClient^ client = ref new SyndicationClient();

    std::for_each(std::begin(urls), std::end(urls), [=,this] (std::wstring url)
    {
        // Create the async operation. feedOp is an
        // IAsyncOperationWithProgress<SyndicationFeed^, RetrievalProgress>^
        // but we don't handle progress in this example.

        auto feedUri = ref new Uri(ref new String(url.c_str()));
        auto feedOp = client->RetrieveFeedAsync(feedUri);

        // Create the task object and pass it the async operation.
        // SyndicationFeed^ is the type of the return value
        // that the feedOp operation will eventually produce.

        // Then, initialize a FeedData object by using the feed info. Each
        // operation is independent and does not have to happen on the
        // UI thread. Therefore, we specify use_arbitrary.
        create_task(feedOp).then([this]  (SyndicationFeed^ feed) -> FeedData^
        {
            return GetFeedData(feed);
        }, task_continuation_context::use_arbitrary())

        // Append the initialized FeedData object to the list
        // that is the data source for the items collection.
        // This all has to happen on the same thread.
        // By using the use_default context, we can append
        // safely to the Vector without taking an explicit lock.
        .then([feedList] (FeedData^ fd)
        {
            feedList->Append(fd);
            OutputDebugString(fd->Title->Data());
        }, task_continuation_context::use_default())

        // The last continuation serves as an error handler. The
        // call to get() will surface any exceptions that were raised
        // at any point in the task chain.
        .then( [this] (task<void> t)
        {
            try
            {
                t.get();
            }
            catch(Platform::InvalidArgumentException^ e)
            {
                //TODO handle error.
                OutputDebugString(e->Message->Data());
            }
        }); //end task chain

    }); //end std::for_each
}

Las tareas anidadas, que son nuevas tareas que se crean dentro de una continuación, no heredan el reconocimiento de apartamentos de la tarea inicial.

Entrega de actualizaciones de progreso

Los métodos que admiten IAsyncOperationWithProgress o IAsyncActionWithProgress proporcionan actualizaciones de progreso periódicamente mientras la operación está en curso, antes de que se complete. Los informes de progreso son independientes de la noción de tareas y continuaciones. Solo se proporciona el delegado para la propiedad Progreso del objeto. Un uso típico del delegado es actualizar una barra de progreso en la interfaz de usuario.