简体   繁体   中英

How to implement lock-free counter with std::atomic?

In my program multiple threads (checkers) requests webpages and if these pages contain some data, another threads (consumers) process the data. I need only predefined count of consumers to start processing (not all). I try to use std::atomic counter and fetch_add to limit working consumers count. But although the counter stay in bounds, consumers get identical counter values and real processing consumers count exceed the limit. Behavior depend on processing duration. Simplified code contains sleep_for instead getting page and processing page functions.

#include <iostream>
#include <thread>
#include <atomic>
#include <chrono>

class cConsumer
{
public:

    cConsumer::cConsumer(
        const size_t aNumber,
        std::atomic<bool> &aFire,
        std::atomic<size_t> &aCounter) :
        mNumber(aNumber),
        mFire(aFire),
        mCounter(aCounter){}

    void cConsumer::operator ()()
    {
        while (true)
        {
            while (!mFire.load()) std::this_thread::sleep_for(mMillisecond);

            size_t vCounter = mCounter.fetch_add(1);
            if (vCounter < 5)
            {
                std::cout << "      FIRE! consumer " << mNumber << ", counter " << vCounter << "\n";
                std::this_thread::sleep_for(mWorkDuration);
            }
            if (vCounter == 5)
            {
                mFire.store(false);
                mCounter.store(0);
            }
        }
    }

private:

    static const std::chrono::milliseconds 
        mMillisecond,
        mWorkDuration;

    const size_t mNumber;

    std::atomic<bool> &mFire;
    std::atomic<size_t> &mCounter;
};

const std::chrono::milliseconds 
    cConsumer::mMillisecond(1),
    cConsumer::mWorkDuration(1300);

class cChecker
{
public:

    cChecker(
        const size_t aNumber,
        std::atomic<bool> &aFire) :
        mNumber(aNumber),
        mFire(aFire),
        mStep(1){ }

    void cChecker::operator ()()
    {
        while (true)
        {
            while (mFire.load()) std::this_thread::sleep_for(mMillisecond);

            std::cout << "checker " << mNumber << " step " << mStep << "\n";
            std::this_thread::sleep_for(mCheckDuration);
            if (mStep % 20 == 1) mFire.store(true);         
            mStep++;
        }
    }

private:

    static const std::chrono::milliseconds 
        mMillisecond,
        mCheckDuration;

    const size_t mNumber;

    size_t mStep;

    std::atomic<bool> &mFire;
};

const std::chrono::milliseconds 
    cChecker::mMillisecond(1),
    cChecker::mCheckDuration(500);

void main()
{
    std::atomic<bool> vFire(false);
    std::atomic<size_t> vCounter(0);

    std::thread vConsumerThreads[16];

    for (size_t i = 0; i < 16; i++)
    {
        std::thread vConsumerThread((cConsumer(i, vFire, vCounter)));
        vConsumerThreads[i] = std::move(vConsumerThread);       
    }

    std::chrono::milliseconds vNextCheckerDelay(239);

    std::thread vCheckerThreads[3];

    for (size_t i = 0; i < 3; i++)
    {
        std::thread vCheckerThread((cChecker(i, vFire)));
        vCheckerThreads[i] = std::move(vCheckerThread);
        std::this_thread::sleep_for(vNextCheckerDelay);
    }

    for (size_t i = 0; i < 16; i++) vConsumerThreads[i].join();

    for (size_t i = 0; i < 3; i++) vCheckerThreads[i].join();
}

Output example (partial)

...
checker 1 step 19
checker 0 step 20
checker 2 step 19
checker 1 step 20
checker 0 step 21
checker 2 step 20
checker 1 step 21
      FIRE! consumer 10, counter 0
      FIRE! consumer 13, counter 4
      FIRE! consumer 6, counter 1
      FIRE! consumer 0, counter 2
      FIRE! consumer 2, counter 3
checker 0 step 22
checker 2 step 21
      FIRE! consumer 5, counter 3
      FIRE! consumer 7, counter 4
      FIRE! consumer 4, counter 1
      FIRE! consumer 15, counter 2
      FIRE! consumer 8, counter 0
checker 1 step 22
      FIRE! consumer 9, counter 0
      FIRE! consumer 11, counter 1
      FIRE! consumer 3, counter 2
      FIRE! consumer 14, counter 3
      FIRE! consumer 1, counter 4
checker 0 step 23
checker 2 step 22
checker 1 step 23
checker 2 step 23
checker 0 step 24
checker 1 step 24

I found one solution that is working but is not elegant: wait for all consumers to try work and to understand that fire is off.

#include <iostream>
#include <thread>
#include <atomic>
#include <chrono>

class cConsumer
{
public:

    cConsumer::cConsumer(
        const size_t aNumber,
        const size_t aConsumerCount,
        std::atomic<bool> &aFire,
        std::atomic<size_t> &aCounter) :
        mNumber(aNumber),
        mConsumerCount(aConsumerCount),
        mFire(aFire),
        mCounter(aCounter){}

    void cConsumer::operator ()()
    {
        while (true)
        {
            while (!mFire.load()) std::this_thread::sleep_for(mMillisecond);

            const size_t vCounter = mCounter.fetch_add(1);

            if (vCounter < 5)
            {
                std::cout << "      FIRE! consumer " << mNumber << ", counter " << vCounter << "\n";
                std::this_thread::sleep_for(mWorkDuration); //stub for process function
            }

            if (vCounter >= 5)
            {
                std::this_thread::sleep_for(mWorkDuration); //wait for other threads to increase counter
                std::this_thread::sleep_for(mWorkDuration); //double wait for long processing
                mFire.store(false);
            }

            if (vCounter == mConsumerCount)
            {               
                mCounter.store(0);
            }
        }
    }

private:

    static const std::chrono::milliseconds 
        mMillisecond,
        mWorkDuration;

    const size_t 
        mNumber,
        mConsumerCount;

    std::atomic<bool> &mFire;
    std::atomic<size_t> &mCounter;
};

const std::chrono::milliseconds 
    cConsumer::mMillisecond(1),
    cConsumer::mWorkDuration(1300);

class cChecker
{
public:

    cChecker(
        const size_t aNumber,
        std::atomic<bool> &aFire) :
        mNumber(aNumber),
        mFire(aFire),
        mStep(1){ }

    void cChecker::operator ()()
    {
        while (true)
        {
            while (mFire.load()) std::this_thread::sleep_for(mMillisecond);

            std::cout << "checker " << mNumber << " step " << mStep << "\n";
            std::this_thread::sleep_for(mCheckDuration);
            if (mStep % 20 == 1) mFire.store(true);         
            mStep++;
        }
    }

private:

    static const std::chrono::milliseconds 
        mMillisecond,
        mCheckDuration;

    const size_t mNumber;

    size_t mStep;

    std::atomic<bool> &mFire;
};

const std::chrono::milliseconds 
    cChecker::mMillisecond(1),
    cChecker::mCheckDuration(500);

void main()
{
    std::atomic<bool> vFire(false);
    std::atomic<size_t> vCouter(0);

    std::thread vConsumerThreads[16];

    for (size_t i = 0; i < 16; i++)
    {
        vConsumerThreads[i] = std::move(std::thread(cConsumer(i, 16, vFire, vCouter)));
    }

    std::chrono::milliseconds vNextCheckerDelay(239);

    std::thread vCheckerThreads[3];

    for (size_t i = 0; i < 3; i++)
    {
        vCheckerThreads[i] = std::move(std::thread(cChecker(i, vFire)));
        std::this_thread::sleep_for(vNextCheckerDelay);
    }

    for (size_t i = 0; i < 16; i++) vConsumerThreads[i].join();

    for (size_t i = 0; i < 3; i++) vCheckerThreads[i].join();

I think that a better solution exists.

What happens here ?

With a little luck, once you set fire, there could be many more worker than 5 passing this line:

    while(!mFire.load()) std::this_thread::sleep_for(mMillisecond);

Suppose there are 10 workers awake, and that counter was 0. Every 10 workers will then execute this:

    size_t vCounter = mCouter.fetch_add(1);

And every of the 10 workers has now a different counter between 1 and 11. The 5 first will execute the if clause:

        if(vCounter < 5)

Any thread having a higher counter will continue. Among them the 6th thread, that will reset the fire and reset the counter:

        if(vCounter == 5)
        {
            mFire.store(false);
            mCouter.store(0);
            cout << "RESET!!!!!! by consume "<<mNumber << endl; // useful to understand
        }

All these iddle threads will then continue to loop waiting for the next fire.

But now the bad things can happen, because you have some workers still working, and you have a bunch of checkers waiting to set fire again:

while(mFire.load()) std::this_thread::sleep_for(mMillisecond);
...   // now that fire is reset, they will go on

and some could reach the following line:

        if(mStep % 20 == 1) {
            mFire.store(true); 
            cout << "SET FIRE" << endl;   // to make the problem visual
        }

As the atomic counter is 0, you'll immediately have 5 new workers that will start a new job in addition to the ones still running.

What can you do about it ?

It's not fully clear to me what you intend to do:

  • do you want to have 5 workers active for each new fire ? In this case, it's ok as you did. The total number of workers could then exceed 5 in total.
  • do you want to have max 5 workers active at any moment in time ? In this case you should never reset the number of workers to 0 as you did, but you should decrement the counter for all the threads that have incremented it. Thus conter will contain the number of threads that are currently in the fire processing section:

     while(true) { while(!mFire.load()) std::this_thread::sleep_for(mMillisecond); size_t vCounter = mCouter.fetch_add(1); // FIRE PROCESSING: INCREMENT COUNTER if(vCounter < 5) { std::cout << " FIRE! consumer " << mNumber << ", counter " << vCounter << "\\n"; std::this_thread::sleep_for(mWorkDuration); std::cout << " finished consumer "<< mNumber<<endl; } if(vCounter == 5) { mFire.store(false); //mCouter.store(0); cout << "RESET!!!!!! by consumer "<<mNumber << endl; } mCouter.fetch_sub(1); // END OF PROCESSING: DECREMENT COUNTER 

Possible solution is using an auxiliary array for consumers done flags. When a consumer finish processing it store true to its done array cell. An additional control thread scans the done array for all cells being true and resets the program state.

#include <iostream>
#include <thread>
#include <atomic>
#include <chrono>

class cConsumer
{
public:

    cConsumer::cConsumer(
        const size_t aNumber,
        const size_t aFiresLimit,
        std::atomic<bool> &aFire,
        std::atomic<bool> &aDone,
        std::atomic<size_t> &aCounter) :
        mNumber(aNumber),
        mFiresLimit(aFiresLimit),
        mFire(aFire),
        mDone(aDone),
        mCounter(aCounter){}

    void cConsumer::operator ()()
    {
        while (true)
        {
            while (!mFire.load()) std::this_thread::sleep_for(mMillisecond);

            const size_t vCounter = mCounter.fetch_add(1);

            if (vCounter < mFiresLimit)
            {
                std::cout << "      FIRE! consumer " << mNumber << ", counter " << vCounter << "\n";
                std::this_thread::sleep_for(mWorkDuration); // instead real processing
            }   

            mDone.store(true);

            while (mDone.load()) std::this_thread::sleep_for(mMillisecond);
        }
    }

private:

    static const std::chrono::milliseconds 
        mMillisecond,
        mWorkDuration;

    const size_t 
        mNumber,
        mFiresLimit;

    std::atomic<bool> 
        &mFire,
        &mDone;

    std::atomic<size_t> &mCounter;
};

const std::chrono::milliseconds 
    cConsumer::mMillisecond(1),
    cConsumer::mWorkDuration(1300);

class cChecker
{
public:

    cChecker(
        const size_t aNumber,
        std::atomic<bool> &aFire) :
        mNumber(aNumber),
        mFire(aFire),
        mStep(1){ }

    void cChecker::operator ()()
    {
        while (true)
        {
            while (mFire.load()) std::this_thread::sleep_for(mMillisecond);

            std::cout << "checker " << mNumber << " step " << mStep << "\n";
            std::this_thread::sleep_for(mCheckDuration);
            if (mStep % 20 == 1) // dummy condition instead real checker function
            {
                mFire.store(true);
            }
            mStep++;
        }
    }

private:

    static const std::chrono::milliseconds 
        mMillisecond,
        mCheckDuration;

    const size_t mNumber;

    size_t mStep;

    std::atomic<bool> &mFire;
};

const std::chrono::milliseconds 
    cChecker::mMillisecond(1),
    cChecker::mCheckDuration(500);

class cController
{
public:

    cController(
        const size_t aConsumerCount,
        std::atomic<bool> &aFire,
        std::atomic<bool> * const aConsumersDone,
        std::atomic<size_t> &aCounter) :
        mConsumerCount(aConsumerCount),
        mFire(aFire),
        mConsumersDone(aConsumersDone),
        mCounter(aCounter){}

    void cController::operator ()()
    {
        while (true)
        {       
            while(!mFire.load()) std::this_thread::sleep_for(mMillisecond);

            bool vAllConsumersDone = false;

            while (!vAllConsumersDone)
            {
                size_t i = 0;
                while ((i < mConsumerCount) && (mConsumersDone[i].load())) i++;
                vAllConsumersDone = (i == mConsumerCount);
                std::this_thread::sleep_for(mMillisecond);
            }

            mFire.store(false);
            for (size_t i = 0; i < mConsumerCount; i++) mConsumersDone[i].store(false);
            mCounter.store(0);
        }
    }

private:

    const size_t mConsumerCount;

    static const std::chrono::milliseconds mMillisecond;

    std::atomic<bool> 
        &mFire,
        * const mConsumersDone;

    std::atomic<size_t> &mCounter;
};

const std::chrono::milliseconds cController::mMillisecond(1);

void main()
{
    static const size_t 
        vCheckerCount = 3,
        vConsumersCount = 16,
        vFiresLimit = 5;

    std::atomic<bool> vFire(false);

    std::atomic<bool> vConsumersDone[vConsumersCount];
    for (size_t i = 0; i < vConsumersCount; i++) vConsumersDone[i].store(false);

    std::atomic<size_t> vCounter(0);    

    std::thread vControllerThread(cController(vConsumersCount, vFire, vConsumersDone, vCounter));

    std::thread vConsumerThreads[vConsumersCount];

    for (size_t i = 0; i < vConsumersCount; i++)
    {
        vConsumerThreads[i] = std::move(std::thread(cConsumer(i, vFiresLimit, vFire, vConsumersDone[i], vCounter)));
    }

    std::chrono::milliseconds vNextCheckerDelay(239);

    std::thread vCheckerThreads[vCheckerCount];

    for (size_t i = 0; i < vCheckerCount; i++)
    {
        vCheckerThreads[i] = std::move(std::thread(cChecker(i, vFire)));
        std::this_thread::sleep_for(vNextCheckerDelay);
    }

    for (size_t i = 0; i < vConsumersCount; i++) vConsumerThreads[i].join();

    for (size_t i = 0; i < vCheckerCount; i++) vCheckerThreads[i].join();

    vControllerThread.join();
}

Output (partial) example:

...
checker 2 step 19
checker 1 step 19
checker 0 step 19
checker 2 step 20
checker 0 step 20
checker 1 step 20
checker 2 step 21
checker 0 step 21
checker 1 step 21
      FIRE! consumer 11, counter 0
      FIRE! consumer 3, counter 2
      FIRE! consumer 4, counter 3
      FIRE! consumer 10, counter 4
      FIRE! consumer 14, counter 1
checker 0 step 22
checker 2 step 22
checker 1 step 22
checker 2 step 23
checker 0 step 23
checker 1 step 23
checker 2 step 24
checker 0 step 24

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM