0

I'm trying to solve some complicated (for me at least) asynchronous scenario at once, but I think it will be better to understand more simple case.

Consider an object, that has allocated memory, carrying by variable:

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

What if while, it's var variable in detached, i.e. asynchronous thread, will be used, in another thread this object will be deleted?

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. Is is it possible that var will not be deleted in destructor?
  2. Do I use mutex with detached threads correctly in code above?

2.1 Do I need cover by mutex::lock and mutex::unlock also delete object line?

I also once again separately point that I need new thread to be asynchronous. I do not need the main thread to be hanged, while new is running. I need two threads at once.


P.S. From a list of commentaries and answers one of most important thing I finally understood - mutex. The biggest mistake I thought is that already locked mutex skips the code between lock and unlock.

Forget about shared variables, mutex itself has noting to do with it. Mutex is just a mechanism for safely pause threads:

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

The concept is extremely simple - mutex object (imagine) has flag isLocked, when (any) thread calls lock method and isLocked is false, it just sets isLocked to true. But if isLocked is true already, mutex somehow on low-level hangs thread that called lock until isLocked will not become false. You can find part of source code of lock method scrolling down this page. Instead of mutex, probably just a bool variable could be used, but it will cause undefined behaviour.

Why is it referred to shared stuff? Because using same variable (memory) simultaneously from multiple threads makes undefined behaviour, so one thread, reaching some variable that currently can be used by another - should wait, until another will finish working with it, that's why mutex is used here.

Why accessing mutex itself from different threads does not make undefined behaviour? I don't know, going to google it.

Ngdgvcb
  • 155
  • 6
  • 1
    Keep you lock as short as possible, use std::scoped lock to manage your lock (never lock unlock manually like you do). See : https://en.cppreference.com/w/cpp/thread/scoped_lock. And don't detach your thread. You need, you need to join it before you delete the object (or you will have a life cycle race). Personally I prefer std::async over std::thread. Synchronize with the future before deleting the object. (https://en.cppreference.com/w/cpp/thread/async) – Pepijn Kramer Aug 09 '22 at 13:57
  • What happens in your code that object is deleted while the thread is still working on it. And your thread will be killed by end of process. In other words no life cycle managment at all. – Pepijn Kramer Aug 09 '22 at 13:59
  • why do you detach the thread? Why do you use manual memory managment via raw pointers (rather than `std::vector`)? – 463035818_is_not_an_ai Aug 09 '22 at 14:00
  • you should use one of the RAII locks. Manually locking and unlocking a mutex is not excpetion safe – 463035818_is_not_an_ai Aug 09 '22 at 14:01
  • @PepijnKramer, well `join`, as I understand do not make new thread run asynchronously. I need to run new thread asynchronously, because main thread meant to draw GUI stuff, and should not be hanged – Ngdgvcb Aug 09 '22 at 14:03
  • `object = new Object();` -- Whenever I see this in `main`, it indicates that the programmer is not aware that C++ is not C#, Java, or any of those other languages that require `new` to create objects and/or they have gone overboard using `new` everywhere. This should be `Object object;`, and let the process of destruction of `object` naturally occur when the last `}` of `main` occurs. – PaulMcKenzie Aug 09 '22 at 14:05
  • @463035818_is_not_a_number why detach - answered in previous comment. Why use pointer - generally because I just prefer it, but in specific case I am talking about - it is a direct2d com pointer, it should to be released manually. – Ngdgvcb Aug 09 '22 at 14:05
  • you don't need to detach a thread to make it run asynchronously. In your example you can join it before `main` exits. Well, you could, if your thread had some mechanism to stop. Thats one issue, your thread never finishes execution. Detatching it is the wrong solution – 463035818_is_not_an_ai Aug 09 '22 at 14:07
  • @PaulMcKenzie, it is just simplification, object can be created whenever in any place in code, it does not matter – Ngdgvcb Aug 09 '22 at 14:07
  • @Ngdgvcb -- *it is just simplification,* -- `Object object;` looks simpler to me. No pointers, and no need for `delete object;` – PaulMcKenzie Aug 09 '22 at 14:08
  • using `new` to create an object is not "simplification", rather it is obfuscation and unecessary compliation. Frankly, that you call it "simplification" suggests that Paul wasnt that wrong with his comment. – 463035818_is_not_an_ai Aug 09 '22 at 14:08
  • @PaulMcKenzie, what if I want to provide that object via com from dll to client program? I have this object in dll, and I want to use it via abstract class from the client. The pointer to object, and the object is necessary in this case, for example – Ngdgvcb Aug 09 '22 at 14:10
  • @463035818_is_not_a_number, so, if call `join` it also run asynchronously? – Ngdgvcb Aug 09 '22 at 14:11
  • @Ngdgvcb -- I am strictly looking at the code you posted. A C++ programmer aware of object lifetimes would not use `new` in their examples like that. Also, maybe the problem gets more interesting if you let the object destroy itself by letting it go out-of-scope instead of forcing the issue with `new` and `delete`. – PaulMcKenzie Aug 09 '22 at 14:11
  • @PaulMcKenzie, however, I described example of the case where there is no other way. Only pointer to object, only `new`. Another case, is more simplier and obvious - when I just need to use object out of scope - when the function, where the object was created is returned. I think it it wrong dispute at all – Ngdgvcb Aug 09 '22 at 14:14
  • @Ngdgvcb `join` blocks, though you only call it when you need the thread to finish and that is latest when `main` exits. Not calling `join` on a `std::thread` is an error, and detaching it is the wrong workaround – 463035818_is_not_an_ai Aug 09 '22 at 14:14
  • 1
    In this program, you've slightly mixed what `mu` is guarding. Within `Object`, all it guards is (one specific) access to `Object::var`. But within `do_something`, it's used to make sure that `if(object)` is a valid way of checking that `object` hasn't been destroyed. What happens if the thread executing `do_something` tries to act between `delete object;` and `object = nullptr;`? – Nathan Pierson Aug 09 '22 at 14:16
  • @463035818_is_not_a_number, sorry, probably my bad English, but I didn't understand the answer. `join` makes thread run asynchronously, i.e. in the case I provided, after `join` the main thread will sleep for 1 sec and the joined will run? – Ngdgvcb Aug 09 '22 at 14:20
  • @NathanPierson, yeah, that's why I added `2.1` to the end of the post. So I need to have mutex also there? But what if it is locked? Then it will not be deleted? – Ngdgvcb Aug 09 '22 at 14:23
  • the thread runs asynchronously as soon as you create it. The call to `join` is not asynchronous. It blocks until the thread returns. Though you only call `join` when you do not need the thread to run asynchronously anymore – 463035818_is_not_an_ai Aug 09 '22 at 14:24
  • The call to `mu.lock()` is blocking if `mu` is already locked. Do you think that if `mu` is already locked when a thread attempts to execute `mu.lock()`, it just skips over all the code until the next `mu.unlock()`? – Nathan Pierson Aug 09 '22 at 14:29
  • Re-using `new`: _I want to use it via abstract class from the client_ Polymorphism moves the goalposts, you didn't show that in your original question. `std::unique_ptr` (or, sometimes, `std::shared_ptr`) is the usual solution. – Paul Sanders Aug 09 '22 at 14:35
  • @NathanPierson I do not actually know, You explain me, please – Ngdgvcb Aug 09 '22 at 14:38
  • @463035818_is_not_a_number *It blocks until the thread returns.* - it blocks current thread? I.e. it will not call next lines of code of current thread after `join` is called until the `joined` thread will not finished, or what? – Ngdgvcb Aug 09 '22 at 14:40
  • yes when a function is said to block, then it means that the function does not return immediately, as opposed to being asynchronous, in which case you expect it to return immediately – 463035818_is_not_an_ai Aug 09 '22 at 14:45
  • @463035818_is_not_a_number, understood, but this is not the behaviour I need – Ngdgvcb Aug 09 '22 at 14:50
  • 2
    Sorry, I think you didnt understand. When `main` exits your thread will be killed anyhow. Usually you want to do this cleanly. And the way to do it is to call `join`. – 463035818_is_not_an_ai Aug 09 '22 at 14:54
  • 1
    To be clear, people aren't just telling you to replace `th.detach();` with `th.join();` and nothing else. They're also saying to move `th.join();` to the end of `main`, after whatever other things `main` needs to do while `th` is working have finished. – Nathan Pierson Aug 09 '22 at 14:55
  • @463035818_is_not_a_number, no I think You didn't yet. I ask another way, and it will be extremely good if the answer will be just yes or no - does `join` call mean "**Pause (at `join` call line) current thread and wait while new thread is ended, if it is not ended yet**"? – Ngdgvcb Aug 09 '22 at 15:30
  • @Ngdgvcb yes. And thats exactly what one usually wants to do when the thread has finished its job. Its unclear why you don't want that. Once `main` returns the thread cannot do anything meaningful anyhow. Detaching a thread is a common beginners workaround to avoid properly joining a thread and to avoid implementing a mechanism to signal to it that it should stop doing what it does. And the issue you face in the code is a consequence of that. – 463035818_is_not_an_ai Aug 09 '22 at 15:34
  • @463035818_is_not_a_number, okay but then it means that just by creating `thread` object without `join` or `detach` it already runs *asynchronous* thread. What is the sense of `detach`? Without detach `join` mechanism will be called in `thread` destructor? – Ngdgvcb Aug 09 '22 at 15:39
  • @Ngdgvcb you can forget that `detach` exists for now. Whenever I thought I encountered a use case for it, it later turned out that it was a mistake, and created more issues that it was supposed to solve. Detaching a thread may look like the simple solution to you, but actually it is the diffucult one. The purpose of `detach` is **not** to make the thread run asynchronously. – 463035818_is_not_an_ai Aug 09 '22 at 15:45
  • @463035818_is_not_a_number, it is not the actual answer, but I think I can test in by myself. Thanks You and other a lot however – Ngdgvcb Aug 09 '22 at 16:09

4 Answers4

2

Do I use mutex with detached threads correctly in code above?

Those are orthogonal concepts. I don't think mutex is used correctly since you only have one thread mutating and accessing the global variable, and you use the mutex to synchronize waits and exits. You should join the thread instead.

Also, detached threads are usually a code smell. There should be a way to wait all threads to finish before exiting the main function.

Do I need cover by mutex::lock and mutex::unlock also delete object line?

No since the destructor will call mu.lock() so you're fine here.

Is is it possible that var will not be deleted in destructor?

No, it will make you main thread to wait though. There are solutions to do this without using a mutex though.

There's usually two way to attack this problem. You can block the main thread until all other thread are done, or use shared ownership so both the main and the thread own the object variable, and only free when all owner are gone.

To block all thread until everyone is done then do cleanup, you can use std::barrier from C++20:

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

This will make all the threads (2 of them) wait until all thread gets to the sync point. Once that sync point is reached by all thread, it will run the on_completion function, which will delete the object once when no one needs it anymore.

The other solution would be to use a std::shared_ptr so anyone can own the pointer and free it only when no one is using it anymore. Note that you will need to remove the object global variable and replace it with a local variable to track the shared ownership:

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;
}
Guillaume Racicot
  • 39,621
  • 9
  • 77
  • 141
  • Thanks for help, I already updated my question with the idea that I need new thread to be asynchronous, because as I wrote, the case I provide in the post is just a simplification. In reality main thread should not be paused because of another threads because it draws the gui. And the object should be global – Ngdgvcb Aug 09 '22 at 14:32
  • @Ngdgvcb I think the main takeaway is that using mutex for synchronization is not a good idea. condition variables are already better. – Guillaume Racicot Aug 09 '22 at 14:40
  • *No, it will make you main thread to wait though. There are solutions to do this without using a mutex though.* even if the new thread is detached? – Ngdgvcb Aug 09 '22 at 14:41
  • 2
    Why are you so insistent on detaching the thread? You don't seem to have much awareness of even the basics of threading and synchronization, so why are you so committed to one specific approach that everyone is (correctly) telling you is highly unlikely to be the right technique for your situation? – Nathan Pierson Aug 09 '22 at 14:51
  • @Ngdgvcb Yes. The barrier approach is one. I would highly recommend using a joined thread though. Detached thread are code smell. You should always have an owner for a thread – Guillaume Racicot Aug 09 '22 at 15:09
0

Have a look at this, it shows the use of scoped_lock, std::async and managment of lifecycles through scopes (demo here : 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;
}
Pepijn Kramer
  • 9,356
  • 2
  • 8
  • 19
  • Seems interesting, but I have question (though I can jsut test in the link You provided): 1) Will it work with object as global variable and as a pointer to object? – Ngdgvcb Aug 09 '22 at 14:35
  • I don't do global variables, they end up to be a pain in larger projects. Also the use of new/delete is no longer a recommended way of working (too many memory leaks, becuase it is not clear who should delete memory when). If you need to dynamically allocate use std::make_unique/std::make_shared. When writing C++ code you need to design with lifetime of objects in mind. As for global data, make an instance of a struct with data in main and pass it around to classes that need it (a pattern called dependency injection, it will really help you write more maintainable code in the future) – Pepijn Kramer Aug 09 '22 at 14:44
  • Wait, in Your example I will see *work done* message only after at leat 150ms, because `future`'s destructor will hang main thread? – Ngdgvcb Aug 09 '22 at 14:49
  • I noticed that on the website too, but not on my local machine. If you add a std::endl to the output you will see it is not blocked (the website doesn't flush cout often enough). So the future is really waiting for the thread to finish its work. Like this : https://onlinegdb.com/M9gdqqG-r – Pepijn Kramer Aug 09 '22 at 14:58
0
  1. Is is it possible that var will not be deleted in destructor?

With

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

You might have to wait that lock finish, but var would be deleted.

Except that your program exhibits undefined behaviour with non protected concurrent access to object. (delete object isn't protected, and you read it in your another thread), so everything can happen.

  1. Do I use mutex with detached threads correctly in code above?

Detached or not is irrelevant.

And your usage of mutex is wrong/incomplete. which variable should your mutex be protecting?

It seems to be a mix between object and var. If it is var, you might reduce scope in do_something (lock only in if-block)

And it misses currently some protection to object.

2.1 Do I need cover by mutex::lock and mutex::unlock also delete object line?

Yes object need protection.

But you cannot use that same mutex for that. std::mutex doesn't allow to lock twice in same thread (a protected delete[]var; inside a protected delete object) (std::recursive_mutex allows that).

So we come back to the question which variable does the mutex protect?

if only object (which is enough in your sample), it would be something like:

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

Alternatively, as you don't have to share data between thread, you might do:

int main()
{
   Object object;

   thread th(do_something);

   Sleep(1000);

   th.join();
   return 0;
}

and get rid of all mutex

Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • @Ngdgvcb: 1) yes. 2) thread start at creation (unless it is default construct). `detach` is mostly see a smell code as handling end of that thread safely/cleanly is harder. – Jarod42 Aug 09 '22 at 15:45
  • So, if to consider Your first example with detach, it means that I have to `mutex::lock` shared variables in every occurrence? – Ngdgvcb Aug 09 '22 at 15:48
  • Detached or not has nothing to do with mutex usage. You have to protect non-atomic shared variables (if at least one access is a write). – Jarod42 Aug 09 '22 at 16:01
  • Well, I think first example is the closest thing to what I need, however thanks to everyone, I'd upvote answers, if I could – Ngdgvcb Aug 09 '22 at 19:07
0

All you assumed and asked in your question is right. The variable will always be freed.

But your code has one big problem. Lets look at your example:

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

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

   Sleep(1000);

   delete object;
   object = nullptr;

   return 0;
}

You create a thread that will call do_something(). But lets just assume that right after the thread creation the kernel interrupts the thread and does something else, like updating the stackoverflow tab in your web browser with this answer. So do_something() isn't called yet and won't be for a while since we all know how slow browsers are.

Meanwhile the main function sleeps 1 second and then calls delete object;. That calls Object::~Object(), which acquires the mutex and deletes the var and releases the mutex and finally frees the object.

Now assume that right at this point the kernel interrupts the main thread and schedules the other thread. object still has the address of the object that was deleted. So your other thread will acquire the mutex, object is not nullptr so it accesses it and BOOM.

PS: object isn't atomic so calling object = nullptr in main() will also race with if (object).

Goswin von Brederlow
  • 11,875
  • 2
  • 24
  • 42