C++/WinRT ile gelişmiş eşzamanlılık ve eşzamansızlık

Bu konuda, C++/WinRT'de eşzamanlılık ve eşzamansızlıkla ilgili gelişmiş senaryolar ele alınmaktadır.

Bu konuya giriş için önce Eşzamanlılık ve zaman uyumsuz işlemler konusunu okuyun.

İşi Windows iş parçacığı havuzuna devretme

Bir korutin, diğer herhangi bir işlev gibi, çağıranın işlev yürütmeyi kendisine geri verene kadar engellendiği bir işlevdir. Ve bir eş yordamın geri dönebileceği ilk nokta, ilk co_await, co_return veya co_yield’dir.

Bu nedenle, bir eş yordamda yoğun hesaplama gerektiren bir iş yapmadan önce, çağıranın engellenmemesi için yürütmenin denetimini çağırana geri vermeniz gerekir (başka bir deyişle, bir askıya alma noktası oluşturmanız gerekir). Bunu zaten başka bir işlemi co_await yaparak yapmıyorsanız, co_awaitwinrt::resume_background işlevini kullanabilirsiniz. Bu, denetimi çağırana döndürür ve ardından bir iş parçacığı havuzu iş parçacığında yürütmeyi hemen sürdürür.

Uygulamada kullanılan iş parçacığı havuzu düşük düzeyli Windows iş parçacığı havuzu olduğundan en iyi şekilde verimlidir.

IAsyncOperation<uint32_t> DoWorkOnThreadPoolAsync()
{
    co_await winrt::resume_background(); // Return control; resume on thread pool.

    uint32_t result;
    for (uint32_t y = 0; y < height; ++y)
    for (uint32_t x = 0; x < width; ++x)
    {
        // Do compute-bound work here.
    }
    co_return result;
}

İş parçacığı yakınlığını göz önünde bulundurarak programlama

Bu senaryo bir önceki senaryoya göre genişler. Bazı işleri iş parçacığı havuzuna boşaltabilirsiniz, ancak ardından ilerleme durumunu kullanıcı arabiriminde (UI) görüntülemek istersiniz.

IAsyncAction DoWorkAsync(TextBlock textblock)
{
    co_await winrt::resume_background();
    // Do compute-bound work here.

    textblock.Text(L"Done!"); // Error: TextBlock has thread affinity.
}

Yukarıdaki kod bir winrt::hresult_wrong_thread özel durumu oluşturur, çünkü textBlock bunu oluşturan iş parçacığından güncelleştirilmelidir( ui iş parçacığıdır). Çözümlerden biri, coroutine'imizin ilk olarak çağrıldığı iş parçacığı bağlamını yakalamaktır. Bunu yapmak için bir winrt::apartment_context nesnesi örnekleyin, arka planda çalışma yapın ve ardından çağıran bağlama geri dönmek için co_awaitapartment_context kullanın.

IAsyncAction DoWorkAsync(TextBlock textblock)
{
    winrt::apartment_context ui_thread; // Capture calling context.

    co_await winrt::resume_background();
    // Do compute-bound work here.

    co_await ui_thread; // Switch back to calling context.

    textblock.Text(L"Done!"); // Ok if we really were called from the UI thread.
}

Yukarıdaki eş yordam, TextBlock'u oluşturan UI iş parçacığından çağrıldığı sürece bu teknik işe yarar. Uygulamanızda bundan emin olduğunuz birçok durum olacaktır.

Arama iş parçacığından emin olmadığınız durumları kapsayan kullanıcı arabirimini güncelleştirmeye yönelik daha genel bir çözüm için co_await işlevini kullanarak belirli bir ön plan iş parçacığına geçebilirsiniz. Aşağıdaki kod örneğinde, TextBlock ile ilişkili dağıtıcı kuyruğunu geçirerek ( DispatcherQueue özelliğine erişerek) ön plan iş parçacığını belirteceğiz. winrt::resume_foreground uygulaması, bu dağıtıcı kuyruğu nesnesinde DispatcherQueue.TryEnqueue'yi çağırarak coroutine içinde ondan sonra gelen işi yürütür.

IAsyncAction DoWorkAsync(TextBlock textblock)
{
    co_await winrt::resume_background();
    // Do compute-bound work here.

    // Switch to the foreground thread associated with textblock.
    co_await winrt::resume_foreground(textblock.DispatcherQueue());

    textblock.Text(L"Done!"); // Guaranteed to work.
}

winrt::resume_foreground işlevi isteğe bağlı bir öncelik parametresi alır. Bu parametreyi kullanıyorsanız, yukarıda gösterilen desen uygundur. Aksi takdirde, co_await winrt::resume_foreground(someDispatcherObject); öğesini yalnızca co_await someDispatcherObject; olacak şekilde basitleştirmeyi seçebilirsiniz.

Yürütme bağlamları, bir eş yordamda devam etme ve geçiş yapma

Genel olarak, bir eşyordamdaki bir askıya alma noktasından sonra özgün yürütme iş parçacığı artık var olmayabilir ve yürütmenin devamı herhangi bir iş parçacığında gerçekleşebilir (başka bir deyişle, herhangi bir iş parçacığı zaman uyumsuz işlemin Completed yöntemini çağırabilir).

Ancak, dört Windows Çalışma Zamanı zaman uyumsuz işlem türünden (co_await herhangi biriyseniz, C++/WinRT sizin olduğunuz noktada co_awaitçağrı bağlamını yakalar. Ayrıca, devam yeniden başladığında hâlâ aynı bağlamda olmanızı sağlar. C++/WinRT, zaten arama bağlamında olup olmadığınızı denetleyerek ve değilse buna geçiş yaparak bunu yapar. co_await öncesinde tek iş parçacıklı bir apartment (STA) iş parçacığındaysanız, sonrasında da aynı iş parçacığında olursunuz; co_await öncesinde çok iş parçacıklı bir apartment (MTA) iş parçacığındaysanız, sonrasında da bunlardan birinde olursunuz.

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

    // The thread context at this point is captured...
    SyndicationFeed syndicationFeed{ co_await syndicationClient.RetrieveFeedAsync(rssFeedUri) };
    // ...and is restored at this point.
}

Bu davranışa güvenebilmenizin nedeni, C++/WinRT'nin bu Windows Çalışma Zamanı zaman uyumsuz işlem türlerini C++ eş yordam dili desteğine uyarlamak için kod sağlamasıdır (bu kod parçalarına bekleme bağdaştırıcısı adı verilir). C++/WinRT'deki geri kalan await edilebilir türler yalnızca iş parçacığı havuzu sarmalayıcıları ve/veya yardımcı türlerdir; bu nedenle iş parçacığı havuzunda tamamlanırlar.

using namespace std::chrono_literals;
IAsyncOperation<int> return_123_after_5s()
{
    // No matter what the thread context is at this point...
    co_await 5s;
    // ...we're on the thread pool at this point.
    co_return 123;
}

co_await başka bir tür kullanırsanız —C++/WinRT ortak yordam uygulaması içinde bile olsa— uyarlayıcılar başka bir kitaplık tarafından sağlanır ve bu uyarlayıcıların yürütmenin sürdürülmesi ve bağlamlar açısından ne yaptığını anlamanız gerekir.

Bağlam geçişlerini en düşük düzeyde tutmak için bu konuda daha önce gördüğümüz tekniklerden bazılarını kullanabilirsiniz. Şimdi bunu yapmanın bazı çizimlerini görelim. Bu sonraki sahte kod örneğinde, bir görüntüyü yüklemek için bir Windows Çalışma Zamanı API'sini çağıran, bu görüntüyü işlemek için bir arka plan iş parçacığına düşen ve ardından görüntüyü kullanıcı arabiriminde görüntülemek için kullanıcı arabirimi iş parçacığına döndüren bir olay işleyicisinin ana hattını göstereceğiz.

IAsyncAction MainPage::ClickHandler(IInspectable /* sender */, RoutedEventArgs /* args */)
{
    // We begin in the UI context.

    // Call StorageFile::OpenAsync to load an image file.

    // The call to OpenAsync occurred on a background thread, but C++/WinRT has restored us to the UI thread by this point.

    co_await winrt::resume_background();

    // We're now on a background thread.

    // Process the image.

    co_await winrt::resume_foreground(this->DispatcherQueue());

    // We're back on MainPage's UI thread.

    // Display the image in the UI.
}

Bu senaryo için StorageFile::OpenAsync çağrısında biraz verimsizlik vardır. Arka plan iş parçacığına gerekli bir bağlam geçişi yapılır (böylece işleyici denetimi çağırana geri verebilir); bunun ardından yeniden devam edildiğinde C++/WinRT kullanıcı arabirimi iş parçacığı bağlamını geri yükler. Ancak bu durumda, UI’yi güncelleyeceğimiz ana kadar UI iş parçacığında olmanız gerekmez. winrt::resume_background çağrımızdan önce ne kadar fazla Windows Çalışma Zamanı API’si çağırırsak, o kadar fazla gereksiz ileri geri bağlam geçişine maruz kalırız. Çözüm, daha önce Windows Çalışma Zamanı API'leri çağırmamaktır. Hepsini winrt::resume_background sonrasına taşıyın.

IAsyncAction MainPage::ClickHandler(IInspectable /* sender */, RoutedEventArgs /* args */)
{
    // We begin in the UI context.

    co_await winrt::resume_background();

    // We're now on a background thread.

    // Call StorageFile::OpenAsync to load an image file.

    // Process the image.

    co_await winrt::resume_foreground(this->DispatcherQueue());

    // We're back on MainPage's UI thread.

    // Display the image in the UI.
}

Daha gelişmiş bir şey yapmak istiyorsanız kendi await uyarlayıcılarınızı yazabilirsiniz. Örneğin, bir co_await öğesinin zaman uyumsuz işlemin tamamlandığı iş parçacığında devam etmesini istiyorsanız (yani bağlam değişikliği olmaz), bu durumda aşağıda gösterilenlere benzer await bağdaştırıcıları yazarak başlayabilirsiniz.

Note

Aşağıdaki kod örneği yalnızca eğitim amacıyla verilmiştir; await bağdaştırıcılarının nasıl çalıştığını anlamaya başlamanızı sağlamaktır. Bu tekniği kendi kod tabanınızda kullanmak istiyorsanız kendi await bağdaştırıcı yapılarınızı geliştirmenizi ve test etmenizi öneririz. Örneğin, complete_on_any, complete_on_current ve complete_on(dağıtıcı) yazabilirsiniz. Ayrıca IAsyncXxx türünü şablon parametresi olarak alan şablonlar oluşturmayı da göz önünde bulundurun.

struct no_switch
{
    no_switch(Windows::Foundation::IAsyncAction const& async) : m_async(async)
    {
    }

    bool await_ready() const
    {
        return m_async.Status() == Windows::Foundation::AsyncStatus::Completed;
    }

    void await_suspend(std::experimental::coroutine_handle<> handle) const
    {
        m_async.Completed([handle](Windows::Foundation::IAsyncAction const& /* asyncInfo */, Windows::Foundation::AsyncStatus const& /* asyncStatus */)
        {
            handle();
        });
    }

    auto await_resume() const
    {
        return m_async.GetResults();
    }

private:
    Windows::Foundation::IAsyncAction const& m_async;
};

no_switch await bağdaştırıcılarının nasıl kullanılacağını anlamak için öncelikle C++ derleyicisi bir co_await ifadeyle karşılaştığında await_ready, await_suspend ve await_resume adlı işlevleri aradığını bilmeniz gerekir. C++/WinRT kitaplığı, varsayılan olarak makul davranışlar elde edebilmeniz için bu işlevleri sağlar.

IAsyncAction async{ ProcessFeedAsync() };
co_await async;

no_switch await uyarlayıcılarını kullanmak için, co_await ifadesinin türünü IAsyncXxx'den no_switch olarak değiştirmeniz yeterlidir; şöyle:

IAsyncAction async{ ProcessFeedAsync() };
co_await static_cast<no_switch>(async);

Ardından C++ derleyicisi, IAsyncXxx ile eşleşen üç await_xxx işlevi aramak yerine no_switch eşleşen işlevleri arar.

winrt::resume_foreground'a daha ayrıntılı bir bakış

C++/WinRT 2.0 sürümünden itibaren, dağıtıcı iş parçacığından çağrılsa bile winrt::resume_foreground işlevi askıya alınır (önceki sürümlerde, yalnızca dağıtıcı iş parçacığında değilse askıya alındığı için bazı senaryolarda kilitlenmelere neden olabilir).

Mevcut davranış, yığın geri sarma ve yeniden kuyruğa alınma işlemlerinin gerçekleşeceğine güvenebileceğiniz anlamına gelir; bu da özellikle düşük düzeyli sistem kodunda sistem kararlılığı açısından önemlidir. Yukarıdaki İş parçacığı bağlılığını göz önünde bulundurarak programlama bölümünün son kod listesi, bazı karmaşık hesaplamaların bir arka plan iş parçacığında nasıl gerçekleştirildiğini ve ardından kullanıcı arabirimini (UI) güncellemek için uygun UI iş parçacığına nasıl geçildiğini gösterir.

winrt::resume_foreground dahili olarak şöyle görünür.

auto resume_foreground(...) noexcept
{
    struct awaitable
    {
        bool await_ready() const
        {
            return false; // Queue without waiting.
            // return m_dispatcher.HasThreadAccess(); // The C++/WinRT 1.0 implementation.
        }
        void await_resume() const {}
        void await_suspend(coroutine_handle<> handle) const { ... }
    };
    return awaitable{ ... };
};

Bu geçerli ve önceki davranış, Win32 uygulama geliştirmesinde PostMessage ile SendMessage arasındaki farka benzer. PostMessage, işi kuyruğa alır ve ardından işin tamamlanmasını beklemeden çağrı yığınını boşaltır. Yığın geri sarma gerekli olabilir.

winrt::resume_foreground işlevi başlangıçta Windows 10 önce tanıtılan CoreDispatcher'ı (CoreWindow'a bağlı) desteklemektedir. WinUI 3 ve Windows Uygulama SDK'sı uygulamalarında bunun yerine DispatcherQueue kullanın. Kendi amaçlarınız için DispatcherQueue oluşturabilirsiniz. Bu basit konsol uygulamasını düşünün.

using namespace Windows::System;

winrt::fire_and_forget RunAsync(DispatcherQueue queue);
 
int main()
{
    auto controller{ DispatcherQueueController::CreateOnDedicatedThread() };
    RunAsync(controller.DispatcherQueue());
    getchar();
}

Yukarıdaki örnek, özel bir iş parçacığında bir denetleyici içinde bulunan bir kuyruk oluşturur ve ardından denetleyiciyi coroutine’e iletir. Coroutine, özel iş parçacığında beklemek (askıya alma ve sürdürme) için kuyruğu kullanabilir. DispatcherQueue'un bir diğer yaygın kullanımı, geleneksel bir masaüstü veya Win32 uygulaması için geçerli kullanıcı arabirimi iş parçacığında kuyruk oluşturmaktır.

DispatcherQueueController CreateDispatcherQueueController()
{
    DispatcherQueueOptions options
    {
        sizeof(DispatcherQueueOptions),
        DQTYPE_THREAD_CURRENT,
        DQTAT_COM_STA
    };
 
    ABI::Windows::System::IDispatcherQueueController* ptr{};
    winrt::check_hresult(CreateDispatcherQueueController(options, &ptr));
    return { ptr, take_ownership_from_abi };
}

Bu, denetleyiciyi oluşturmak için Win32 stili createDispatcherQueueController işlevini çağırarak ve ardından sonuçta elde edilen kuyruk denetleyicisinin sahipliğini çağırana WinRT nesnesi olarak aktararak Win32 işlevlerini C++/WinRT projelerinize nasıl çağırabileceğinizi ve birleştirebileceğinizi gösterir. Mevcut Petzold stili Win32 masaüstü uygulamanızda verimli ve sorunsuz kuyruğa alma işlemini de tam olarak bu şekilde destekleyebilirsiniz.

winrt::fire_and_forget RunAsync(DispatcherQueue queue);
 
int main()
{
    Window window;
    auto controller{ CreateDispatcherQueueController() };
    RunAsync(controller.DispatcherQueue());
    MSG message;
 
    while (GetMessage(&message, nullptr, 0, 0))
    {
        DispatchMessage(&message);
    }
}

Yukarıdaki basit ana işlev bir pencere oluşturarak başlar. Bunun bir pencere sınıfı kaydettiğinizi ve en üst düzey masaüstü penceresini oluşturmak için CreateWindow'u çağırdığını düşünebilirsiniz. CreateDispatcherQueueController işlevi, daha sonra bu denetleyiciye ait dağıtıcı kuyruğu ile bir coroutine çağrılmadan önce kuyruk denetleyicisini oluşturmak için çağrılır. Daha sonra geleneksel bir ileti pompası girilir ve bu yazışmada koroutin doğal olarak sürdürülmesi gerçekleşir. Bunu yaptıktan sonra, uygulamanızdaki eşzamansız veya mesaj tabanlı iş akışınız için eş yordamların zarif dünyasına geri dönebilirsiniz.

winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ... // Begin on the calling thread...
 
    co_await winrt::resume_foreground(queue);
 
    ... // ...resume on the dispatcher thread.
}

winrt::resume_foreground çağrısı her zaman sıraya alınır ve ardından çağrı yığınını geri açar. ayrıca isteğe bağlı olarak yeniden verme önceliğini de ayarlayabilirsiniz.

winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...
 
    co_await winrt::resume_foreground(queue, DispatcherQueuePriority::High);
 
    ...
}

Veya varsayılan kuyruğa alma sırasını kullanarak.

...
#include <winrt/Windows.System.h>
using namespace Windows::System;
...
winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...
 
    co_await queue;
 
    ...
}

Note

Yukarıda gösterildiği gibi, co_await uyguladığınız türün ait olduğu ad alanı için projeksiyon başlığını eklediğinizden emin olun. Örneğin, Windows::System::DispatcherQueue veya Microsoft::UI::Dispatching::DispatcherQueue.

Ya da bu durumda, kuyruğun kapatılmasını algılamak ve bunu sorunsuz bir şekilde ele almak.

winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...
 
    if (co_await queue)
    {
        ... // Resume on dispatcher thread.
    }
    else
    {
        ... // Still on calling thread.
    }
}

co_await ifadesi, true değerini döndürür; bu da yürütmenin dağıtıcı iş parçacığında devam edeceğini gösterir. Başka bir deyişle, kuyruğa alma başarılı oldu. Buna karşılık, kuyruğun denetleyicisi kapatılmakta olduğundan ve artık kuyruk isteklerini işlemediğinden, yürütmenin çağıran iş parçacığında devam ettiğini belirtmek için false döndürür.

Yani, C++/WinRT’yi coroutine’lerle birleştirdiğinizde elinizin altında büyük bir güç olur; özellikle de eski usul, Petzold tarzı masaüstü uygulamaları geliştirirken.

Zaman uyumsuz işlemi iptal etme ve geri çağırmaları iptal etme

Windows Çalışma Zamanı'ın zaman uyumsuz programlamaya yönelik özellikleri, devam eden zaman uyumsuz bir eylemi veya işlemi iptal etmenize olanak tanır. Burada, büyük olabilecek bir dosya koleksiyonunu almak için StorageFolder::GetFilesAsync çağrısında bulunan ve sonuçta elde edilen zaman uyumsuz işlem nesnesini bir veri üyesinde depolayan bir örnek verilmiştir. Kullanıcının işlemi iptal etme seçeneği vardır.

// MainPage.xaml
...
<Button x:Name="workButton" Click="OnWork">Work</Button>
<Button x:Name="cancelButton" Click="OnCancel">Cancel</Button>
...

// MainPage.h
...
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Storage.Search.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Foundation::Collections;
using namespace Windows::Storage;
using namespace Windows::Storage::Search;
using namespace Microsoft::UI::Xaml;
...
struct MainPage : MainPageT<MainPage>
{
    MainPage()
    {
        InitializeComponent();
    }

    IAsyncAction OnWork(IInspectable /* sender */, RoutedEventArgs /* args */)
    {
        workButton().Content(winrt::box_value(L"Working..."));

        // Enable the Pictures Library capability in the app manifest file.
        StorageFolder picturesLibrary{ KnownFolders::PicturesLibrary() };

        m_async = picturesLibrary.GetFilesAsync(CommonFileQuery::OrderByDate, 0, 1000);

        IVectorView<StorageFile> filesInFolder{ co_await m_async };

        workButton().Content(box_value(L"Done!"));

        // Process the files in some way.
    }

    void OnCancel(IInspectable const& /* sender */, RoutedEventArgs const& /* args */)
    {
        if (m_async.Status() != AsyncStatus::Completed)
        {
            m_async.Cancel();
            workButton().Content(winrt::box_value(L"Canceled"));
        }
    }

private:
    IAsyncOperation<::IVectorView<StorageFile>> m_async;
};
...

İptal işleminin uygulama tarafı için basit bir örnekle başlayalım.

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

using namespace winrt;
using namespace Windows::Foundation;
using namespace std::chrono_literals;

IAsyncAction ImplicitCancelationAsync()
{
    while (true)
    {
        std::cout << "ImplicitCancelationAsync: do some work for 1 second" << std::endl;
        co_await 1s;
    }
}

IAsyncAction MainCoroutineAsync()
{
    auto implicit_cancelation{ ImplicitCancelationAsync() };
    co_await 3s;
    implicit_cancelation.Cancel();
}

int main()
{
    winrt::init_apartment();
    MainCoroutineAsync().get();
}

Yukarıdaki örneği çalıştırırsanız , ImplicitCancelationAsync'in saniyede üç saniye boyunca bir ileti yazdırdığını ve ardından iptal edilmenin bir sonucu olarak otomatik olarak sonlandırıldığını görürsünüz. Bu işe yarar çünkü bir co_await ifadeyle karşılaşıldığında bir eş yordam iptal edilip edilmediğini denetler. Eğer öyleyse, kısa devre yaparak çıkar; değilse, her zamanki gibi askıya alınır.

İptal işlemi, coroutine askıya alınırken elbette gerçekleşebilir. Yalnızca coroutine devam ettiğinde veya başka bir co_awaitöğeye çarptığında iptal olup olmadığını denetler. Sorun, iptal isteğine yanıt verme gecikmesinin muhtemelen fazla kaba taneli olmasıdır.

Yani, bir diğer seçenek de coroutine’iniz içinden iptal durumunu açıkça denetlemektir. Yukarıdaki örneği aşağıdaki listede yer alan kodla güncelleştirin. Bu yeni örnekte , ExplicitCancelationAsyncwinrt::get_cancellation_token işlevi tarafından döndürülen nesneyi alır ve coroutine'nin iptal edilip edilmediğini düzenli aralıklarla denetlemek için kullanır. İptal edilmediği sürece eşyordam sonsuza kadar döner; iptal edildiğinde ise döngü ve işlev normal şekilde sonlanır. Sonuç önceki örnekle aynıdır, ancak burada çıkış açıkça ve denetim altında gerçekleşir.

IAsyncAction ExplicitCancelationAsync()
{
    auto cancelation_token{ co_await winrt::get_cancellation_token() };

    while (!cancelation_token())
    {
        std::cout << "ExplicitCancelationAsync: do some work for 1 second" << std::endl;
        co_await 1s;
    }
}

IAsyncAction MainCoroutineAsync()
{
    auto explicit_cancelation{ ExplicitCancelationAsync() };
    co_await 3s;
    explicit_cancelation.Cancel();
}
...

winrt::get_cancellation_token üzerinde beklemek, korutinin sizin adınıza ürettiği IAsyncAction hakkında bilgi sahibi olan bir iptal belirteci almanızı sağlar. İptal durumunu sorgulamak için bu belirteç üzerinde işlev çağrısı işlecini kullanabilirsiniz; yani özünde iptali yoklamış olursunuz. İşlemciye bağlı bir işlem gerçekleştiriyorsanız veya büyük bir koleksiyon üzerinde yineleme yapıyorsanız, bu makul bir tekniktir.

İptal geri çağırmasını kaydetme

Windows Çalışma Zamanı'ın iptal işlemi otomatik olarak diğer zaman uyumsuz nesnelere aktarılmaz. Ancak Windows SDK'sının 10.0.17763.0 (Windows 10 sürüm 1809) sürümünde kullanıma sunulan iptal geri çağırmasını kaydedebilirsiniz. Bu, iptalin yayılmasını sağlayan bir önleyici kancadır ve mevcut eşzamanlılık kitaplıklarıyla bütünleştirmeyi mümkün kılar.

Bu sonraki kod örneğinde NestedCoroutineAsync işi yapar, ancak içinde özel bir iptal mantığı yoktur. CancelationPropagatorAsync, esasen iç içe geçmiş eş yordamı sarmalayan bir yapıdır; bu sarmalayıcı iptali önceden iletir.

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

using namespace winrt;
using namespace Windows::Foundation;
using namespace std::chrono_literals;

IAsyncAction NestedCoroutineAsync()
{
    while (true)
    {
        std::cout << "NestedCoroutineAsync: do some work for 1 second" << std::endl;
        co_await 1s;
    }
}

IAsyncAction CancelationPropagatorAsync()
{
    auto cancelation_token{ co_await winrt::get_cancellation_token() };
    auto nested_coroutine{ NestedCoroutineAsync() };

    cancelation_token.callback([=]
    {
        nested_coroutine.Cancel();
    });

    co_await nested_coroutine;
}

IAsyncAction MainCoroutineAsync()
{
    auto cancelation_propagator{ CancelationPropagatorAsync() };
    co_await 3s;
    cancelation_propagator.Cancel();
}

int main()
{
    winrt::init_apartment();
    MainCoroutineAsync().get();
}

CancelationPropagatorAsync, kendi iptal geri çağırma işlevi için bir lambda işlevi kaydeder ve ardından içteki işlem tamamlanana kadar beklemeye geçer (askıya alınır). CancellationPropagatorAsync iptal edildiğinde ya da edilirse, iptali iç içe geçmiş coroutine'e iletir. İptal için yoklama yapmanıza gerek yoktur; ve iptal süresiz olarak engellenmez. Bu mekanizma, C++/WinRT hakkında hiçbir şey bilmeyen bir eş yordam veya eşzamanlılık kitaplığıyla birlikte çalışmak için kullanabileceğiniz kadar esnektir.

İlerleme durumunu raporlama

Coroutine'iniz IAsyncActionWithProgress veya IAsyncOperationWithProgress döndürüyorsa, winrt::get_progress_token işlevi tarafından döndürülen nesneyi alabilir ve ilerleme durumunu bir ilerleme işleyicisine geri bildirmek için kullanabilirsiniz. İşte bir kod örneği.

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

using namespace winrt;
using namespace Windows::Foundation;
using namespace std::chrono_literals;

IAsyncOperationWithProgress<double, double> CalcPiTo5DPs()
{
    auto progress{ co_await winrt::get_progress_token() };

    co_await 1s;
    double pi_so_far{ 3.1 };
    progress.set_result(pi_so_far);
    progress(0.2);

    co_await 1s;
    pi_so_far += 4.e-2;
    progress.set_result(pi_so_far);
    progress(0.4);

    co_await 1s;
    pi_so_far += 1.e-3;
    progress.set_result(pi_so_far);
    progress(0.6);

    co_await 1s;
    pi_so_far += 5.e-4;
    progress.set_result(pi_so_far);
    progress(0.8);

    co_await 1s;
    pi_so_far += 9.e-5;
    progress.set_result(pi_so_far);
    progress(1.0);

    co_return pi_so_far;
}

IAsyncAction DoMath()
{
    auto async_op_with_progress{ CalcPiTo5DPs() };
    async_op_with_progress.Progress([](auto const& sender, double progress)
    {
        std::wcout << L"CalcPiTo5DPs() reports progress: " << progress << L". "
                   << L"Value so far: " << sender.GetResults() << std::endl;
    });
    double pi{ co_await async_op_with_progress };
    std::wcout << L"CalcPiTo5DPs() is complete !" << std::endl;
    std::wcout << L"Pi is approx.: " << pi << std::endl;
}

int main()
{
    winrt::init_apartment();
    DoMath().get();
}

İlerlemeyi bildirmek için ilerleme belirtecini, argüman olarak ilerleme değerini vererek çağırın. Geçici bir sonuç ayarlamak için ilerleme belirtecinde yöntemini kullanın set_result() .

Note

Geçici sonuçları raporlamak için C++/WinRT sürüm 2.0.210309.3 veya üzeri gerekir.

Yukarıdaki örnek, her ilerleme raporu için bir geçici sonuç ayarlamayı seçer. Herhangi bir durumda, istediğiniz zaman geçici sonuçları raporlamayı seçebilirsiniz. İlerleme raporuyla bir arada olması gerekmez.

Note

Zaman uyumsuz bir eylem veya işlem için birden fazla tamamlama işleyicisi uygulamak doğru değildir. Tamamlanmış olayı için tek bir temsilciniz olabilir veya bunu yapabilirsiniz co_await . her ikisine de sahipseniz, ikincisi başarısız olur. Aşağıdaki iki tür tamamlama işleyicisinden biri kullanılabilir; ancak aynı zaman uyumsuz nesne için ikisi birden kullanılamaz.

auto async_op_with_progress{ CalcPiTo5DPs() };
async_op_with_progress.Completed([](auto const& sender, AsyncStatus /* status */)
{
    double pi{ sender.GetResults() };
});
auto async_op_with_progress{ CalcPiTo5DPs() };
double pi{ co_await async_op_with_progress };

Tamamlama işleyicileri hakkında daha fazla bilgi için bkz: Zaman uyumsuz eylemler ve işlemler için temsilci türleri.

Ateşle ve unut

Bazen, diğer çalışmalarla eşzamanlı olarak gerçekleştirilebilecek bir göreviniz vardır ve bu görevin tamamlanmasını beklemeniz gerekmez (başka hiçbir çalışma buna bağımlı değildir) veya bir değer döndürmek için buna ihtiyacınız yoktur. Bu durumda, görevi atabilir ve unutabilirsiniz. Bunu, dönüş türü winrt::fire_and_forget olan bir eş yordam yazarak yapabilirsiniz (Windows Çalışma Zamanı zaman uyumsuz işlem türlerinden biri ya da concurrency::task yerine).

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

using namespace winrt;
using namespace std::chrono_literals;

winrt::fire_and_forget CompleteInFiveSeconds()
{
    co_await 5s;
}

int main()
{
    winrt::init_apartment();
    CompleteInFiveSeconds();
    // Do other work here.
}

winrt::fire_and_forget , içinde zaman uyumsuz işlemler gerçekleştirmeniz gerektiğinde olay işleyicinizin dönüş türü olarak da yararlıdır. Burada bir örnek verilmiştir (ayrıca bkz. C++/WinRT'de güçlü ve zayıf başvurular).

winrt::fire_and_forget MyClass::MyMediaBinder_OnBinding(MediaBinder const&, MediaBindingEventArgs args)
{
    auto lifetime{ get_strong() }; // Prevent *this* from prematurely being destructed.
    auto ensure_completion{ unique_deferral(args.GetDeferral()) }; // Take a deferral, and ensure that we complete it.

    auto file{ co_await StorageFile::GetFileFromApplicationUriAsync(Uri(L"ms-appx:///video_file.mp4")) };
    args.SetStorageFile(file);

    // The destructor of unique_deferral completes the deferral here.
}

İlk bağımsız değişken (gönderen) adsız bırakılır, çünkü onu hiçbir zaman kullanmayız. Bu nedenle bunu referans olarak bırakmakta güvendeyiz. Ancak args'ın değer olarak geçirildiğini unutmayın. Yukarıdaki Parametre geçirme bölümüne bakın.

Çekirdek tutamacı bekleniyor

C++/WinRT, bir çekirdek olayı sinyal verilene kadar askıya almak için kullanabileceğiniz bir winrt::resume_on_signal işlevi sağlar. co_await resume_on_signal(h) dönene kadar tanıtıcının geçerli kalmasını sağlamak sizin sorumluluğunuzdadır. resume_on_signal sizin için bunu yapamaz, çünkü bu ilk örnekte olduğu gibi resume_on_signal başlamadan önce bile tutamacı kaybetmiş olabilirsiniz.

IAsyncAction Async(HANDLE event)
{
    co_await DoWorkAsync();
    co_await resume_on_signal(event); // The incoming handle is not valid here.
}

Gelen HANDLE, yalnızca işlev geri dönene kadar geçerlidir ve bu işlev (bir yardımcı rutin olarak) ilk askıya alma noktasında geri döner (bu durumda ilk co_await). DoWorkAsync beklenirken, denetim arayana geri döndü, arama çerçevesi kapsam dışına çıktı ve artık tanıtıcının coroutine devam ettiğinde geçerli olup olmayacağını bilmiyorsunuz.

Teknik olarak, koroutinimiz parametreleri gerektiği gibi değere göre alıyor (yukarıdaki Parametre geçirme bölümüne bakın). Ancak bu durumda, bu rehberliğe ( yalnızca harfi değil) uymamız için bir adım daha ileri gitmemiz gerekir. Tanıtıcıyla birlikte güçlü bir referansı (başka bir deyişle, sahipliği) iletmemiz gerekir. Nasıl yapıldığını burada bulabilirsiniz.

IAsyncAction Async(winrt::handle event)
{
    co_await DoWorkAsync();
    co_await resume_on_signal(event); // The incoming handle *is* valid here.
}

Değere göre winrt::handle geçirmek, çekirdek tanıtıcısının coroutine ömrü boyunca geçerli kalmasını sağlayan sahiplik semantiği sağlar.

Bu eş yordama şu şekilde hitap edebilirsiniz.

namespace
{
    winrt::handle duplicate(winrt::handle const& other, DWORD access)
    {
        winrt::handle result;
        if (other)
        {
            winrt::check_bool(::DuplicateHandle(::GetCurrentProcess(),
		        other.get(), ::GetCurrentProcess(), result.put(), access, FALSE, 0));
        }
        return result;
    }

    winrt::handle make_manual_reset_event(bool initialState = false)
    {
        winrt::handle event{ ::CreateEvent(nullptr, true, initialState, nullptr) };
        winrt::check_bool(static_cast<bool>(event));
        return event;
    }
}

IAsyncAction SampleCaller()
{
    handle event{ make_manual_reset_event() };
    auto async{ Async(duplicate(event)) };

    ::SetEvent(event.get());
    event.close(); // Our handle is closed, but Async still has a valid handle.

    co_await async; // Will wake up when *event* is signaled.
}

Bu örnekte olduğu gibi, resume_on_signal'e bir zaman aşımı değeri iletebilirsiniz.

winrt::handle event = ...

if (co_await winrt::resume_on_signal(event.get(), std::literals::2s))
{
    puts("signaled");
}
else
{
    puts("timed out");
}

Eşzamansız zaman aşımları kolaylaştırıldı

C++/WinRT, C++ coroutine'lerine büyük ölçüde dayanır. Eşzamanlılık kodu yazma üzerindeki etkisi dönüşümseldir. Bu bölümde, zaman uyumsuzlu ilişkin ayrıntıların önemli olmadığı durumlar anlatılır ve tek istediğiniz sonucun orada ve sonra olmasıdır. Bu nedenle C++/WinRT'nin IAsyncAction Windows Çalışma Zamanı zaman uyumsuz işlem arabirimini uygulaması, std::future tarafından sağlanana benzer bir get işlevine sahiptir.

using namespace winrt::Windows::Foundation;
int main()
{
    IAsyncAction async = ...
    async.get();
    puts("Done!");
}

get işlevi, asenkron nesne tamamlanana kadar süresiz olarak bekler. Zaman uyumsuz nesneler çok kısa ömürlü olma eğilimindedir, bu nedenle genellikle ihtiyacınız olan tek şey budur.

Ancak bunun yeterli olmadığı durumlar vardır ve bir süre geçtikten sonra beklemeyi bırakmanız gerekir. Windows Çalışma Zamanı tarafından sağlanan yapı taşları sayesinde bu kodu yazmak her zaman mümkün olmuştur. Ancak artık C++/WinRT , wait_for işlevini sağlayarak bunu çok daha kolay hale getiriyor. Ayrıca IAsyncAction üzerinde de uygulanır ve yine std::future tarafından sağlanana benzer.

using namespace std::chrono_literals;
int main()
{
    IAsyncAction async = ...
 
    if (async.wait_for(5s) == AsyncStatus::Completed)
    {
        puts("done");
    }
}

Note

wait_for arabirimde std::chrono::d uration kullanır, ancak std::chrono::d uration'ın sağladığından daha küçük bir aralıkla sınırlıdır (kabaca 49,7 gün).

Bu sonraki örnekteki wait_for yaklaşık beş saniye bekler ve ardından tamamlamayı denetler. Karşılaştırma olumluysa, asenkron nesnenin başarıyla tamamlandığını bilirsiniz ve işiniz bitmiştir. Bir sonuç bekliyorsanız, sonucu almak için GetResults yöntemine yapılan bir çağrıyla bunu takip edebilirsiniz.

Note

wait_for ve get birbirini dışlar (ikisini birden arayamazsınız). Her ikisi de bir garson olarak sayılır ve Windows Çalışma Zamanı zaman uyumsuz eylemler/işlemler yalnızca tek bir garsonu destekler.

int main()
{
    IAsyncOperation<int> async = ...
 
    if (async.wait_for(5s) == AsyncStatus::Completed)
    {
        printf("result %d\n", async.GetResults());
    }
}

Zaman uyumsuz nesne o zamana kadar tamamlandığından , GetResults yöntemi daha fazla beklemeden sonucu hemen döndürür. Gördüğünüz gibi wait_for zaman uyumsuz nesnenin durumunu döndürür. Bu nedenle, daha ayrıntılı denetim için kullanabilirsiniz, örneğin.

switch (async.wait_for(5s))
{
case AsyncStatus::Completed:
    printf("result %d\n", async.GetResults());
    break;
case AsyncStatus::Canceled:
    puts("canceled");
    break;
case AsyncStatus::Error:
    puts("failed");
    break;
case AsyncStatus::Started:
    puts("still running");
    break;
}
  • AsyncStatus::Completed değerinin zaman uyumsuz nesnenin başarıyla tamamlandığı anlamına geldiğini ve herhangi bir sonucu almak için GetResults yöntemini çağırabileceğinizi unutmayın.
  • AsyncStatus::Canceled , zaman uyumsuz nesnenin iptal edildiği anlamına gelir. İptal genellikle çağıran tarafından istenir, bu nedenle bu durumu işlemek nadir olacaktır. Genellikle, iptal edilen bir asenkron nesne basitçe atılır. Dilerseniz, iptal özel durumunu yeniden fırlatmak için GetResults yöntemini çağırabilirsiniz.
  • AsyncStatus::Error , zaman uyumsuz nesnenin bir şekilde başarısız olduğu anlamına gelir. İsterseniz özel durumu yeniden dağıtmak için GetResults yöntemini çağırabilirsiniz.
  • AsyncStatus::Started , zaman uyumsuz nesnenin hala çalıştığı anlamına gelir. Windows Çalışma Zamanı zaman uyumsuz deseni, birden fazla bekleme işlemine veya birden fazla bekleyiciye izin vermez. Bu, döngüde wait_for çağıramazsınız anlamına gelir. Bekleme fiilen zaman aşımına uğradıysa, elinizde yalnızca birkaç seçenek kalır. Nesneyi bırakabilir veya herhangi bir sonucu almak için GetResults yöntemini çağırmadan önce durumunu yoklayabilirsiniz. Ancak en iyisi bu noktada nesneyi atmaktır.

Alternatif bir desen, yalnızca Başlatıldı'yı denetlemek ve GetResults'un diğer servis taleplerini ele almasına izin vermektir.

if (async.wait_for(5s) == AsyncStatus::Started)
{
    puts("timed out");
}
else
{
    // will throw appropriate exception if in canceled or error state
    auto results = async.GetResults();
}

Diziyi zaman uyumsuz olarak döndürme

Aşağıda hata MIDL2025 oluşturan bir MIDL 3.0 örneği verilmiştir : [msg]sözdizimi hatası [context]: bekleniyor > veya "[" yakınında.

Windows.Foundation.IAsyncOperation<Int32[]> RetrieveArrayAsync();

Bunun nedeni, parametreli arabirimde parametre türü bağımsız değişkeni olarak dizi kullanmanın geçersiz olmasıdır. Bu nedenle, bir diziyi çalışma zamanı sınıf yönteminden zaman uyumsuz olarak geri geçirme hedefine ulaşmak için daha az belirgin bir yönteme ihtiyacımız vardır.

Bir PropertyValue nesnesine kutulanmış diziyi döndürebilirsiniz. Çağıran kod daha sonra onu kutudan çıkarır. Burada, SampleComponent çalışma zamanı sınıfını bir Windows Çalışma Zamanı Bileşeni (C++/WinRT) projesine ekleyip (örneğin) Boş Uygulama, Paketlenmiş (Masaüstünde WinUI 3) projesinden bunu kullanarak deneyebileceğiniz bir kod örneği verilmiştir.

// SampleComponent.idl
namespace MyComponentProject
{
    runtimeclass SampleComponent
    {
        Windows.Foundation.IAsyncOperation<IInspectable> RetrieveCollectionAsync();
    };
}

// SampleComponent.h
...
struct SampleComponent : SampleComponentT<SampleComponent>
{
    ...
    Windows::Foundation::IAsyncOperation<Windows::Foundation::IInspectable> RetrieveCollectionAsync()
    {
        co_return Windows::Foundation::PropertyValue::CreateInt32Array({ 99, 101 }); // Box an array into a PropertyValue.
    }
}
...

// SampleCoreApp.cpp
...
MyComponentProject::SampleComponent m_sample_component;
...
auto boxed_array{ co_await m_sample_component.RetrieveCollectionAsync() };
auto property_value{ boxed_array.as<winrt::Windows::Foundation::IPropertyValue>() };
winrt::com_array<int32_t> my_array;
property_value.GetInt32Array(my_array); // Unbox back into an array.
...

Önemli API'ler