c++-cxppl

Do I need to synchronize my destructor with async functions when using PPL?


Let's say I have a ViewModel that can be destroyed when a user navigates away from its bound View. The destructor performs cleanup on a subscription member variable:

MyViewModel::~MyViewModel()
{
    if (m_subscription)
    {
        if (m_contentChangedToken.Value != 0)
        {
            m_subscription->ContentChanged -= m_contentChangedToken;
            m_contentChangedToken.Value = 0;
        }
    }
}

After the ViewModel is created, a function runs which asynchronously gets the subscription, assigns it to a member variable, and assigns event listeners

void MyViewModel::AwesomeFunctionAsync()
{
    create_task(TargetedContentSubscription::GetAsync(c_subId))
        .then([this](TargetedContentSubscription^ subscription)
    {
        if (subscription)
        {
            m_subscription = subscription;
            m_contentChangedToken = m_subscription->ContentChanged += // attach event
        }
    }, task_continuation_context::use_arbitrary());
}

Now let's say my ViewModel is being destroyed while a background thread is running code inside AwesomeFunctionAsync. Is there a race condition lurking here? For instance, might the destructor run before the event is attached by the background thread? Or can I trust the destructor is always last due to the GC?


Solution

  • Unless someone explicitly tries to delete the object, you will be fine since the lambda captures the this pointer and will keep it alive.

    For example, try the following simple test:

    ref struct TestClass sealed
    {
      void DoStuffAsync()
      {
        concurrency::create_async([this]()
        {
          Sleep(1000);
          PrintValue();
        });
      }
    
      void PrintValue()
      {
        // Accessing 'name' after deletion is undefined behavior, but it 
        // "works on my machine" for the purposes of this demonstration.
        std::string message = name + ": PrintValue is running.";
    
        // Accessing 'data.size()' after deletion is also undefined behavior
        if (data.size() == 0)
        {
          message += " Oops, I'm about to crash\r\n";
        }
        else
        {
          message = message + " Data is " + std::to_string(data[0]) + 
            ", " + std::to_string(data[1]) + "\r\n";
        }
    
        OutputDebugStringA(message.c_str());
      }
    
      virtual ~TestClass()
      {
        std::string message = name + ": Destructor is running.\r\n";
        OutputDebugStringA(message.c_str());
      }
    
    internal: // so we can use 'const char *'
    
      TestClass(const char* name) : name{ name }, data{ 1, 2 }
      {
        std::string message = this->name + ": Constructor is running.\r\n";
        OutputDebugStringA(message.c_str());
      }
    
    private:
      std::string name;
      std::vector<int> data;
    };
    
    void Test()
    {
      OutputDebugStringA("Starting 'no async' test\r\n");
      {
        auto c = ref new TestClass("no async");
        c->PrintValue();
      }
      OutputDebugStringA("---\r\nDone. Starting 'async' test\r\n");
    
      {
        auto c = ref new TestClass("async");
        c->DoStuffAsync();
      }
      OutputDebugStringA("---\r\nDone. Starting 'explicit delete' test\r\n");
    
      {
        auto c = ref new TestClass("explicit delete");
        c->DoStuffAsync();
        delete c;
      }
    }
    
    
    MainPage::MainPage()
    {
      InitializeComponent();
    
      Test();
    }
    

    When you run it, you will see something like this in the Output window:

    Starting 'no async' test 
    no async: Constructor is running. 
    no async: PrintValue is running. Data is 1, 2 
    no async: Destructor is running.
    --- Done. Starting 'async' test 
    async: Constructor is running.
    --- Done. Starting 'explicit delete' test 
    explicit delete: Constructor is running. 
    explicit delete: Destructor is running. 
    async: PrintValue is running. Data is 1, 2 
    : PrintValue is running. Oops, I'm about to crash 
    async: Destructor is running.
    

    Note the 'async' version doesn't run the destructor until after PrintValue has run asynchronously. But the 'explicit delete' version destroys the object, which will crash when it tries to access member variables approximately 1 second later. (You can see that accessing name doesn't crash -- although it's undefined behavior -- but if you tried to access the elements of data you would get an exception (or worse)).