Programación asincrónica en C++ (aplicaciones de Windows en tiempo de ejecución)

Applies to Windows and Windows Phone

Este artículo 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.

Considera también la posibilidad de leer Modelos de programación asincrónica y sugerencias en Hilo (aplicaciones de la Tienda Windows con C++ y XAML) para saber cómo usamos el Runtime de simultaneidad para implementar operaciones asincrónicas en Hilo, una aplicación de Windows en tiempo de ejecución con C++ y XAML.

Tipos asincrónicos de Windows en tiempo de ejecución

Windows en tiempo de ejecución cuenta con un modelo bien definido para llamar a métodos asincrónicos y proporciona los tipos que necesitas para consumirlos. Si no estás familiarizado con el modelo asincrónico de Windows en tiempo de ejecución, lee Programación asincrónica antes de leer el resto de este artículo.

Aunque puedes consumir las API de Windows en tiempo de ejecución asincrónicas directamente en C++, el enfoque preferido es usar la task class y sus funciones y tipos relacionados, que están contenidos en el espacio de nombres concurrency y definidos en <ppltasks.h>. La clase concurrency::task es un tipo de uso general, pero cuando se usa el conmutador de compilador /ZW, que es obligatorio para los componentes y las aplicaciones de Windows en tiempo de ejecución, la clase task encapsula los tipos asincrónicos de Windows en tiempo de ejecución para que sea más fácil:

  • encadenar múltiples operaciones sincrónicas y asincrónicas;

  • administrar excepciones en las cadenas de tareas;

  • realizar cancelaciones en las cadenas de tareas;

  • garantizar que las tareas individuales se ejecuten en el contexto o apartamento de subproceso apropiado.

En este artículo se proporcionan pautas básicas acerca de cómo usar la clase de task con API asincrónicas de Windows en tiempo de ejecución. Para obtener documentación más completa acerca de task y sus métodos relacionados, incluido create_task, consulta Paralelismo de tareas (tiempo de ejecución de simultaneidad). Para obtener más información sobre cómo crear métodos públicos asincrónicos para el consumo por parte de JavaScript u otros lenguajes compatibles con Windows en tiempo de ejecución, consulta el tema sobre la creación de operaciones asincrónicas en C++ para aplicaciones de Windows en tiempo de ejecución.

Consumo de una operación asincrónica mediante el uso de una tarea

En el siguiente ejemplo se muestra cómo usar la clase de tarea para consumir un método async que devuelve una interfaz de IAsyncOperation y cuya operación produce un valor. He aquí 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 un 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 que la función task::then crea y devuelve se conoce como una continuación. El argumento de entrada (en este caso) al lambda proporcionado por el usuario es el resultado que la operación de la tarea produce cuando se completa. Es el mismo valor que se recuperaría al llamar a IAsyncOperation::GetResults si estuvieras usando la interfaz IAsyncOperation directamente.

El método task::then vuelve de inmediato, y su delegado no se ejecuta hasta que el trabajo asincrónico se completa satisfactoriamente. En este ejemplo, si la operación asincrónica hace que se inicie una excepción o finaliza en el estado Cancelado como resultado de una solicitud de cancelación, la continuación nunca se ejecutará. Más adelante, describiremos el modo en que se escriben las continuaciones que se ejecutan incluso si se canceló la tarea previa o fue errónea.

Aunque declares la variable de tarea en la pila local, administra su vigencia para que no se elimine hasta que todas las operaciones se completen y todas las referencias a ella queden fuera del alcance, incluso si el método vuelve antes de que la operación se complete.

Creación de una cadena de tareas

En la programación asincrónica, es común definir una secuencia de operaciones, también conocida como cadenas de tareas, en la que cada continuación se ejecuta solamente cuando se completa una de las anteriores. En algunos casos, la tarea anterior (o antecedente) produce un valor que la continuación acepta como entrada. Mediante el uso del método task::then, puedes crear cadenas de tareas de una manera sencilla e intuitiva. El método devuelve una task<T> donde T es el tipo de devolución de la función lambda. Puedes componer múltiples 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; como una tarea conocida como tarea asincrónica. El siguiente ejemplo ilustra una cadena de tareas que tiene dos continuaciones. La tarea inicial adquiere el controlador de un archivo existente, y cuando la operación se completa, la primera continuación inicia una nueva operación asincrónica para eliminar el archivo. Cuando la operación se completa, se ejecuta la segunda continuación y produce un mensaje de confirmación.



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

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


El ejemplo anterior ilustra cuatro puntos importantes:

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

  • La segunda continuación no realiza un control de errores y, por lo tanto, toma void y no task<void> como entrada. Es una continuación basada en valores.

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

  • Dado que la segunda continuación se basa en valores, si la operación que la llamada a DeleteAsync comenzó inicia una excepción, la segunda continuación no se ejecuta.

Nota  La creación de una cadena de tareas constituye tan solo una de las maneras de usar la clase de task para componer operaciones asincrónicas. También puedes componer operaciones usando los operadores de unión y elección && y ||. Para obtener más información, consulta Paralelismo de tareas (tiempo de ejecución de simultaneidad).

Tipos devueltos de tareas y tipos devueltos de función Lambda

En una continuación de tarea, el tipo devuelto de la función lambda se encapsula en un objeto de task. Si el lambda devuelve una double, el tipo de tarea de continuación es task<double>. No obstante, el objeto de tarea está diseñado para que no produzca tipos devueltos anidados sin necesidad. Si un lambda devuelve una IAsyncOperation<SyndicationFeed^>^, la continuación devuelve una task<SyndicationFeed^>, no una task<task<SyndicationFeed^>> ni una task<IAsyncOperation<SyndicationFeed^>^>^. Este proceso se conoce como desencapsulación asincrónica y también garantiza que la operación asincrónica dentro de la continuación se complete antes de que se invoque a la siguiente continuación.

En el ejemplo anterior, observe que la tarea devuelve task<void> incluso cuando el lambda devuelva un objeto IAsyncInfo. En la siguiente tabla se resumen los tipos de conversiones que se producen entre una función lambda y la tarea envolvente:

Tipo devuelto lambda

Tipo 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>

 

Tareas de cancelación

Normalmente, es bueno dar al usuario la opción de cancelar una operación asincrónica. Y en algunos casos, probablemente tengas que cancelar una operación mediante programación desde fuera de la cadena de tareas. Aunque cada tipo devuelto *Async tenga un método Cancel que hereda de IAsyncInfo, es extraño exponerlo a métodos externos. La manera que se prefiere para admitir la cancelación en una cadena de tareas es usar un cancellation_token_source para crear un cancellation_token y después pasar el token al constructor de la tarea inicial. Si se crea una tarea asincrónica con un token de cancelación y se llama a cancellation_token_source::cancel, la tarea automáticamente llama a Cancel en la operación IAsync* y pasa la solicitud de cancelación a su cadena de continuación. El siguiente seudocódigo demuestra 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 hacia abajo en la cadena de tareas. Las continuaciones basadas en valores simplemente no se ejecutarán, pero las continuaciones basadas en tarea harán que se inicie la excepción cuando se llame a task::get. Si tienes una continuación de control de errores, asegúrate de que almacene en memoria la excepción task_canceled explícitamente. (Esta excepción no se deriva de Platform::Exception).

La cancelación es cooperativa. Si tu continuación realiza trabajo de larga duración que es más que simplemente invocar un método de Windows en tiempo de ejecución, es tu responsabilidad comprobar el estado del token de cancelación periódicamente y detener la ejecución si se cancela. Después de que limpies todos los recursos que se asignaron en la continuación, llama a cancel_current_task para cancelar la tarea y propagar la cancelación hacia abajo a cualquier continuación basada en valores que la siga. He aquí 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 cambio, 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 obtener más información, consulta el tema sobre la cancelación en la PPL.

Control de errores en una cadena de tareas

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

Para administrar errores y la cancelación en una cadena de tareas, no tienes que hacer que todas las continuaciones se basen en tareas ni adjuntar cada operación que pueda iniciarse dentro de un bloque try…catch. En cambio, 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, se propagará hacia abajo en la cadena de tareas y eludirá cualquier continuación basada en valores, para que puedas administrarla en la continuación basada en tareas de control de errores. Podemos reescribir 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 fue una IAsyncAction que no produce ningún resultado porque task::get también obtiene cualquier excepción que se haya transportado hacia abajo a la tarea. Si la tarea de entrada está almacenando una excepción, se inicia en la llamada a task::get. Si no llamas a task::get o no usas una continuación basada en tareas al final de la cadena, o no capturas el tipo de excepción iniciado, se inicia una unobserved_task_exception cuando se hayan eliminado todas las referencias a la tarea.

Solamente captura las excepciones que puedas administrar. Si tu aplicación se encuentra con un error del que no puedes recuperarte, es mejor dejar que se bloquee a dejar que continúe ejecutándose en un estado desconocido. Además, en general, no intentes capturar la propia unobserved_task_exception. Esta excepción principalmente se usa para fines de diagnóstico. Generalmente, cuando se inicia unobserved_task_exception, indica un error en el código. Normalmente, la causa es una excepción que debe controlarse o una excepción irrecuperable que otro error en el código causó.

Administración del contenido del subproceso

La interfaz de usuario de una aplicación de Windows en tiempo de ejecución se ejecuta en un contenedor uniproceso (STA). Una tarea cuyo lambda devuelve IAsyncAction o IAsyncOperation reconoce contenedores. De manera predeterminada, si la tarea se crea en el STA, todas sus continuaciones se ejecutarán también en él, a menos que especifiques lo contrario. En otras palabras, toda la cadena de tareas hereda el reconocimiento de apartamentos de la tarea primaria. Este comportamiento ayuda a simplificar las interacciones con los controles de la interfaz de usuario, a la que solamente puede obtenerse acceso desde el STA.

Por ejemplo, en una aplicación de Windows en tiempo de ejecución, en la función miembro de cualquier clase que representa una página XAML, puedes rellenar un control ListBox desde dentro de 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 reconoce apartamentos y, de manera predeterminada, sus continuaciones se ejecutan en el primer subproceso en segundo plano disponible.

Puedes invalidar el contexto de subproceso predeterminado para cualquier tipo de tarea mediante el uso de la sobrecarga de task::then que toma un task_continuation_context. Por ejemplo, en algunos casos, probablemente quieras programar la continuación de una tarea que reconoce 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 contenedor multiproceso. Esto puede mejorar el rendimiento de la continuación porque su trabajo no tiene que estar sincronizado con otro trabajo que se esté realizando en el subproceso de interfaz de usuario.

El siguiente ejemplo demuestra cuándo es útil especificar la opción task_continuation_context::use_arbitrary y también muestra cómo el contexto de continuación predeterminado es útil para sincronizar operaciones simultáneas en colecciones no seguras para subprocesos. En este código, repetimos una lista de URL para fuentes RSS, y para cada URL, iniciamos una operación asincrónica para recuperar los datos de fuente. No podemos controlar el orden en el que se recuperan las fuentes. En realidad, no nos interesa hacerlo. 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 otras, podemos acelerar el proceso especificando el contexto de continuación task_continuation_context::use_arbitrary. No obstante, después de que se inicializa cada objeto FeedData, tenemos que agregarlo a un Vector, que no es una colección segura para subprocesos. Por lo tanto, podemos crear una continuación y especificar task_continuation_context::use_current para garantizar que todas las llamadas a Append se produzcan en el mismo contexto ASTA (contenedor de subproceso único de aplicaciones). Dado que task_continuation_context::use_default es el contexto predeterminado, no tenemos que especificarlo explícitamente, pero lo hacemos aquí por razones 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 tareas nuevas que se crean dentro de una continuación, no heredan el reconocimiento de apartamentos de la tarea inicial.

Control de actualizaciones de progreso

Los métodos que admiten IAsyncOperationWithProgress o IAsyncActionWithProgress proporcionan actualizaciones de progreso periódicamente mientras que 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. Eres tan solo el delegado de la propiedad Progress del objeto. Un uso típico del delegado es actualizar una barra de progreso en la interfaz de usuario.

Temas relacionados

Creación de operaciones asincrónicas en C++ para aplicaciones de la Tienda Windows
Referencia del lenguaje Visual C++
Guía básica para crear aplicaciones de Windows en tiempo de ejecución con C++
Programación asincrónica
Paralelismo de tareas (tiempo de ejecución de simultaneidad)
task class

 

 

Mostrar:
© 2014 Microsoft