1

I am learning to use c++ lock_guard. Online resources say that we do not need to unlock manually and secondly in the case of an exception the mutex is automatically released so that other threads can proceed.

I am trying to find an example for the second case. Basically, I am trying to find use case when one thread gets an exception then the other thread should continue.

std::mutex m;
void f1() {
    lock_guard<std::mutex> lock(m);
    // some task  that may raise exception
}
void f2() {
    lock_guard<std::mutex> lock(m);
    // some other task
}
int main() {
    std::thread T1(f1);   
    T1.detach();

    std::thread T2(f2);   
    T2.join();
}

I tried with divided by zero arithmetic inside f1. But it crashes the whole program. Then I tried with allocating a very large memory (for example new int[100000000000]) inside f1. Then also the whole program crashed saying bad_alloc.

std::mutex m;
int a,b;
void f1() {
    lock_guard<std::mutex> lock(m);
    a = 1;
    int * ptr = new int[10000000000]; // too large
    b = 2;
}
void f2() {
    lock_guard<std::mutex> lock(m);
    cout << a <<" : "<<b <<endl;
}
int main() {
    std::thread T1(f1);   
    T1.detach();

    std::thread T2(f2);   
    T2.join();
}

error:

terminate called after throwing an instance of 'std::bad_alloc'
  what():  std::bad_alloc
Aborted (core dumped)

If I use try-catch block around the problematic code segment, then thread2 executes and the program does not terminate abruptly. But now T1 does not release the lock(as expected from try catch block).

std::mutex m;
int a,b;
void f1() {
    lock_guard<std::mutex> lock(m);
    a = 1;
    try {
        int * ptr = new int[10000000000];
    }catch(...) {
        cout <<"new faild"<<endl;
    }
    // still locked
    std::this_thread::sleep_for(std::chrono::milliseconds(2000)); //2s
    b = 2;
}
void f2() {
    lock_guard<std::mutex> lock(m);
    cout << a <<" : "<<b <<endl;
}
int main() {
    std::thread T1(f1);   
    T1.detach();

    std::thread T2(f2);   
    T2.join();
}

I am also not convinced with the try-catch block in the above situation because of the whole point of not using mutex.lock()/unlock() was to gracefully handle and releasing of the mutex.

Am I missing something? Please give one example where an exception occurs (some common exception cases) in one thread and the mutex is released and other threads continue to execute. Also, the main program should also not terminate.

Thanks!

Debashish
  • 1,155
  • 19
  • 34
  • 2
    Move the lock guard into the try block... – StoryTeller - Unslander Monica Dec 24 '18 at 09:14
  • 3
    If an exception escapes from a thread without being caught, it terminates the program. This is completely independent from the use of scope guards or any such thing. That means that your example program is unsuitable to demonstrate anything. BTW: If you want an exception thrown, just throw one, no need to try to trigger that indirectly. One more note: The term you need to research is called "RAII idiom". – Ulrich Eckhardt Dec 24 '18 at 09:19
  • 1
    Re: "I tried with divide by zero" -- the error message for divide by zero might **say** "exception", but it's not a C++ exception. C++ exceptions are explicitly thrown. – Pete Becker Dec 24 '18 at 15:00

2 Answers2

9

std::lock_guard is a very simple class. It looks something like this:

template <typename T>
class lock_guard
{
public:
    lock_guard(T& mtx)
        : mtx_{mtx}
    {
        mtx_.lock();
    }

    ~lock_guard()
    {
        mtx_.unlock();
    }

    // not copyable
    lock_guard(const lock_guard&) = delete;
    lock_guard& operator=(const lock_guard&) = delete;

private:
    T& mtx_;
};

As you can see, all it does is lock the mutex in its constructor and unlock it in its destructor.

This is useful because an objects destructor is called when it goes out of scope for any reason, including when an exception is thrown (if it gets caught somewhere; an uncaught exception terminates the application):

std::mutex mutex;

void thread_func()
{
    try {
        std::lock_guard<std::mutex> guard{mutex};
        // mutex is now locked
        throw std::exception{};
    } catch (...) {
        // mutex is already unlocked here.
    }
    // mutex is also unlocked here.
}
Miles Budnek
  • 28,216
  • 2
  • 35
  • 52
  • Thanks! Just want to ask, if we are using `try` and `catch` block, we can also use `mutex.lock()` inside of `try()` and `mutex.unlock()` inside `catch()`. Is that equivalent to your last code section ?. Also `mutex.unlock()` outside everything. – Debashish Dec 24 '18 at 09:37
  • 5
    @Debashish - And what happens when you add a `return` statement in the try block to exit the function and forget to unlock the mutex? Real code is complex and has many flows of control. RAII allows you to keep it from getting intractable. – StoryTeller - Unslander Monica Dec 24 '18 at 09:39
  • 1
    Yes, that would be equivalent in this case. It's easy to forget to manually unlock a mutex in every code path though. It's also very often the case that the code actually doing the mutex locking and unlocking doesn't directly catch exceptions, and leaves that job to some function higher up the call stack. – Miles Budnek Dec 24 '18 at 09:41
  • @StoryTeller. The `return` case is a good valid point. thanks! – Debashish Dec 24 '18 at 09:43
1

The point is similar to constructors/destructors: automatically release ("destruct") the mutex ownership. This also means that the destructor of the lock_guard object will be automatically called when stack unwrapping due to an exception occurs.

In your code, you catch the exception before the lock_guard releases the mutex, so there is no benefit. Put the lock_guard inside the try block.

Michael Chourdakis
  • 10,345
  • 3
  • 42
  • 78