Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Boost shared_lock. Read preferred?

Tags:

c++

boost

I was checking out the boost library(version 1.45) for a reader/writer lock. When I ran my tests on it, it seemed like the shared_ptr was preferring my reader threads, i.e. when my writer tried to take the lock for its operation, it didn't stop any subsequent reads from occurring.

Is it possible in boost to change this behavior?

using namespace std;
using namespace boost;

mutex outLock;
shared_mutex workerAccess;
bool shouldIWork = true;

class WorkerKiller
{
public:   

    void operator()()  
    {
        upgrade_lock<shared_mutex> lock(workerAccess); 
        upgrade_to_unique_lock<shared_mutex> uniqueLock(lock);

        cout << "Grabbed exclusive lock, killing system" << endl;
        sleep(2);
        shouldIWork = false;
        cout << "KILLING ALL WORK" << endl;  
    }  

private:  
};

class Worker
{  
public:   

    Worker()
    {  
    }  

    void operator()()  
    {
        shared_lock<shared_mutex> lock(workerAccess); 

        if (!shouldIWork) {
            outLock.lock();
            cout << "Workers are on strike.  This worker refuses to work" << endl;
            outLock.unlock();
        } else {
            sleep(1);

            outLock.lock(); 
            cout << "Worked finished her work" << endl;
            outLock.unlock(); 
        }
    }  
};  

int main(int argc, char* argv[])  
{  
    Worker w1;
    Worker w2;
    Worker w3;
    Worker w4;
    WorkerKiller wk;

    boost::thread workerThread1(w1);
    boost::thread workerThread2(w2);

    boost::thread workerKillerThread(wk);

    boost::thread workerThread3(w3);
    boost::thread workerThread4(w4);

    workerThread1.join();
    workerThread2.join();
    workerKillerThread.join();
    workerThread3.join();

    return 0;
}

And here is the output every time:

Worked finished her work
Worked finished her work
Worked finished her work
Worked finished her work
Grabbed exclusive lock, killing system
KILLING ALL WORK

My Requirement

If the writer tried to grab an exclusive lock, I'd like for all previous read operations to finish. And then all subsequent read operations to block.

like image 349
anoneironaut Avatar asked Aug 22 '12 22:08

anoneironaut


Video Answer


2 Answers

I'm a little late to this question, but I believe I have some pertinent information.

The proposals of shared_mutex to the C++ committee, which the boost libs are based on, purposefully did not specify an API to give readers nor writers priority. This is because Alexander Terekhov proposed an algorithm about a decade ago that is completely fair. It lets the operating system decide whether the next thread to get the mutex is a reader or writer, and the operating system is completely ignorant as to whether the next thread is a reader or writer.

Because of this algorithm, the need for specifying whether a reader or writer is preferred disappears. To the best of my knowledge, the boost libs are now (boost 1.52) implemented with this fair algorithm.

The Terekhov algorithm consists of having the read/write mutex consist of two gates: gate1 and gate2. Only one thread at a time can pass through each gate. The gates can be implemented with a mutex and two condition variables.

Both readers and writers attempt to pass through gate1. In order to pass through gate1 it must be true that a writer thread is not currently inside of gate1. If there is, the thread attempting to pass through gate1 blocks.

Once a reader thread passes through gate1 it has read ownership of the mutex.

When a writer thread passes through gate1 it must also pass through gate2 before obtaining write ownership of the mutex. It can not pass through gate2 until the number of readers inside of gate1 drops to zero.

This is a fair algorithm because when there are only 0 or more readers inside of gate1, it is up to the OS as to whether the next thread to get inside of gate1 is a reader or writer. A writer becomes "prioritized" only after it has passed through gate1, and is thus next in line to obtain ownership of the mutex.

I used your example compiled against an example implementation of what eventually became shared_timed_mutex in C++14 (with minor modifications to your example). The code below calls it shared_mutex which is the name it had when it was proposed.

I got the following outputs (all with the same executable):

Sometimes:

Worked finished her work
Worked finished her work
Grabbed exclusive lock, killing system
KILLING ALL WORK
Workers are on strike.  This worker refuses to work
Workers are on strike.  This worker refuses to work

And sometimes:

Worked finished her work
Grabbed exclusive lock, killing system
KILLING ALL WORK
Workers are on strike.  This worker refuses to work
Workers are on strike.  This worker refuses to work
Workers are on strike.  This worker refuses to work

And sometimes:

Worked finished her work
Worked finished her work
Worked finished her work
Worked finished her work
Grabbed exclusive lock, killing system
KILLING ALL WORK

I believe it should be theoretically possible to also obtain other outputs, though I did not confirm that experimentally.

In the interest of full disclosure, here is the exact code I executed:

#include "../mutexes/shared_mutex"
#include <thread>
#include <chrono>
#include <iostream>

using namespace std;
using namespace ting;

mutex outLock;
shared_mutex workerAccess;
bool shouldIWork = true;

class WorkerKiller
{
public:   

    void operator()()  
    {
        unique_lock<shared_mutex> lock(workerAccess); 

        cout << "Grabbed exclusive lock, killing system" << endl;
        this_thread::sleep_for(chrono::seconds(2));
        shouldIWork = false;
        cout << "KILLING ALL WORK" << endl;  
    }  

private:  
};

class Worker
{  
public:   

    Worker()
    {  
    }  

    void operator()()  
    {
        shared_lock<shared_mutex> lock(workerAccess); 

        if (!shouldIWork) {
            lock_guard<mutex> _(outLock);
            cout << "Workers are on strike.  This worker refuses to work" << endl;
        } else {
            this_thread::sleep_for(chrono::seconds(1));

            lock_guard<mutex> _(outLock);
            cout << "Worked finished her work" << endl;
        }
    }  
};  

int main()  
{  
    Worker w1;
    Worker w2;
    Worker w3;
    Worker w4;
    WorkerKiller wk;

    thread workerThread1(w1);
    thread workerThread2(w2);

    thread workerKillerThread(wk);

    thread workerThread3(w3);
    thread workerThread4(w4);

    workerThread1.join();
    workerThread2.join();
    workerKillerThread.join();
    workerThread3.join();
    workerThread4.join();

    return 0;
}
like image 65
Howard Hinnant Avatar answered Oct 17 '22 05:10

Howard Hinnant


A google search of "boost shared lock starvation" turned up this link:

  • Example for boost shared_mutex (multiple reads/one write)?

It looks like "upgrade" might be the key. See also:

  • Example of how to use boost upgradeable mutexes

  • http://HowardHinnant.github.io/shared_mutex

like image 1
paulsm4 Avatar answered Oct 17 '22 05:10

paulsm4