C++/WinRT를 통한 동시성 및 비동기 작업Concurrency and asynchronous operations with C++/WinRT

이 항목에서는 C++/WinRT를 통해 Windows 런타임 비동기 개체를 만들고 사용하는 방법을 보여 줍니다.This topic shows the ways in which you can both create and consume Windows Runtime asynchronous objects with C++/WinRT.

이 항목을 참조한 후에 고급 동시성 및 비동기도 참조하세요.After reading this topic, also see More advanced concurrency and asynchrony for further scenarios.

비동기 작업 및 Windows 런타임 “비동기” 함수Asynchronous operations and Windows Runtime "Async" functions

완료하는 데 50밀리초 이상 걸릴 가능성이 높은 Windows 런타임 API는 비동기 함수(이름이 “Async”로 끝나는 함수)로 구현됩니다.Any Windows Runtime API that has the potential to take more than 50 milliseconds to complete is implemented as an asynchronous function (with a name ending in "Async"). 비동기 함수 구현은 다른 스레드에서 작업을 시작하고 비동기 작업을 나타내는 개체와 함께 즉시 반환됩니다.The implementation of an asynchronous function initiates the work on another thread and returns immediately with an object that represents the asynchronous operation. 비동기 작업이 완료되면, 반환된 개체에 작업의 결과 값이 포함됩니다.When the asynchronous operation completes, that returned object contains any value that resulted from the work. Windows::Foundation Windows 런타임 네임스페이스에는 네 가지 유형의 비동기 작업 개체가 포함됩니다.The Windows::Foundation Windows Runtime namespace contains four types of asynchronous operation object.

각 비동기 작업 유형은 winrt::Windows::Foundation C++/WinRT 네임스페이스의 해당 유형에 프로젝션됩니다.Each of these asynchronous operation types is projected into a corresponding type in the winrt::Windows::Foundation C++/WinRT namespace. C++/WinRT에는 내부 await 어댑터 구조체도 포함되어 있습니다.C++/WinRT also contains an internal await adapter struct. 직접 사용하지는 않지만, 이 구조체 덕분에 co_await 문을 작성하여 비동기 작업 유형 중 하나를 반환하는 함수의 결과를 협조적으로 기다릴 수 있습니다.You don't use it directly but, thanks to that struct, you can write a co_await statement to cooperatively await the result of any function that returns one of these asychronous operation types. 또한 이러한 유형을 반환하는 고유한 코루틴을 작성할 수 있습니다.And you can author your own coroutines that return these types.

비동기 Windows 함수의 예로 IAsyncOperationWithProgress<TResult, TProgress> 형식의 비동기 작업 개체를 반환하는 SyndicationClient::RetrieveFeedAsync가 있습니다.An example of an asynchronous Windows function is SyndicationClient::RetrieveFeedAsync, which returns an asynchronous operation object of type IAsyncOperationWithProgress<TResult, TProgress>. C++/WinRT를 사용하여 이러한 API를 호출하는 몇 가지 방법을 차단 방법과 비차단 방법 순으로 살펴보겠습니다.Let's look at some ways—first blocking, and then non-blocking—of using C++/WinRT to call an API such as that.

호출 스레드 차단Block the calling thread

아래 코드 예제는 RetrieveFeedAsync에서 비동기 작업 개체를 받은 후 해당 개체에서 get을 호출하여 비동기 작업 결과가 제공될 때까지 호출 스레드를 차단합니다.The code example below receives an asynchronous operation object from RetrieveFeedAsync, and it calls get on that object to block the calling thread until the results of the asynchronous operation are available.

이 예제를 복사하여 Windows 콘솔 애플리케이션(C++/WinRT) 프로젝트의 주 소스 코드 파일에 직접 붙여넣으려는 경우 먼저 프로젝트 속성에서 미리 컴파일된 헤더 사용 안 함을 설정합니다.If you want to copy-paste this example directly into the main source code file of a Windows Console Application (C++/WinRT) project, then first set Not Using Precompiled Headers in project properties.

// main.cpp
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Web.Syndication.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

void ProcessFeed()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;
    SyndicationFeed syndicationFeed{ syndicationClient.RetrieveFeedAsync(rssFeedUri).get() };
    // use syndicationFeed.
}

int main()
{
    winrt::init_apartment();
    ProcessFeed();
}

get을 호출하면 편리하게 코딩할 수 있으며 어떤 이유로든 코루틴을 사용하지 않으려는 콘솔 앱이나 백그라운드 스레드에 적합합니다.Calling get makes for convenient coding, and it's ideal for console apps or background threads where you may not want to use a coroutine for whatever reason. 하지만 동시 또는 비동기가 아니므로 UI 스레드에는 적합하지 않으며, 둘 중 하나에서 사용하려고 하면 최적화되지 않은 빌드에서 어설션이 발생합니다.But it's not concurrent nor asynchronous, so it's not appropriate for a UI thread (and an assertion will fire in unoptimized builds if you attempt to use it on one). 따라서 OS 스레드 정체로 인해 다른 유용한 작업을 수행하지 못하는 경우를 방지하려면 다른 기술이 필요합니다.To avoid holding up OS threads from doing other useful work, we need a different technique.

코루틴 작성Write a coroutine

C++/WinRT는 C++ 코루틴을 프로그래밍 모델에 통합하여 결과를 협조적으로 기다릴 수 있는 자연스러운 방법을 제공합니다.C++/WinRT integrates C++ coroutines into the programming model to provide a natural way to cooperatively wait for a result. 코루틴을 작성하여 고유한 Windows 런타임 비동기 작업을 생성할 수 있습니다.You can produce your own Windows Runtime asynchronous operation by writing a coroutine. 아래 코드 예제에서는 ProcessFeedAsync가 코루틴입니다.In the code example below, ProcessFeedAsync is the coroutine.

참고

get 함수가 C++/WinRT 프로젝션 형식 winrt::Windows::Foundation::IAsyncAction에 있으므로 C++/WinRT 프로젝트 내에서 함수를 호출할 수 있습니다.The get function exists on the C++/WinRT projection type winrt::Windows::Foundation::IAsyncAction, so you can call the function from within any C++/WinRT project. 실제 Windows 런타임 형식 IAsyncAction의 ABI(애플리케이션 이진 인터페이스) 표면에 속하지 않으므로 get 함수는 IAsyncAction 인터페이스의 멤버로 나열되지 않습니다.You will not find the function listed as a member of the IAsyncAction interface, because get is not part of the application binary interface (ABI) surface of the actual Windows Runtime type IAsyncAction.

// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Web.Syndication.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

void PrintFeed(SyndicationFeed const& syndicationFeed)
{
    for (SyndicationItem const& syndicationItem : syndicationFeed.Items())
    {
        std::wcout << syndicationItem.Title().Text().c_str() << std::endl;
    }
}

IAsyncAction ProcessFeedAsync()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;
    SyndicationFeed syndicationFeed{ co_await syndicationClient.RetrieveFeedAsync(rssFeedUri) };
    PrintFeed(syndicationFeed);
}

int main()
{
    winrt::init_apartment();

    auto processOp{ ProcessFeedAsync() };
    // do other work while the feed is being printed.
    processOp.get(); // no more work to do; call get() so that we see the printout before the application exits.
}

코루틴은 일시 중단했다가 다시 시작할 수 있는 함수입니다.A coroutine is a function that can be suspended and resumed. 위의 ProcessFeedAsync 코루틴에서는 co_await 문에 도달할 때 코루틴이 비동기 방식으로 RetrieveFeedAsync 호출을 시작한 후 즉시 일시 중단되고 컨트롤을 호출자(위 예제에서는 main)에 반환합니다.In the ProcessFeedAsync coroutine above, when the co_await statement is reached, the coroutine asynchronously initiates the RetrieveFeedAsync call and then it immediately suspends itself and returns control back to the caller (which is main in the example above). 그러면 피드를 검색하고 출력하는 동안 main이 작업을 계속할 수 있습니다.main can then continue to do work while the feed is being retrieved and printed. 작업이 완료되면(RetrieveFeedAsync 호출 완료 시) ProcessFeedAsync 코루틴이 다음 문에서 다시 시작됩니다.When that's done (when the RetrieveFeedAsync call completes), the ProcessFeedAsync coroutine resumes at the next statement.

코루틴을 다른 코루틴에 집계할 수 있습니다.You can aggregate a coroutine into other coroutines. 또는 get을 호출하여 차단하고 완료될 때까지 기다린 다음, 결과가 있을 경우 가져올 수 있습니다.Or you can call get to block and wait for it to complete (and get the result if there is one). 또는 Windows 런타임을 지원하는 다른 프로그래밍 언어에 전달할 수 있습니다.Or you can pass it to another programming language that supports the Windows Runtime.

대리자를 사용하여 비동기 작업의 완료 및/또는 진행률 이벤트를 처리할 수도 있습니다.It's also possible to handle the completed and/or progress events of asynchronous actions and operations by using delegates. 자세한 내용과 코드 예제는 비동기 작업을 위한 대리자 형식을 참조하세요.For details, and code examples, see Delegate types for asynchronous actions and operations.

Windows 런타임 형식의 비동기 반환Asychronously return a Windows Runtime type

다음 예제에서는 특정 URI에 대해 RetrieveFeedAsync 호출을 래핑하여 SyndicationFeed를 비동기 방식으로 반환하는 RetrieveBlogFeedAsync 함수를 제공합니다.In this next example we wrap a call to RetrieveFeedAsync, for a specific URI, to give us a RetrieveBlogFeedAsync function that asynchronously returns a SyndicationFeed.

// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Web.Syndication.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

void PrintFeed(SyndicationFeed const& syndicationFeed)
{
    for (SyndicationItem const& syndicationItem : syndicationFeed.Items())
    {
        std::wcout << syndicationItem.Title().Text().c_str() << std::endl;
    }
}

IAsyncOperationWithProgress<SyndicationFeed, RetrievalProgress> RetrieveBlogFeedAsync()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;
    return syndicationClient.RetrieveFeedAsync(rssFeedUri);
}

int main()
{
    winrt::init_apartment();

    auto feedOp{ RetrieveBlogFeedAsync() };
    // do other work.
    PrintFeed(feedOp.get());
}

위의 예제에서 RetrieveBlogFeedAsync는 진행률과 반환 값이 둘 다 있는 IAsyncOperationWithProgress를 반환합니다.In the example above, RetrieveBlogFeedAsync returns an IAsyncOperationWithProgress, which has both progress and a return value. RetrieveBlogFeedAsync가 작업을 수행하고 피드를 검색하는 동안 다른 작업을 수행할 수 있습니다.We can do other work while RetrieveBlogFeedAsync is doing its thing and retrieving the feed. 해당 비동기 작업 개체에서 get을 호출하여 차단하고 완료될 때까지 기다린 다음, 작업 결과를 가져옵니다.Then, we call get on that asynchronous operation object to block, wait for it to complete, and then obtain the results of the operation.

Windows 런타임 형식을 비동기 방식으로 반환하는 경우 IAsyncOperation<TResult> 또는 IAsyncOperationWithProgress<TResult, TProgress> 를 반환해야 합니다.If you're asynchronously returning a Windows Runtime type, then you should return an IAsyncOperation<TResult> or an IAsyncOperationWithProgress<TResult, TProgress>. 자사 또는 타사 런타임 클래스는 Windows 런타임 함수로 전달하거나 전달받을 수 있는 형식(예: int 또는 winrt::hstring)을 한정합니다.Any first- or third-party runtime class qualifies, or any type that can be passed to or from a Windows Runtime function (for example, int, or winrt::hstring). Windows 런타임이 아닌 형식에 이러한 비동기 작업 유형 중 하나를 사용하려고 하면 컴파일러에서 “WinRT 형식이어야 합니다.” 오류를 생성하여 도와줍니다. The compiler will help you with a "must be WinRT type" error if you try to use one of these asychronous operation types with a non-Windows Runtime type.

코루틴에 co_await 문이 없는 경우, 코루틴이 되려면 co_return 또는 co_yield 문이 하나 이상 있어야 합니다.If a coroutine doesn't have at least one co_await statement then, in order to qualify as a coroutine, it must have at least one co_return or one co_yield statement. 코루틴이 비동기성을 도입하지 않아 컨텍스트를 차단하거나 전환하지 않고 값을 반환할 수 있는 경우도 있습니다.There will be cases where your coroutine can return a value without introducing any asynchrony, and therefore without blocking nor switching context. 다음은 값을 캐시하여 두 번째 이상 호출 시 해당 작업을 수행하는 예제입니다.Here's an example that does that (the second and subsequent times it's called) by caching a value.

winrt::hstring m_cache;

IAsyncOperation<winrt::hstring> ReadAsync()
{
    if (m_cache.empty())
    {
        // Asynchronously download and cache the string.
    }
    co_return m_cache;
}

Windows 런타임이 아닌 형식의 비동기 반환Asychronously return a non-Windows-Runtime type

Windows 런타임 형식이 ‘아닌’ 형식을 비동기 방식으로 반환하는 경우 PPL(병렬 패턴 라이브러리) concurrency::task를 반환해야 합니다. If you're asynchronously returning a type that's not a Windows Runtime type, then you should return a Parallel Patterns Library (PPL) concurrency::task. std::future보다 성능이 뛰어나고 향후 호환성도 우수한 concurrency::task를 사용하는 것이 좋습니다.We recommend concurrency::task because it gives you better performance (and better compatibility going forward) than std::future does.

<pplawait.h>를 포함하면, concurrency::task를 코루틴 형식으로 사용할 수 있습니다.If you include <pplawait.h>, then you can use concurrency::task as a coroutine type.

// main.cpp
#include <iostream>
#include <ppltasks.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Web.Syndication.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

concurrency::task<std::wstring> RetrieveFirstTitleAsync()
{
    return concurrency::create_task([]
        {
            Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
            SyndicationClient syndicationClient;
            SyndicationFeed syndicationFeed{ syndicationClient.RetrieveFeedAsync(rssFeedUri).get() };
            return std::wstring{ syndicationFeed.Items().GetAt(0).Title().Text() };
        });
}

int main()
{
    winrt::init_apartment();

    auto firstTitleOp{ RetrieveFirstTitleAsync() };
    // Do other work here.
    std::wcout << firstTitleOp.get() << std::endl;
}

매개 변수 전달Parameter-passing

동기 함수의 경우 기본적으로 const& 매개 변수를 사용해야 합니다.For synchronous functions, you should use const& parameters by default. 그러면 참조 계산을 포함하며 연동된 증가 및 감소를 의미하는 복사본 오버헤드를 방지할 수 있습니다.That will avoid the overhead of copies (which involve reference counting, and that means interlocked increments and decrements).

// Synchronous function.
void DoWork(Param const& value);

하지만 코루틴에 참조 매개 변수를 전달하는 경우 문제가 발생할 수 있습니다.But you can run into problems if you pass a reference parameter to a coroutine.

// NOT the recommended way to pass a value to a coroutine!
IASyncAction DoWorkAsync(Param const& value)
{
    // While it's ok to access value here...

    co_await DoOtherWorkAsync(); // (this is the first suspension point)...

    // ...accessing value here carries no guarantees of safety.
}

코루틴에서 실행은 첫 번째 일시 중단 지점까지 동기화됩니다. 이 경우 컨트롤이 호출자에 반환되고 호출하는 프레임이 범위를 벗어납니다.In a coroutine, execution is synchronous up until the first suspension point, where control is returned to the caller and the calling frame goes out of scope. 코루틴이 다시 시작될 때까지 참조 매개 변수가 참조하는 소스 값이 변경되었을 수 있습니다.By the time the coroutine resumes, anything might have happened to the source value that a reference parameter references. 코루틴의 관점에서 참조 매개 변수의 수명은 제어되지 않습니다.From the coroutine's perspective, a reference parameter has uncontrolled lifetime. 따라서 위 예제에서 co_await까지는 ‘값’에 액세스해도 안전하지만 이후에는 안전하지 않습니다. So, in the example above, we're safe to access value up until the co_await, but not after it. 호출자에 의해 이 소멸되는 이벤트에서 그 이후 코루틴 내의 해당 값에 액세스하려고 하면 메모리가 손상됩니다.In the event that value is destructed by the caller, attempting to access it inside the coroutine after that results in a memory corruption. 함수가 일시 중단되었다가 다시 시작된 후 ‘값’을 사용하려고 시도할 위험이 있는 경우 DoOtherWorkAsync에 ‘값’을 안전하게 전달할 수도 없습니다. Nor can we safely pass value to DoOtherWorkAsync if there's any risk that that function will in turn suspend and then try to use value after it resumes.

일시 중단했다가 다시 시작한 후 매개 변수를 안전하게 사용하려면 코루틴이 기본적으로 값으로 전달을 사용하여 값으로 캡처함으로써 수명 문제를 방지해야 합니다.To make parameters safe to use after suspending and resuming, your coroutines should use pass-by-value by default to ensure that they capture by value, and avoid lifetime issues. 이 지침을 따르지 않아도 안전하다고 확신할 수 있는 경우는 흔치 않습니다.Cases when you can deviate from that guidance because you're certain that it's safe to do so are going to be rare.

// Coroutine
IASyncAction DoWorkAsync(Param value); // not const&

값으로 전달하기 위해서는 저비용으로 인수를 이동 또는 복사할 수 있어야 하며, 이는 일반적으로 스마트 포인터에서 흔한 경우입니다.Passing by value requires that the argument be inexpensive to move or copy; and that's typically the case for a smart pointer.

값을 이동하려는 경우가 아니면, const 값으로 전달하는 것이 좋다는 주장도 가능합니다.It's also arguable that (unless you want to move the value) passing by const value is good practice. 복사본을 만드는 소스 값에는 영향을 미치지 않지만 의도를 보다 명확하게 하며, 실수로 복사본을 수정하는 경우 도움이 됩니다.It won't have any effect on the source value from which you're making a copy, but it makes the intent clear, and helps if you inadvertently modify the copy.

// coroutine with strictly unnecessary const (but arguably good practice).
IASyncAction DoWorkAsync(Param const value);

표준 벡터를 비동기 호출 수신자에 전달하는 방법을 설명하는 표준 배열 및 벡터도 참조하세요.Also see Standard arrays and vectors, which deals with how to pass a standard vector into an asynchronous callee.

코루틴의 서명은 변경할 수 없지만 구현은 변경할 수 있는 경우에는 첫 번째 co_await 전에 로컬 복사본을 만들 수 있습니다.If you can't change your coroutine's signature, but you can change the implementation, then you can make a local copy before the first co_await.

IASyncAction DoWorkAsync(Param const& value)
{
    auto safe_value = value;
    // It's ok to access both safe_value and value here.

    co_await DoOtherWorkAsync();

    // It's ok to access only safe_value here (not value).
}

Param 복사에 비용이 많이 들면 첫 번째 co_await 전에 필요한 구성 요소를 추출합니다.If Param is expensive to copy, then extract just the pieces you need before the first co_await.

IASyncAction DoWorkAsync(Param const& value)
{
    auto safe_data = value.data;
    // It's ok to access safe_data, value.data, and value here.

    co_await DoOtherWorkAsync();

    // It's ok to access only safe_data here (not value.data, nor value).
}

클래스-멤버 코루틴에서 안전하게 this 포인터 액세스Safely accessing the this pointer in a class-member coroutine

C++/WinRT의 강한 참조 및 약한 참조를 참조하세요.See Strong and weak references in C++/WinRT.

중요 APIImportant APIs