【问题标题】:Destructor, when object's dynamic variable is locked by mutex will not free it?析构函数,当对象的动态变量被互斥锁锁定时不会释放它?
【发布时间】:2022-08-14 12:41:44
【问题描述】:

我试图一次解决一些复杂的(至少对我而言)异步场景,但我认为理解更简单的情况会更好。

考虑一个已分配内存的对象,由变量携带:

#include <thread>
#include <mutex>
using namespace std;

mutex mu;

class Object
{
public:
  char *var;

  Object()
  {
      var = new char[1]; var[0] = 1;
  }
  ~Object()
  {
      mu.lock();
      delete[]var; // destructor should free all dynamic memory on it\'s own, as I remember
      mu.unlock();
  }
}*object = nullptr;

int main()
{
   object = new Object();

   return 0;
}

如果同时,detached 中的var 变量,即异步线程,将被使用,在另一个线程中该对象将被删除怎么办?

void do_something()
{
    for(;;)
    {
         mu.lock();
         
         if(object)
             if(object->var[0] < 255)
                  object->var[0]++;
             else
                  object->var[0] = 0;

         mu.unlock();
    }
}

int main()
{
   object = new Object();

   thread th(do_something);
   th.detach();

   Sleep(1000);

   delete object;
   object = nullptr;

   return 0;
}
  1. 是否有可能var不会在析构函数中被删除?
  2. 我是否在上面的代码中正确使用mutexdetached 线程?

    2.1 我需要mutex::lockmutex::unlock 以及delete object 线路的掩护吗?

    我也再次单独指出我需要新线程异步.我不需要挂起主线程,而 new 正在运行。我一次需要两个线程。


    附言从评论和答案列表中,我终于明白了最重要的事情之一 - 互斥锁。我认为最大的错误是已经锁定的互斥体跳过了锁定和解锁之间的代码。

    忘记共享变量,互斥锁本身与它无关。互斥锁只是一种安全暂停线程的机制:

    mutex mu;
    
    void a()
    {
        mu.lock();
        Sleep(1000);
        mu.unlock();
    }
    
    int main()
    {
        thread th(a);
        th.detach();
    
        mu.lock(); // hangs here, until mu.unlock from a() will be called
        mu.unlock();
    
        return;
    }
    

    这个概念非常简单——互斥对象(想象)具有标志isLocked,当(任何)线程调用lock 方法并且isLocked 为假时,它只是将isLocked 设置为真。但是,如果isLocked 已经为真,那么低级别的互斥锁会以某种方式挂起调用lock 的线程,直到isLocked 不会变为假。你可以找到lock方法的部分源代码向下滚动this page。代替互斥锁,可能只使用一个布尔变量,但它会导致undefined behaviour

    为什么它被称为共享的东西?因为从多个线程同时使用相同的变量(内存)会产生未定义的行为,所以一个线程到达某个当前可以被另一个线程使用的变量 - 应该等待,直到另一个线程完成使用它,这就是为什么在这里使用互斥锁.

    为什么从不同线程访问互斥锁本身不会产生未定义的行为?不知道,去google一下。

  • 让你的锁尽可能短,使用 std::scoped lock 来管理你的锁(永远不要像你一样手动解锁)。请参阅:en.cppreference.com/w/cpp/thread/scoped_lock。并且不要分离你的线程。您需要,您需要在删除对象之前加入它(否则您将进行生命周期竞赛)。就我个人而言,我更喜欢 std::async 而不是 std::thread。在删除对象之前与未来同步。 (en.cppreference.com/w/cpp/thread/async)
  • 当线程仍在处理对象时,在您的代码中会发生什么被删除。并且您的线程将在进程结束时被终止。换句话说,根本没有生命周期管理。
  • 在这个程序中,你稍微混淆了mu 所保护的东西。在Object 中,它所保护的只是(一个特定的)对Object::var 的访问。但是在do_something 中,它用于确保if(object) 是检查object 是否未被破坏的有效方法。如果执行do_something 的线程试图在delete object;object = nullptr; 之间运行会发生什么?
  • 对不起,我想你没听懂。当main 退出时,你的线程无论如何都会被杀死。通常你想干净地做到这一点。方法是致电join
  • 需要明确的是,人们不只是告诉您将 th.detach(); 替换为 th.join(); 并没有别的。他们还说将th.join(); 移动到main 的末尾,在mainth 工作时需要做的任何其他事情已经完成之后。

标签: c++ mutex stdthread detach stdmutex


【解决方案1】:

我是否在上面的代码中正确使用了带有分离线程的互斥锁?

这些是正交概念。我认为互斥锁使用不正确,因为您只有一个线程变异和访问全局变量,并且您使用互斥锁来同步等待和退出。您应该改为加入该线程。

此外,分离的线程通常是代码异味。应该有一种方法可以在退出主函数之前等待所有线程完成。

我是否需要 mutex::lock 和 mutex::unlock 覆盖也删除对象行?

不,因为析构函数会调用mu.lock(),所以你在这里很好。

var 是否有可能不会在析构函数中被删除?

不,它会让你的主线程等待。不过,有一些解决方案可以在不使用互斥锁的情况下做到这一点。

通常有两种方法可以解决这个问题。您可以阻塞主线程,直到所有其他线程都完成,或者使用共享所有权,以便主线程和线程都拥有object 变量,并且只有在所有所有者都离开时才释放。

要阻塞所有线程直到每个人都完成然后进行清理,您可以使用 C++20 中的std::barrier

void do_something(std::barrier<std::function<void()>>& sync_point)
{
    for(;;)
    {

         
         if(object)
             if(object->var[0] < 255)
                  object->var[0]++;
             else
                  object->var[0] = 0;


    } // break at a point so the thread exits
    sync_point.arrive_and_wait();
}

int main()
{
   object = new Object();

   auto const on_completion = []{ delete object; };

   // 2 is the number of threads. I'm counting the main thread since
   // you're using detached threads
   std::barrier<std::function<void()>> sync_point(2, on_completion);

   thread th(do_something, std::ref(sync_point));
   th.detach();

   Sleep(1000);
   sync_point.arrive_and_wait();

   return 0;
}

Live example

这将使所有线程(其中 2 个)等待,直到所有线程到达同步点。一旦所有线程达到该同步点,它将运行on_completion 函数,该函数将在不再需要该对象时删除一次。

另一种解决方案是使用std::shared_ptr,这样任何人都可以拥有该指针,并且只有在没有人再使用它时才释放它。请注意,您需要删除 object 全局变量并将其替换为局部变量以跟踪共享所有权:

void do_something(std::shared_ptr<Object> object)
{
    for(;;)
    {
         
         if(object)
             if(object->var[0] < 255)
                  object->var[0]++;
             else
                  object->var[0] = 0;

    }
}

int main()
{
   std::shared_ptr<Object> object = std::make_shared<Object>();

   // You need to pass it as parameter otherwise it won't be safe
   thread th(do_something, object);
   th.detach();

   Sleep(1000);

   // If the thread is done, this line will call delete
   // If the thread is not done, the thread will call delete
   // when its local `object` variable goes out of scope.
   object = nullptr;

   return 0;
}

【讨论】:

  • 感谢您的帮助,我已经更新了我的问题,即我需要新线程是异步的,因为正如我所写,我在帖子中提供的案例只是一个简化。实际上,主线程不应该因为另一个线程而暂停,因为它绘制了 gui。并且对象应该是全局的
  • @Ngdgvcb 我认为主要的收获是使用互斥锁进行同步不是一个好主意。条件变量已经更好了。
  • 不,它会让你的主线程等待。不过,有一些解决方案可以在不使用互斥锁的情况下做到这一点。即使新线程已分离?
  • 你为什么这么执着于拆线?您似乎甚至对线程和同步的基础知识都没有太多了解,那么为什么您如此致力于一种特定的方法,以至于每个人都(正确地)告诉您不太可能是适合您情况的正确技术?
  • @Ngdgvcb 是的。屏障方法是其中之一。我强烈建议使用连接线程。分离的线程是代码气味。您应该始终拥有线程的所有者
【解决方案2】:
  1. var 是否有可能在析构函数中不会被删除?

    ~Object()
    {
        mu.lock();
        delete[]var; // destructor should free all dynamic memory on it's own, as I remember
        mu.unlock();
    }
    

    您可能需要等待 lock 完成,但 var 将被删除。

    除了您的程序表现出未定义的行为以及对object 的非受保护并发访问。 (delete object 不受保护,您在另一个线程中阅读它),所以一切都可能发生。

    1. 我是否在上面的代码中正确使用了带有分离线程的互斥锁?

    分离与否无关紧要。

    而且您对互斥锁的使用是错误的/不完整的。 您的互斥锁应该保护哪个变量?

    它似乎是objectvar 的混合体。 如果是var,您可能会缩小do_something 中的范围(仅在if-block 中锁定)

    它目前缺少对object 的一些保护。

    2.1 我是否需要mutex::lockmutex::unlock 以及delete object 线路的掩护?

    是的object 需要保护。

    但是您不能为此使用相同的互斥锁。 std::mutex 不允许在同一个线程中锁定两次(受保护的delete[]var; 在受保护的delete object 内)(std::recursive_mutex 允许这样做)。

    所以我们回到互斥锁保护哪个变量的问题?

    如果只有object(在您的示例中就足够了),它将类似于:

    #include <thread>
    #include <mutex>
    using namespace std;
    
    mutex mu;
    
    class Object
    {
    public:
      char *var;
    
      Object()
      {
          var = new char[1]; var[0] = 1;
      }
      ~Object()
      {
          delete[]var; // destructor should free all dynamic memory on it's own, as I remember
      }
    }*object = nullptr;
    
    void do_something()
    {
        for(;;)
        {
             mu.lock();
             
             if(object)
                 if(object->var[0] < 255)
                      object->var[0]++;
                 else
                      object->var[0] = 0;
    
             mu.unlock();
        }
    }
    
    int main()
    {
       object = new Object();
    
       thread th(do_something);
       th.detach();
    
       Sleep(1000);
    
       mu.lock(); // or const std::lock_guard<std::mutex> lock(mu); and get rid of unlock
       delete object;
       object = nullptr;
       mu.unlock();
    
       return 0;
    }
    

    或者,由于您不必在线程之间共享数据,您可以这样做:

    int main()
    {
       Object object;
    
       thread th(do_something);
    
       Sleep(1000);
    
       th.join();
       return 0;
    }
    

    并摆脱所有互斥锁

【讨论】:

  • @Ngdgvcb:1)是的。 2)线程在创建时开始(除非它是默认构造)。 detach 主要是看到一个气味代码,因为安全/干净地处理该线程的结尾更难。
  • 那么,如果考虑您的第一个带有分离的示例,这意味着我必须在每次出现时mutex::lock 共享变量?
  • 分离与否与互斥锁的使用无关。您必须保护非原子共享变量(如果至少一个访问是写入)。
  • 好吧,我认为第一个例子是最接近我需要的东西,但是感谢大家,如果可以的话,我会赞成答案
【解决方案3】:

看看这个,它展示了 scoped_lock、std::async 的使用以及通过范围管理生命周期(这里的演示:https://onlinegdb.com/FDw9fG9rS

#include <future>
#include <mutex>
#include <chrono>
#include <iostream>

// using namespace std; <== dont do this
// mutex mu; avoid global variables.

class Object
{
public:
    Object() :
        m_var{ 1 }
    {
    }

    ~Object()
    {
    }

    void do_something()
    {
        using namespace std::chrono_literals;

        for(std::size_t n = 0; n < 30; ++n)
        {
            // extra scope to reduce time of the lock
            {
                std::scoped_lock<std::mutex> lock{ m_mtx };
                m_var++;
                std::cout << ".";
            }

            std::this_thread::sleep_for(150ms);
        }
    }


private:
    std::mutex m_mtx;
    char m_var;
    
};

int main()
{
    Object object;

    // extra scope to manage lifecycle of future
    {
        // use a lambda function to start the member function of object
        auto future = std::async(std::launch::async, [&] {object.do_something(); });

        std::cout << "do something started\n";
        
        // destructor of future will synchronize with end of thread;
    }
    std::cout << "\n work done\n";

    // safe to go out of scope now and destroy the object
    return 0;
}

【讨论】:

  • 似乎很有趣,但我有疑问(尽管我可以在您提供的链接中进行测试):1)它是否可以将对象用作全局变量和指向对象的指针?
  • 我不做全局变量,它们最终会成为大型项目的痛苦。此外,使用 new/delete 不再是推荐的工作方式(内存泄漏太多,因为不清楚何时应该删除内存)。如果您需要动态分配使用 std::make_unique/std::make_shared。在编写 C++ 代码时,您需要在设计时考虑到对象的生命周期。至于全局数据,在 main 中创建一个带有数据的结构实例并将其传递给需要它的类(一种称为依赖注入的模式,它确实会帮助您在未来编写更多可维护的代码)
  • 等等,在你的例子中我会看到完成工作至少在 150 毫秒后才发送消息,因为 future'is 析构函数会挂起主线程?
  • 我在网站上也注意到了这一点,但在我的本地机器上没有。如果您将 std::endl 添加到输出中,您将看到它没有被阻止(网站不会经常刷新 cout)。所以未来真的是在等待线程完成它的工作。像这样:onlinegdb.com/M9gdqqG-r
【解决方案4】:

您在问题中假设和提出的所有问题都是正确的。该变量将始终被释放。

但是你的代码有一个大问题。让我们看看你的最后一个例子:

int main()
{
   object = new Object();

   thread th(do_something);
   th.detach();

   Sleep(1000);

   delete object;
   object = nullptr;

   return 0;
}

您创建一个将调用do_something() 的线程。但是让我们假设在线程创建后内核立即中断线程并执行其他操作,例如使用此答案更新 Web 浏览器中的 stackoverflow 选项卡。所以do_something() 还没有被调用,并且暂时不会调用,因为我们都知道浏览器有多慢。

同时主函数休眠1秒,然后调用delete object;。这会调用Object::~Object(),它获取互斥体并删除变量并释放互斥体并最终释放对象。

一段时间后,另一个线程再次运行,但现在varobject 不再存在。繁荣

【讨论】:

    猜你喜欢
    • 2017-02-23
    • 1970-01-01
    • 2011-03-17
    • 2015-03-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-25
    • 1970-01-01
    相关资源
    最近更新 更多