C++/CX의 비동기 프로그래밍

참고 항목

이 항목은 C++/CX 애플리케이션 유지에 도움을 주기 위해 작성되었습니다. 하지만 새로운 응용 프로그램에 대해 C++/WinRT를 사용하는 것이 좋습니다. C++/WinRT는 헤더 파일 기반 라이브러리로 구현된 WinRT(Windows 런타임) API용 완전한 표준 C++17 언어 프로젝션이며, 최신 Windows API에 대해 최고 수준의 액세스를 제공하도록 설계되었습니다.

이 문서는 ppltasks.h의 동시성 네임스페이스에 task 정의된 테스크 클라스 concurrency 를 사용해서 Visual C++ 구성 요소 확장(C++/CX)에서 비동기 메서드를 활용하는 권장된 사용법을 설명합니다.

Windows 런타임 비동기 형식

Windows 런타임은 비동기 메서드를 호출하기 위한 잘 정의된 모델을 제공하고 이러한 메서드를 사용하는 데 필요한 형식을 제공합니다. Windows 런타임 비동기 모델이 생소한 경우 이 문서의 나머지 부분을 읽기 전 비동기 프로그래밍 을 숙지하세요.

C++에서 직접 비동기 Windows 런타임 API를 사용할 수 있지만 기본 접근 방식은 테스크 클래스 와 관련 형식 및 함수를 사용하는 것입니다. ( 동시성 네임스페이스에 포함되고 정의된) <ppltasks.h>. concurrency::task 클래스의 유형은 일반용이지만 /ZW 컴파일러 스위치(UWP(유니버설 Windows 플랫폼) 앱 및 구성 요소에 필요)가 사용될 경우에는 task 클래스가 Windows 런타임 비동기 유형을 캡슐화하여 다음 작업을 수행하기가 쉬워집니다.

  • 여러 동기, 비동기 작업을 함께 연결합니다.

  • 태스크 체인에서 예외상황 처리

  • 테스크 체인에서 취소처리

  • 개별 테스크가 적절한 스레드 컨텍스트 또는 아파트먼트에서 실행되는지 확인하세요

이 문서는 Windows 런타임 비동기 API와 함께 task 클래스를 사용하는 방법에 대한 기본 참고 자료를 제공합니다. 작업create_task를 포함하는 메서드에 대한 자세한 내용은 작업 병렬 처리(동시성 런타임) 을 참조하세요.

테스크를 사용하여 비동기 작업을 활용하는 것

다음 예제는 테스크 클래스를 활용하여 비동기 메서드, 즉 IAsyncOperation 인터페이스를 출력하고 값을 생성하는 메서드 사용법을 보여 줍니다. 기본 단계는 다음과 같습니다.

  1. create_task 메서드를 호출하고 IAsyncOperation^ 개체를 전송합니다.

  2. 테스크에서 멤버 함수 task::then 을 호출하고 비동기 작업이 완료될 시 호출될 람다를 제공합니다.

#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...
}

task::then 함수를 사용해 생성과 출력하는 테스크를 연속작업 이라고 합니다. 이 경우 사용자가 제공한 람다에 대한 입력 인수는 작업이 완료될 때 생성되는 결과값입니다. IAsyncOperation::GetResults를 호출하여 검색된 결과는 IAsyncOperation 인터페이스를 직접 사용한 결과와 같습니다.

task::then 메서드는 즉시 결과를 출력하고 대리자는 비동기 작업이 성공적으로 완료될 때까지 실행되지 않습니다. 이 예제에서는, 비동기 작업으로 인해 예외가 허용되거나 취소 요청의 결과 취소 상태로 끝나는 경우 연속 작업이 절대 실행되지 않습니다. 이전 테스크가 취소되거나 실패한 경우에도 실행되는 연속 작업을 작성하는 방법을 나중에 설명할겁니다.

로컬 스택에서 테스크 변수를 설정하지만, 작업이 완료되기 전에 메서드가 반환되더라도 모든 작업이 완료되고 모든 참조가 범위를 벗어날 때까지 삭제되지 않도록 유지 기간을 관리합니다.

테스트 체인 만들기

비동기 프로그래밍에서는 테스크 체인 이라고 하는 작업 시퀸스를 정의하는 것이 일반적이며, 각 연속 작업은 이전 작업이 완료된 경우에만 실행됩니다. 경우에 따라, 이전 (또는 선행) 테스트는 연속 작업이 입력 허용하는 값을 생성합니다. task::then 메서드를 사용함으로서, 직관적이고 간단한 방식으로 테스크 체인을 만들 수 있습니다. 이 메서드는 테스크<T> 을 출력하며, 여기서 T 는 람다 함수의 출력 형식입니다. 하나의 태스크 체인에 여러 연속 작업을 작성할 수 있습니다(예: myTask.then(…).then(…).then(…);).

테스크 체인은 연속 작업에서 새 비동기 작업을 만들 때 특히 유용합니다. 이러한 테스크를 비동기 테스크라고 합니다. 다음 예제는 두 개의 연속 작업이 있는 테스크 체인을 보여 줍니다. 최초 테스크는 기존 파일에 대한 처리법을 획득하고 해당 작업이 완료되면 첫 번째 연속 작업이 새 비동기 작업을 시작하여 파일을 삭제합니다. 해당 작업이 완료되면 두 번째 연속 작업이 실행되고 확인 메시지를 출력합니다.

#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.");
    });
}

앞의 예제는 다음과 같은 4가지 중요 포인트를 설명합니다.

  • 첫 번째 연속 작업은 IAsyncAction^ 개체를 테스크<보이드> 로 변환하고 테스크를 출력합니다.

  • 두 번째 연속 작업은 오류 처리를 수행하지 않으며, 따라서 입력으로 task<void>가 아닌 void를 사용합니다. 값 기반 연속 작업입니다.

  • 두 번째 연속 작업은 DeleteAsync 작업 완료 전까지 실행되지 않습니다.

  • 두 번째 연속 작업은 값 기반이므로 DeleteAsync 호출에 의해 시작된 작업에 예외가 발생할시 절대 실행되지 않습니다.

참고 테스크 체인을 만드는 것은 테스크 클래스를 사용하여 비동기 작업을 작성하는 방법 중 하나일 뿐입니다. 조인 및 선택 연산자 &&</strong> 그리고 ||를 사용해서 작업을 만들 수도 있습니다. 자세한 내용은 테스크 병렬 처리(동시성 런타임)를 참조하세요.

람다 함수 출력 형식 및 테스크 출력 형식

테스크 연속작업에서 람다 함수의 출력 형식은 테스크 개체에 래핑됩니다. 람다가 double을 반환하면 연속 작업의 작업 유형은 task<double>가 됩니다. 그러나 테스크 개체는 불필요하게 중첩된 출력 형식을 생성하지 않도록 설계되었습니다. 람다가 IAsyncOperation<SyndicationFeed^>^를 반환하는 경우 연속 작업은 task<task<SyndicationFeed^>> 또는 task<IAsyncOperation<SyndicationFeed^>^>^가 아닌 task<SyndicationFeed^>를 반환합니다. 이 프로세스를 비동기 래핑 해제 하고 하며 다음 연속 작업이 시행되기 전에 연속 작업 내의 비동기 작업이 완료되도록 합니다.

앞의 예제에서 테스크가 테스크<보이드> 를 출력하는 것을 확인하세요, 람다가 IAsyncInfo 개체를 출력하는 상황에서라도 말입니다. 다음 테이블은 람다 함수와 밀폐 테스크 간에 발생하는 형식 변환이 요약되어 있습니다.

람다 출력 유형 .then 반환 형식
TResult task<TResult>
IAsyncOperation<TResult>^ task<TResult>
IAsyncOperationWithProgress<TResult, TProgress>^ task<TResult>
IAsyncAction^ task<void>
IAsyncActionWithProgress<TProgress>^ task<void>
task<TResult> task<TResult>

작업 취소

사용자에게 비동기 작업을 취소할 수 있는 옵션을 제공하는 것이 권장됩니다. 경우에 따라 테스크 체인 외부에서 프로그래밍 방식으로 작업을 취소해야 할 수도 있습니다. 각 *Async 출력 형식에 취소 메서드, 즉 IAsyncInfo에서 이어받은 메서드가 존재하지만 , 외부 메서드에 노출시키는 것은 어색합니다. 태스크 체인에서 취소를 지원하는 좋은 방법은 cancellation_token_source를 사용하여 cancellation_token을 만든 다음 이 토큰을 초기 작업의 생성자로 전달하는 것입니다. 취소 토큰으로 비동기 테스크를 만들고 [cancellation_token_source::cancel](/cpp/parallel/concrt/reference/cancellation-token-source-class?view=vs-2017& -view=true) 가 호출되면, 테스크는 자동으로 취소를 호출하고, 이는 IAsync* 작업안에서 이루어집니다. 그리고 취소 요청을 연속 체인 아래로 전송합니다. 다음 유사 부호는 기본 방식을 보여 줍니다.

//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 ...

테스크가 취소되면 task_canceled 예외가 테스크 체인 아래도 전파됩니다. 값 기반 연속 작업은 그저 실행되지 않지만 테스크 기반 연속 작업은 task::get 이 호출되면 예외를 허용합니다. 오류 처리 연속 작업이 있으면 task_canceled 예외를 명시적으로 catch하는지 확인하세요(이 예외는 (이 예외성은 Platform::Exception에서 기인하지 않습니다.)

작업 취소는 협조적입니다. 연속 작업이 UWP 메서드 호출 외 장기 실행 작업을 수행하는 경우 취소 토큰의 상태를 주기적으로 검사하고 취소된 경우 실행을 중지해야 합니다. 연속 작업에 할당된 모든 리소스를 정리한 후 cancel_current_task를 호출하여 해당 작업을 취소하고 값 기반의 후속 연속 작업에 취소를 하향 전파합니다. 여기 다른 예시가 있습니다: FileSavePicker 작업의 결과를 나타내는 테스크 체인 생성. 사용자가 취소 버튼을 선택하면, IAsyncInfo::Cancel 메서드는 호출되지 않습니다. 대신 작업은 성공합니다만 nullptr를 출력합니다. 연속 작업은 입력 매개 변수를 테스트하고 입력이 nullptr이면 cancel_current_task를 호출합니다.

자세한 내용은 PPL 취소를 참조하세요.

테스크 체인에서 오류를 처리하는 것

선행 작업이 취소되거나 예외가 허용되는 경우에도 연속 작업이 실행되길 원한다면 테스크<TResult> 아니면 테스크<보이드>로 람다 함수 입력값을 특정해서 테스크 기반 연속 작업을 만듭니다. 이는 선행 작업이 IAsyncAction^를 출력하는 경우에 해당합니다.

테스크 체인에서 오류 및 취소를 처리하려면 모든 연속 작업을 테스크 기반으로 만들거나 try…catch 블럭에서 예외를 허용하는 모든 작업을 폐쇄화 할 필요가 없습니다. 대신 체인의 끝에 테스크 기반 연속 작업을 추가하고 거기서 모든 오류를 처리할 수 있습니다. —취소된 테스크 예외를 포함하는— 모든 예외상황은 테스크 체인 아래로 전파되고 값 기반 연속 작업을 우회하므로 오류 처리 테스크 기반 연속 작업에서 처리할 수 있습니다. 이전 예제를 다시 작성하여 오류 처리 테스크 기반 연속 작업을 사용할 수 있습니다.

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

    });
}

테스크 기반 연속 작업에서는 멤버 함수 task::get 을 호출하여 테스크 결과를 가져옵니다. task::get을 결국 호출해야 합니다. 작업이 결과를 생성하지 않는 IAsyncAction, 즉 task::get 또한 테스크로 전송된 어떤 예외사항이라도 가져오기에 발생한 결과이기 때문입니다. 입력 테스크가 예외사항을 저장하는 경우 task::get가 호출될 시 허용됩니다. task::get을 호출하지 않거나, 체인의 끝에 작업 기반 연속 작업을 사용하지 않거나, 발생한 예외 유형을 catch하지 않으면 작업에 대한 모든 참조가 삭제되었을 때 unobserved_task_exception이 발생합니다.

처리할 수 있는 예외만 포착하세요. 앱에서 복구 불가능한 오류가 발생하는 경우 앱이 알 수 없는 상태로 계속 실행되도록 하는 것보다 앱이 강제 종료되도록 하는 것이 좋습니다. 또한 일반적으로 unobserved_task_exception 자체는 catch하려고 하지 마세요. 이 예외는 주로 진단용도로써 사용됩니다. unobserved_task_exception이 발생하면 대개 코드에 버그가 있다는 뜻입니다. 원인은 종종 처리되야 하는 예외사항이거나 코드의 다른 오류로 인해 복구할 수 없는 예외사항입니다.

스레드 컨텍스트 관리하기

UWP 앱의 UI는 STA(단일 스레드 아파트먼트)에서 실행됩니다. 람다가 IAsyncAction 또는 IAsyncOperation 을 출력하는 테스크는 아파트먼트를 인식합니다. STA에서 작업이 만들어진 경우 달리 지정하지 않는 한 모든 연속 작업이 기본적으로 실행됩니다. 즉, 전체 테스크 체인은 페어런트 테스크에서 아파트먼트 인식을 물려받습니다. 이 동작은 STA에서만 액세스할 수 있는 UI 컨트롤과의 상호 작용을 간소화하는 데 도움을 줍니다.

예들 들어 UWP 앱에서 XAML 페이지를 나타내는 어떤 클래스의 맴버 함수일지라도 ListBox 컨트롤을 덪붙일 수 있습니다. 이는 task::then 메서드 내에서 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;
    });
}

태스크가 IAsyncAction 혹은 IAsyncOperation 을 도출하지 않는다면, 아파트먼트를 인식하지 않으며, 기본적으로 해당 연속 작업은 사용 가능한 첫 번째 백그라운드 스레드에서 실행됩니다.

task::then 의 오버로드, 즉 task_continuation_context를 가지는 오버로드를 사용함으로써 두 종류의 태스크의 기본 스레스 컨텍스트를 중단 시킬수 있습니다. 예를 들어 경우에 따라 백그라운드 스레드에서 아파트먼트 인식 연속 작업을 짜 놓는 것이 좋습니다. 이 경우 task_continuation_context::use_arbitrary를 전송해서 다중 스레드 아파트먼트에서 테스크의 사용 가능한 다음 스레드 작업을 예약할 수 있습니다. 이렇게 하면 UI 스레드에서 발생하는 다른 작업과 동기화할 필요가 없으므로 연속 작업의 성능이 향상될 수 있습니다.

다음 예제는 task_continuation_context::use_arbitrary 옵션 지정이 유용한 경우와, 스레드로부터 안전하지 않은 컬렉션에서 동시 작업을 동기화에 왜 기본 연속 컨텍스트가 유용한지 보여줍니다. 이 코드에서는 RSS 피드에 대한 URL 목록을 루프하고 각 URL에 대해 피드 데이터를 추출하는 비동기 작업을 시작합니다. 피드가 추출되는 순서를 제어할 수는 없으며 실제로 상관 없습니다. 각 RetrieveFeedAsync 작업이 완료되면 첫 번째 연속 작업은 SyndicationFeed^ 개체를 수락하고 이를 사용하여 앱 정의 FeedData^ 개체를 초기화합니다. 이러한 각각의 작업은 상호 독립적이므로 task_continuation_context::use_arbitrary 연속 작업 컨텍스트를 지정하여 잠재적으로 속도를 높일 수 있습니다. 그러나 각 FeedData 개체가 초기화되면 스레드들이 자유롭게 접속하는 컬렉션이 아닌 Vector에 추가해야 합니다. 따라서 연속 작업을 만들고 [task_continuation_context::use_current](/cpp/parallel/concrt/reference/task-continuation-context-class?view=vs-2017& -view=true) 를 특정해서 동일한 ASTA(Application Single-Threaded Apartment) 컨텍스트에서 첨부에 대한 모든 호출이 발생되도록 합니다. task_continuation_context::use_default는 기본 컨텍스트이므로 명시적으로 지정할 필요가 없지만 여기서는 명확히 하기 위해 지정합니다.

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

연속 작업 내에서 생성되는 새 작업인 중첩 작업은 최초 태스크의 아파트먼트 인식을 물려받지 않습니다.

진행율 업데이트 처리

IAsyncOperationWithProgress 또는 IAsyncActionWithProgress를 지원하는 메서드는 작업이 완료되기 전에 진행율 업데이트를 주기적으로 제공합니다. 진행률 보고는 태스크 및 연속 작업 개념과는 독립적입니다. 개체의 진행률 속성의 대리자만 제공합니다. 대리자의 일반적인 사용은 UI에서 진행률 바를 갱신하는 것입니다.