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?
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)).