[C++ Concurrent Programming Practice] Mutual Exclusion and Deadlock

std::mutex

It is generally not recommended to use std::mutex directly like the following. You need to manually lock and unlock it yourself.

void use_lock()
{<!-- -->
while(true)
{<!-- -->
mtx.lock();
shared_data + + ;
std::cout << "shared_data = " << shared_data << std::endl;
std::cout << "current thread is " << std::this_thread::get_id() << std::endl;
mtx.unlock();
std::this_thread::sleep_for(std::chrono::microseconds(10));
}
}

void test_lock()
{<!-- -->
std::thread t1(use_lock);
std::thread t2([]() {<!-- -->
while(true)
{<!-- -->
mtx.lock();
shared_data + + ;
std::cout << "shared_data = " << shared_data << std::endl;
std::cout << "current thread is " << std::this_thread::get_id() << std::endl;
mtx.unlock();
std::this_thread::sleep_for(std::chrono::microseconds(10));
}
});

t1.join();
t2.join();
}
</code><img class="look-more-preCode contentImg-no-view" src="//i2.wp.com/csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreBlack.png" alt ="" title="">

lock_guard

Using the class template std::lock_guard<>, the RAII method is implemented: automatically locking when creating an object and automatically unlocking when destructing.

void use_lock()
{<!-- -->
while(true)
{<!-- -->
{<!-- -->
std::lock_guard<std::mutex> lock(mtx);
shared_data + + ;
std::cout << "shared_data = " << shared_data << std::endl;
std::cout << "current thread is " << std::this_thread::get_id() << std::endl;
} // This scope causes the lock to be released during sleep
std::this_thread::sleep_for(std::chrono::microseconds(10));
}
}

Pointers and references to protected shared data must not be passed outside the scope of the lock.

Thread-safe stack

Sometimes we can aggregate the access and modification of shared data into a function, and add locks within the function to ensure data security. But for read-type operations, even if the read function is thread-safe, the return value is thrown for external use, which is unsafe. For example, for a stack object, when a thread calls empty() or size(), the return value may be correct. However, once the function returns, other threads can immediately access the stack container, such as push or pop an element, the result returned by just calling empty() or size() It makes no sense.

Below is a thread-unsafe stack. The two threads accessed the stack alternately one after another. When both found that it was not empty, they both performed pop. Pop crashed twice.

template<typename T>
class threadunsafe_stack
{<!-- -->
private:
std::stack<T> data;
mutable std::mutex m; // Guaranteed to lock a const object
public:
threadunsafe_stack() {<!-- -->}
threadunsafe_stack(const threadunsafe_stack & amp; other)
{<!-- -->
std::lock_guard<std::mutex> lock(other.m);
m = other.m;
}
threadunsafe_stack & amp; operator=(const threadunsafe_stack & amp;) = delete;
\t
void push(T t)
{<!-- -->
std::lock_guard<std::mutex> lock(m);
data.push(t);
}

// problem code
Tpop()
{<!-- -->
std::lock_guard<std::mutex> lock(m);
auto element = data.top();
data.pop();
return element;
}

bool empty() const
{<!-- -->
std::lock_guard<std::mutex> lock(m);
return data.empty();
}
};
</code><img class="look-more-preCode contentImg-no-view" src="//i2.wp.com/csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreBlack.png" alt ="" title="">

When the following call is made, an error of popping the empty stack will occur.

void test_threadunsafe_unstack()
{<!-- -->
threadunsafe_stack<int> stk;
stk.push(1);

std::thread t1([ & amp;stk]() {<!-- -->
if(!stk.empty())
{<!-- -->
std::this_thread::sleep_for(std::chrono::seconds(1)); // Make the phenomenon more obvious
stk.pop();
}
});

std::thread t2([ & amp;stk]() {<!-- -->
if(!stk.empty())
{<!-- -->
std::this_thread::sleep_for(std::chrono::seconds(1)); // Make the phenomenon more obvious
stk.pop();
}
});

t1.join();
t2.join();
}
</code><img class="look-more-preCode contentImg-no-view" src="//i2.wp.com/csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreBlack.png" alt ="" title="">

You can use the method of throwing exceptions and call data.empty() once in the pop function to determine whether the stack is empty. If it is empty, an exception will be thrown. When using it in the outer layer, you need to catch the exception.
But there are still problems with this pop function. For example, T is a vector type, so inside the pop function, element is of type vector. At first, element stores some int values. The program is fine and the function performs pop operation. Assume that the program memory increases sharply at this time, causing the current When the memory used by the program is large enough, the available effective space is not enough. When the function returns element, there will be a vector that causes failure during copy assignment. Even if we catch the exception and release some space, the stack elements will be popped off the stack and the data will be lost. This is actually caused by improper memory management, but the optimization solution is given in the book C++ Concurrent Programming.

// Implement two versions of pop
std::shared_ptr<T> pop() // Returns a shared_ptr pointing to the element popped from the top of the stack
{<!-- -->
    std::lock_guard<std::mutex> lock(m);
    if (data.empty()) throw empty_stack();
    std::shared_ptr<T> res = std::make_shared<T>(data.top());
    data.pop();
    return res;
}

void pop(T & amp; t) // Pass in reference
{<!-- -->
    std::lock_guard<std::mutex> lock(m);
    if (data.empty()) throw empty_stack();
    t = data.top();
    data.pop();
}
</code><img class="look-more-preCode contentImg-no-view" src="//i2.wp.com/csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreBlack.png" alt ="" title="">

The first pop function can also not throw an exception and directly return nullptr if the stack is empty.

std::shared_ptr<T> pop()
{<!-- -->
    std::lock_guard<std::mutex> lock(m);
    if (data.empty()) return nullptr;
    std::shared_ptr<T> res = std::make_shared<T>(data.top());
    data.pop();
    return res;
}

Deadlock

The opposite order of locking results in waiting for each other. One way to avoid deadlock is to encapsulate the locking and unlocking functions into independent functions. This ensures that the lock is unlocked after the operation is performed in an independent function, and will not cause multiple locks to be used in one function. (This code is easier to write, so I won’t post it).

Lock simultaneously

But we can’t avoid using two mutexes inside a function. We can lock them at the same time.

For example, when we perform a function to exchange two objects, in order to ensure that the exchange is completed correctly, we need to lock the two objects within the function. If both threads run this function and the parameters of the call are opposite, It may cause deadlock. This problem can be solved by locking simultaneously.

std::lock + std::lock_guard<>
class some_big_object
{<!-- -->
private:
int data;
public:
friend void swap(some_big_object & amp; lhs, some_big_object & amp; rhs)
{<!-- -->
// ...
}
};

class X // This is a structure that contains a complex member object and a mutex
{<!-- -->
private:
std::mutex m;
some_big_object some_detail;
public:
X(const some_big_object & amp; sd): some_detail(sd) {<!-- -->}
friend void swap(X & amp; lhs, X & amp; rhs)
{<!-- -->
        if( & amp;lhs == & amp;rhs)
            return ;
std::lock(lhs.m, rhs.m);
std::lock_guard<std::mutex> lock1(lhs.m, std::adopt_lock);
std::lock_guard<std::mutex> lock2(rhs.m, std::adopt_lock);
swap(lhs.some_detail, rhs.some_detail);
}
};
</code><img class="look-more-preCode contentImg-no-view" src="//i2.wp.com/csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreBlack.png" alt ="" title="">
  • You must first determine whether the two exchanged objects are the same object. If so, return immediately, because locking the same std:mutex object twice in a row is undefined behavior.
  • std:lock may throw an exception when it locks lhs.m or rhs.m internally. If std ::lock successfully acquires the lock on one of the mutexes, but throws an exception when acquiring the lock on the other mutex, then the first lock will be released. That is, “all members succeed together”: either all locks are successful, or no locks are acquired and an exception is thrown.
  • The std::adopt_lock in the construction parameter of std::lock_guard indicates that the mutex has been locked. When the std::lock_guard object is constructed No need to lock again in the constructor. (Adoption lock)
C++17 std::scoped_lock<>

std::scoped_lock<> in C++17 can lock multiple mutexes at the same time and release them automatically. (RAII)

friend void swap(X & amp; lhs, X & amp; rhs)
{<!-- -->
    if( & amp;lhs == & amp;rhs)
        return ;
    std::scoped_lock guard(lhs.m, rhs.m); // Also uses C++17 class template parameter deduction
    swap(lhs.some_detail, rhs.some_detail);
}

Level lock

syntaxbug.com © 2021 All Rights Reserved.