3

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?

Additional question

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.

user2443626
  • 93
  • 1
  • 7
  • 3
    C++ standard is oblivious of C#. Can you please clarify what you mean by: "_C++ class that is associated to this C# class, and I want to delete C++ object on disposing C# class..._"? – Ron Aug 23 '17 at 23:02
  • This is positively not a c++ question. – Captain Giraffe Aug 23 '17 at 23:09
  • @captain probably both threading models and how they interact (probably inderspecified) are required to answer this question. What kind of C# operations correspond to C++ sequencimg? What do C# threads act like with regards to C++ memory model? I suspect nobody specified it, and instead we have "whatever the various compilers that mix managed and unmananged code do", but I could be wrong. – Yakk - Adam Nevraumont Aug 23 '17 at 23:22
  • 1
    The disposable pattern is certainly a C++ induced disease. Egged-on by the common "I have to do something *important* in the destructor" feeling that C++ is saddled with. The pattern itself dates from .NET version 1, made pointless in version 2 by the SafeHandle wrapper classes. You can always tell that it is pointless when you can see that the Disposing(bool) overload never does anything useful with *disposing* is false. And thread-safety is never, never a concern, Dispose() can only be called when nobody uses the object anymore. So it is inevitably thread-safe. – Hans Passant Aug 23 '17 at 23:25
  • Dispose can be called whenever, by whatever. Two threads could have a reference to the same disposable. One may call Dispose but that doesn't set the references to null; successive calls can be made (by either thread). In practice (and with at least somewhat decent code) probably not going to happen that way, but to say that 'Dispose can only be called when nobody uses it anymore'... That sounds more like the finalizer – pinkfloydx33 Aug 24 '17 at 00:16
  • @Ron Let's say I'm working on game engine, and I have a Component class which I want to use in C++ and in C#, so I have this native C++ pointer `self`, which points to C++ object in memory, and I can call C++ member functions on using this `self` pointer. For instance, I make a native call to C++ function `Init(self)` in C#, which basically calls `((Component*)self)->Init()` – user2443626 Aug 24 '17 at 13:01
  • @HansPassant But how can I be sure that my class variables are ok? Let's say I acquired resource in C++, and I no longer use the object, so GC decides to recycle C# object and hence call C++ destructor on cleaning unmanaged resources. So if C++ member variables were not synchronized, there's no guarantee that pointer that points to native resource was updated and actually contains the resource acquired in a different thread (I know that it's almost impossible to get this behaviour, but I want to be 100% sure of a proper resource destruction, and not just rely on implementation) – user2443626 Aug 24 '17 at 13:14
  • That just nonsensical, the GC never calls a C++ destructor. Only a C++ compiler knows how to do that. The GC calls a *finalizer*. With a very nice rock-hard guarantee, it only ever does that when an object is not referenced anywhere anymore. None of this has anything to do with IDisposable, calling Dispose() or using the *using* statement is up to the programmer, not automatic and optional. There are not many good scenarios were it is safe to use Dispose() when a variable is accessed by multiple threads, you have to know when they are all done. – Hans Passant Aug 24 '17 at 16:47
  • @HansPassant you got me wrong! Of course GC never calls C++ destructor, that's obvious, I was referring to my specific example where C++ destructor is called via finalizer. My concern is not that variable is accessed on different threads at the same time, but rather that one thread has updated a variable but never let other threads know, and if it still in the L2 cache in finalizer thread, than you end up with the wrong behaviour. Correct me if I'm wrong! – user2443626 Aug 24 '17 at 17:56
  • @HansPassant It seems to me that our assumptions that this code will work properly are only based on the fact that by that time finalizer will acquire an unsynchronized variable from the memory or L3 cache, instead of fetching it from potentially not updated L2/L1 cache with the old value. – user2443626 Aug 24 '17 at 18:01

1 Answers1

2

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.

mksteve
  • 12,614
  • 3
  • 28
  • 50