locked
Thread pool task blocking UI thread

    Question

  • The following code run in a Windows Store app blocks the UI for 30 seconds despite that fact that the loop should run in a separate thread:

    int seconds(30);
    ComPtr<IThreadPoolStatics> threadPool;
    HRESULT hr = GetActivationFactory(HStringReference(RuntimeClass_Windows_System_Threading_ThreadPool).Get(), &threadPool);
    ComPtr<ABI::Windows::Foundation::IAsyncAction> asyncActionPtr;
    hr = threadPool->RunAsync(Callback<IWorkItemHandler>( // Line 1
        [seconds](ABI::Windows::Foundation::IAsyncAction* asyncAction) -> HRESULT {
            std::chrono::system_clock::time_point end(std::chrono::system_clock::now() + std::chrono::seconds(seconds)); // Line 3
            while (std::chrono::system_clock::now() < end);
        return S_OK; // Line 4
    }).Get(), &asyncActionPtr);
    if (FAILED(hr)) throw std::exception("Cannot start thread"); // Line 2

    When I set breakpoints in the marked lines, I can see that Line 1 is hit before Line 2, then Line 3 and after 30 seconds Line 4. In these 30 seconds the UI is blocked and the Thread view in Visual Studio show the same thread (SHcore.dll) for all of the breakpoints. I am using Windows 8 and Visual Studio 2012. Can somebody explain?


    • Edited by dataweb Monday, September 23, 2013 6:54 AM
    Monday, September 23, 2013 6:53 AM

Answers

  • WRL::Callback does not by default create a delegate object which is agile.  By calling this on the main UI thread ASTA, you are creating a callback object which must occur on the UI thread.  The net effect here is that the thread pool thread synchronously invokes a callback which gets marshaled over to the UI thread to run (your lambda which does the timed loop ends up running on the UI thread).  You can see this setting a breakpoint in your loop and looking at the threads in the process.

    Using WRL::Callback, you can rectify this by injecting FtmBase into the object:

    Callback<Implements<RuntimeClassFlags<ClassicCom>, IWorkItemHandler, FtmBase>>(...)

    The C++/CX examples which construct delegates via "ref new WorkItemHandler([](...){...})" create by default agile delegates.  This is why you do not see it with the other posted examples.

    • Marked as answer by dataweb Thursday, October 3, 2013 9:13 AM
    Wednesday, October 2, 2013 11:19 PM

All replies

  • Hi dataweb,

    I wasn't able to run your code to reproduce this, but I think the problem is that your Task.Get accessor blocks waiting for the ThreadPool task to finish. See http://msdn.microsoft.com/en-us/library/dd321468.aspx

    I tried duplicating it in C++/Cx with PPL and everything worked as expected:

    void ThreadPoolTest::MainPage::Button_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e)
    {
    	auto workItem = ref new WorkItemHandler(
        [this](IAsyncAction^ workItem)
    	{
    		concurrency::wait(10000);
     		OutputDebugString(L"After wait\r\n");
    	});
    	auto task = create_task(ThreadPool::RunAsync(workItem));
    	task.then([this]()-> void
    	{
    		out->Text="Really done";
    		OutputDebugString(L"Done\r\n");
    	});
    	
    	out->Text = "Done";
    	OutputDebugString(L"Done (with the click)\r\n");
    }

    With breakpoints on the OutputDebugStrings I break on the following threads. There is the expected 10 second gab between "Before wait" and "After wait":

    <task thread> Before wait
    <UI thread> - Done
    <task thread> - After wait
    <UI thread>Really done
    
    --Rob

    Tuesday, September 24, 2013 1:09 AM
    Owner
  • Hi Rob,

    thanks for your response.

    However I cannot see, where my code calls task<>::get. It only calls Callback<>::Get() to retrieve the ABI delegate.

    Or am I missing something?

    Thanks

    --Peter

    Tuesday, September 24, 2013 12:11 PM
  • Hi Peter,

    Sorry, right spot but the wrong class. I think that's where the problem is, but I may not have untwisted your code properly. If you need me to look into this farther can you provide a minimal project I can run?

    --Rob

    Tuesday, September 24, 2013 4:29 PM
    Owner
  • Hi Rob,

    sorry, I wasn't available these days. Here is how you can easily reproduce the problem:

    1. Create new C++ blank Windows Store App in VS 2012.

    2. Drop a button on the page and double-click to create the handler.

    3. Add these lines after the existing #include directives:
        #include <chrono>
        #include <wrl.h>
        #include <windows.system.threading.h>

    4. Add these lines after the existing using directives:
        using namespace Microsoft::WRL;
        using namespace Microsoft::WRL::Wrappers;
        using namespace ABI::Windows::Foundation;
        using namespace ABI::Windows::System::Threading;

    5. Replace the handler with this code:
        void App1::MainPage::Button_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e)
        {
            ComPtr<IThreadPoolStatics> threadPool;
            HRESULT hr = GetActivationFactory(HStringReference(RuntimeClass_Windows_System_Threading_ThreadPool).Get(), &threadPool);
            ComPtr<IAsyncAction> asyncActionPtr;
            hr = threadPool->RunAsync(Callback<IWorkItemHandler>([](IAsyncAction* asyncAction) -> HRESULT {
                std::chrono::system_clock::time_point end(std::chrono::system_clock::now() + std::chrono::seconds(30));
                while (std::chrono::system_clock::now() < end);
                return S_OK;
            }).Get(), &asyncActionPtr);
    }

    6. Run the app and click the button.

    => I would expect the waiting loop to run within a separate thread and therefore unblocking the UI immediately. But actually the buttons turns black again only after 30 seconds which means that the UI is blocked during that time. Do you see why this is the case?

    -- Peter


       



    • Edited by dataweb Wednesday, October 2, 2013 2:00 PM
    Wednesday, October 2, 2013 1:58 PM
  • WRL::Callback does not by default create a delegate object which is agile.  By calling this on the main UI thread ASTA, you are creating a callback object which must occur on the UI thread.  The net effect here is that the thread pool thread synchronously invokes a callback which gets marshaled over to the UI thread to run (your lambda which does the timed loop ends up running on the UI thread).  You can see this setting a breakpoint in your loop and looking at the threads in the process.

    Using WRL::Callback, you can rectify this by injecting FtmBase into the object:

    Callback<Implements<RuntimeClassFlags<ClassicCom>, IWorkItemHandler, FtmBase>>(...)

    The C++/CX examples which construct delegates via "ref new WorkItemHandler([](...){...})" create by default agile delegates.  This is why you do not see it with the other posted examples.

    • Marked as answer by dataweb Thursday, October 3, 2013 9:13 AM
    Wednesday, October 2, 2013 11:19 PM
  • Hi Bill,

    this sound like the answer I was looking for, thanks a lot.

    I would like to understand how the thread pool or windows manage this difference between agile and non-agile objects. Does it mean that the thread pool somehow checks whether my delgegate is agile and if not, it dispatches it to the UI thread instead of creating a separate thread?

    I you happen to be on Stack Overflow as well, you will find my question there too. And it is still not answered:

    http://stackoverflow.com/questions/18938508/why-is-thread-pool-work-item-executed-on-ui-thread

    Thank you

    Peter

    EDIT: Needless to say: Your solution also works perfectly :))
    • Marked as answer by dataweb Thursday, October 3, 2013 9:13 AM
    • Unmarked as answer by dataweb Thursday, October 3, 2013 9:13 AM
    • Edited by dataweb Thursday, October 3, 2013 9:18 AM
    Thursday, October 3, 2013 9:13 AM
  • Remember that WinRT is based on COM.  The thread pool is taking the delegate you pass into RunAsync and marshaling it over to the thread pool thread.  Because the delegate you created via WRL::Callback is a non-agile ASTA object, the thread pool worker thread gets a proxy to it.  When it invokes this, the call to delegate (into the proxy) is marshaled over to the ASTA by COM.

    A lot of the time, you don't need to worry about this.  A great many WinRT objects are agile.  The high-level languages (C++/CX, C#) create objects (delegates / classes / etc...) which by default are agile.  With WRL, FtmBase is the "make this object agile" base class and it needs to be there when you create runtime classes, callbacks, etc...  in order to get an agile object.

    If you want more details around the threading model and its implications, I might suggest that you look at the Build 2013 talk: http://channel9.msdn.com/Events/Build/2013/4-107.

    Thursday, October 3, 2013 1:58 PM
  • Thanks again. proxy is the keyword. I learned a lot today.

    Thank you very much
    Peter


    BTW: The link has a dot too many ;-)
    • Edited by dataweb Thursday, October 3, 2013 3:17 PM
    Thursday, October 3, 2013 3:12 PM