I came across this implementation of disposable pattern provided by microsoft: https://msdn.microsoft.com/en-us/library/system.idisposable(v=vs.110).aspx
using System;
class BaseClass : IDisposable
{
// Flag: Has Dispose already been called?
bool disposed = false;
// Public implementation of Dispose pattern callable by consumers.
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
// Protected implementation of Dispose pattern.
protected virtual void Dispose(bool disposing)
{
if (disposed)
return;
if (disposing) {
// Free any other managed objects here.
//
}
// Free any unmanaged objects here.
//
disposed = true;
}
~BaseClass()
{
Dispose(false);
}
}
Let's say I have a C++ class that is associated to this C# class, and I want to delete C++ object on disposing C# class to make sure that my unmanaged resources are released properly. I add a function DestructNative(self) which basically makes a native C++ call delete (CppObject*)self on associated C++ object. So my code looks like this:
// Protected implementation of Dispose pattern.
protected virtual void Dispose(bool disposing)
{
if (disposed)
return;
if (disposing) {
// Free any other managed objects here.
//
}
DestructNative(self);
disposed = true;
}
So my question is, knowing that C# finalizers can be called from a different thread, do I need to provide synchronization inside C++ object's destructor to make sure I don't have any race conditions when Dispose(false) called from C# finalizer?
Is microsoft disposable pattern broken? Seems like disposed flag is a simple variable which is not synchronized if finalizer is called from a different thread.
Is microsoft disposable pattern broken? Seems like disposed flag is a simple variable which is not synchronized if finalizer is called from a different thread.
No it is not broken.
This question poses 2 interesting problems. For a class which pre-dates C++11 thinking and is thread unaware, what is the impact of the following.
class PreCpp11 {
public:
int ** ptr;
bool mInitDone;
PreCpp11() : mInitDone(false) {
ptr = new int*[100];
}
init() {
for( size_t i = 0; i < 100; i++ ){
ptr[i] = new int[100];
}
mInitDone = true;
}
~PreCpp11() {
if( mInitDone ){
for( size_t i =0; i <100; i++ ){
delete ptr[i];
}
}
delete []ptr;
}
}
After the code
PreCpp11 * myObj = new PreCpp11();
myObj->init();
send_object_to_thread2( myObj );
Where thread 2 performs
PreCpp11 obj = get_obj_from_sync();
delete obj;
If the destructor is called on a different thread, how have we avoided a data race.
Given that for a disposable implementation, will that cause a data race as above.
In both cases I believe the answer to be this code is acceptable and compliant. However, it relies on the inter-thread communication of the PreCpp11 object to itself be compliant.
My thinking....
I have a whole bunch of data race opportunities, this thread is guaranteed to see the values I have written into the ptr array, but other threads have no guarantee that a inter-thread happens-before relationship have occurred.
However, when I inter-thread communicate my class with the second thread, then the synchronization which occurs to ensure my pointer is synchronized correctly between the initiating thread and the "disposing" thread, create an inter-thread happens-before relationship, which given this occurs after I have called init, that the values seen in thread 2 are the values thread 1 saw, when it started to communicate with the second thread.
So if thread 1 continues to modify the object after it has been given to thread 2, then data races can occur, but assuming the communication between threads is compliant, then the second thread sees the first behavior.
From cppreference memory_order
Sequenced-before
->init() is sequenced before
send_object_to_thread2( myObj );
Happens before
->init() happens before the synchronized communication with thread2.
Inter-thread happens-before
->init() happens before thread 2 gets the data and calls the destructor
->init() is sequenced-before the synchronized write to the inter-thread communication, and the write occurs before the synchronized read.
The actual write-read is ordered, as they are synchronized.
So as long as inter-thread communication of the object is synchronized, and no further modification of the object occurs post- hand-off to the new thread, there is no data race.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With