Consider the following example code:
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <random>
#include <array>
#include <numeric>
int main()
{
constexpr std::size_t num_mutices = 3;
constexpr std::size_t num_entries = 6;
constexpr std::size_t num_threads = 4;
std::array<std::mutex, num_mutices> mutices;
std::array<std::size_t, num_entries> data;
// fill data with 1, 2, ...
std::iota(data.begin(), data.end(), 1);
// setup random generators
std::random_device rd;
std::vector<std::mt19937> rand_gens;
for(std::size_t i = 0; i < num_threads; i++)
rand_gens.push_back(std::mt19937(rd()));
std::vector<std::thread> threads;
for(std::size_t iThread = 0; iThread < num_threads; iThread++)
{
threads.emplace_back(std::thread([&data, &mutices, &rand_gens, iThread]()
{
// distribution for picking a random value from data
std::uniform_int_distribution<std::size_t> dist(0,data.size()-1);
for(std::size_t a = 0; a < 100; a++)
{
auto iFirst = dist(rand_gens[iThread]);
auto iSecond = dist(rand_gens[iThread]);
std::unique_lock<std::mutex> lock1(mutices[iFirst % mutices.size()]);
std::unique_lock<std::mutex> lock2(mutices[iSecond % mutices.size()]);
std::lock(lock1, lock2);
std::size_t tmp = data[iFirst];
data[iFirst] = data[iSecond];
data[iSecond] = tmp;
mutices[iFirst % mutices.size()].unlock();
mutices[iSecond % mutices.size()].unlock();
}
}));
}
for(std::size_t i = 0; i < num_threads; i++)
threads[i].join();
}
Basically I have multiple threads accessing elements of a vector at random and swapping them. Of course this is just a minimal working example of what I want to do (adding edges to a bidirectional boost::graph).
The problem is that this program can run into a deadlock and I don't know how to account for that.
I think this can be done by using hierachical mutices. And I found the following nice explanation: Use Lock Hierarchies to Avoid Deadlock
It states:
Rule 1: While holding a lock on a mutex at level N, you may only acquire new locks on mutexes at lower levels
So the levels in our case are the indices of the mutices used. Thus I should calculate the min and max of the mutices to lock and obtain them in that order. However this still leads to deadlock as the following scenario shows:
Thread 1 wants to lock Mutex 1 and Mutex 3. Thread 2 wants to lock Mutex 2 and Mutex 3. Now T1 locks M1, T2 lock M2 and now both want to obtain M3.
I tried solve this by using try_lock and unlocking mutices again if the thread could not obtain all needed mutices but somehow I don't seem to get it :P.