C#中的可释放模式用于C++对象销毁

3

我发现了微软提供的可抛弃模式的实现:https://msdn.microsoft.com/zh-cn/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);
   }
}

假设我有一个与这个C#类相关联的C++类,并且我想在处理C#类时删除 C++ 对象,以确保我的未托管资源被正确释放。我添加了一个函数 DestructNative(self),它基本上会在关联的C++对象上进行原生的C++调用delete (CppObject*)self。因此,我的代码如下:

   // 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;
   }

我的问题是,由于知道C#终结器可以从不同的线程调用,我是否需要在C++对象的析构函数中提供同步以确保在从C#终结器调用Dispose(false)时没有任何竞争条件?

附加问题

微软的可处理模式是否有问题?似乎disposed标志是一个简单的变量,如果从不同的线程调用终结器,则不会进行同步。


3
C++标准不知道C#。您能否澄清一下您所说的:“与此C#类相关联的C++类,我想在处理C#类时删除C++对象...”的意思? - Ron
@captain,也许需要同时考虑线程模型及其交互(可能未明确规定)才能回答这个问题。C#中哪些操作对应于C++的排序?就C++内存模型而言,C#线程表现如何?我怀疑没有人指定过,而是“以各种混合管理和不管理代码的编译器为准”,但我可能错了。 - Yakk - Adam Nevraumont
1
可被丢弃的模式绝对是C++引发的疾病。这种感觉源于C++常见的“我必须在析构函数中做一些重要的事情”的负担。该模式本身可以追溯到.NET 1版本,但在版本2中由SafeHandle包装类变得无意义。当您可以看到Disposing(bool)重载在disposing为false时从未执行任何有用操作时,您总是可以确定它是无意义的。而线程安全永远不会是一个问题,只有在没有人再使用对象时才能调用Dispose()。因此,它必然是线程安全的。 - Hans Passant
Dispose可以在任何时候、由任何方式调用。两个线程可能引用同一个可处理对象。其中一个线程可能会调用Dispose,但这并不会将引用设置为null;随后的调用可以由任一线程进行。实际上(并且使用至少相当不错的代码),可能不会以这种方式发生,但说“只有在没有人再使用它时才能调用Dispose”...那听起来更像是终结器。 - pinkfloydx33
@HansPassant 我认为我们假设这段代码能够正常工作的前提只是基于最终器会从内存或L3缓存中获取一个非同步变量,而不是从可能未更新的L2/L1缓存中获取旧值。 - user2443626
显示剩余5条评论
1个回答

2
“微软可处理模式”是否有问题?似乎已处理的标志是一个简单的变量,如果终结器从不同的线程调用,则无法同步。
不,它没有问题。这个问题提出了两个有趣的问题。对于一个先于C++11思想并且不知道线程的类,以下内容的影响是什么。
 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;
     }
 }

代码之后。
PreCpp11 * myObj = new PreCpp11();
myObj->init();
send_object_to_thread2( myObj );

线程2执行的位置。
 PreCpp11 obj = get_obj_from_sync();
 delete obj;

如果在不同的线程上调用析构函数,我们如何避免数据竞争?
考虑到一种可处理的实现,那么像上面那样会引起数据竞争吗?
我认为在这两种情况下,答案都是代码是可以接受和符合规范的。然而,它依赖于 PreCpp11 对象之间的跨线程通信符合规范。
我的想法是...
我有很多数据竞争的机会,这个线程保证能看到我写入 ptr 数组的值,但是其他线程不能保证已经发生了跨线程 happens-before 关系。
然而,当我使用第二个线程与类进行跨线程通信时,确保指针在启动线程和“处理”线程之间正确同步的同步创建了一个跨线程 happens-before 关系。这发生在我调用 init 之后,假设线程 1 在将对象传递给线程 2 后继续修改对象,则可能会发生数据竞争,但是假设线程之间的通信符合规范,则第二个线程看到的是第一次行为。
来自cppreference memory_order顺序之前
 ->init() is sequenced before 
 send_object_to_thread2( myObj );

发生在之前
->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.

只要对象的线程间通信是同步的,并且在将对象移交给新线程后不会进行进一步修改,就不会出现数据竞争。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接