Thread scheduling when releasing lock











up vote
2
down vote

favorite












My expectation when unlocking a mutex was that the scheduler checks for other threads currently trying to lock this mutex and then execute one of those waiting threads. I wrote a test program (see code below) with 2 threads both trying to acquire the same mutex in a loop and do some work (sleep for 1ms). The difference is that one thread t1 waits a short while between unlocking and trying to reacquire the mutex and the other thread t2 doesn’t. I was expecting that both threads acquire the mutex about the same number of times. However, on windows t1 often acquires the mutex only a single time and the other thread hundreds of times. On linux the behavior is different and both threads get work done with t2 roughly twice as many. Why does t1 on windows almost never acquire the mutex? How do I have to modify the code so that it does?



Example code:



#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>

using namespace std;

int main()
{
mutex m;
atomic<bool> go(false);
int t1Counter = 0, t2Counter = 0;

thread t1([&] {
while(!go);
while(go) {
this_thread::sleep_for(100us);
lock_guard<mutex> lg(m);
this_thread::sleep_for(1ms);
++t1Counter;
}
});
thread t2([&] {
while(!go);
while(go) {
lock_guard<mutex> lg(m);
this_thread::sleep_for(1ms);
++t2Counter;
}
});

go = true;
this_thread::sleep_for(1s);
go = false;
t1.join();
t2.join();

cout << t1Counter << " " << t2Counter << endl;
}









share|improve this question
























  • The search terms you are looking for are "fairness" and "starvation". Windows mutex doesn't guarantee fairness. I'm not familiar with Linux; looks like its implementation is more fair.
    – Igor Tandetnik
    Nov 22 at 16:28












  • Calling std::this_thread::yield between unlock and relock might change the picture somewhat.
    – Igor Tandetnik
    Nov 22 at 16:34










  • @IgorTandetnik Doesn't really change much. Thread t1 runs about ~5 times instead of only once, but t2 still runs hundred of times.
    – sebi707
    Nov 22 at 17:26






  • 1




    Accidental synchronization problem, those sleep_for() calls are not innocent. You can tell something is amiss from the t2Counter value. If t2 runs (nearly) unimpeded then the value should be close to a 1000. My crystal ball says that you see something closer to 60 or 95. On Windows, a sleep can only complete at the clock tick interrupt, it ticks 64 times per second by default.
    – Hans Passant
    Nov 22 at 18:04










  • @HansPassant You are right. Replacing the sleep_for with some busy waiting sleep does give much more sensible results.
    – sebi707
    Nov 22 at 19:26















up vote
2
down vote

favorite












My expectation when unlocking a mutex was that the scheduler checks for other threads currently trying to lock this mutex and then execute one of those waiting threads. I wrote a test program (see code below) with 2 threads both trying to acquire the same mutex in a loop and do some work (sleep for 1ms). The difference is that one thread t1 waits a short while between unlocking and trying to reacquire the mutex and the other thread t2 doesn’t. I was expecting that both threads acquire the mutex about the same number of times. However, on windows t1 often acquires the mutex only a single time and the other thread hundreds of times. On linux the behavior is different and both threads get work done with t2 roughly twice as many. Why does t1 on windows almost never acquire the mutex? How do I have to modify the code so that it does?



Example code:



#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>

using namespace std;

int main()
{
mutex m;
atomic<bool> go(false);
int t1Counter = 0, t2Counter = 0;

thread t1([&] {
while(!go);
while(go) {
this_thread::sleep_for(100us);
lock_guard<mutex> lg(m);
this_thread::sleep_for(1ms);
++t1Counter;
}
});
thread t2([&] {
while(!go);
while(go) {
lock_guard<mutex> lg(m);
this_thread::sleep_for(1ms);
++t2Counter;
}
});

go = true;
this_thread::sleep_for(1s);
go = false;
t1.join();
t2.join();

cout << t1Counter << " " << t2Counter << endl;
}









share|improve this question
























  • The search terms you are looking for are "fairness" and "starvation". Windows mutex doesn't guarantee fairness. I'm not familiar with Linux; looks like its implementation is more fair.
    – Igor Tandetnik
    Nov 22 at 16:28












  • Calling std::this_thread::yield between unlock and relock might change the picture somewhat.
    – Igor Tandetnik
    Nov 22 at 16:34










  • @IgorTandetnik Doesn't really change much. Thread t1 runs about ~5 times instead of only once, but t2 still runs hundred of times.
    – sebi707
    Nov 22 at 17:26






  • 1




    Accidental synchronization problem, those sleep_for() calls are not innocent. You can tell something is amiss from the t2Counter value. If t2 runs (nearly) unimpeded then the value should be close to a 1000. My crystal ball says that you see something closer to 60 or 95. On Windows, a sleep can only complete at the clock tick interrupt, it ticks 64 times per second by default.
    – Hans Passant
    Nov 22 at 18:04










  • @HansPassant You are right. Replacing the sleep_for with some busy waiting sleep does give much more sensible results.
    – sebi707
    Nov 22 at 19:26













up vote
2
down vote

favorite









up vote
2
down vote

favorite











My expectation when unlocking a mutex was that the scheduler checks for other threads currently trying to lock this mutex and then execute one of those waiting threads. I wrote a test program (see code below) with 2 threads both trying to acquire the same mutex in a loop and do some work (sleep for 1ms). The difference is that one thread t1 waits a short while between unlocking and trying to reacquire the mutex and the other thread t2 doesn’t. I was expecting that both threads acquire the mutex about the same number of times. However, on windows t1 often acquires the mutex only a single time and the other thread hundreds of times. On linux the behavior is different and both threads get work done with t2 roughly twice as many. Why does t1 on windows almost never acquire the mutex? How do I have to modify the code so that it does?



Example code:



#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>

using namespace std;

int main()
{
mutex m;
atomic<bool> go(false);
int t1Counter = 0, t2Counter = 0;

thread t1([&] {
while(!go);
while(go) {
this_thread::sleep_for(100us);
lock_guard<mutex> lg(m);
this_thread::sleep_for(1ms);
++t1Counter;
}
});
thread t2([&] {
while(!go);
while(go) {
lock_guard<mutex> lg(m);
this_thread::sleep_for(1ms);
++t2Counter;
}
});

go = true;
this_thread::sleep_for(1s);
go = false;
t1.join();
t2.join();

cout << t1Counter << " " << t2Counter << endl;
}









share|improve this question















My expectation when unlocking a mutex was that the scheduler checks for other threads currently trying to lock this mutex and then execute one of those waiting threads. I wrote a test program (see code below) with 2 threads both trying to acquire the same mutex in a loop and do some work (sleep for 1ms). The difference is that one thread t1 waits a short while between unlocking and trying to reacquire the mutex and the other thread t2 doesn’t. I was expecting that both threads acquire the mutex about the same number of times. However, on windows t1 often acquires the mutex only a single time and the other thread hundreds of times. On linux the behavior is different and both threads get work done with t2 roughly twice as many. Why does t1 on windows almost never acquire the mutex? How do I have to modify the code so that it does?



Example code:



#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>

using namespace std;

int main()
{
mutex m;
atomic<bool> go(false);
int t1Counter = 0, t2Counter = 0;

thread t1([&] {
while(!go);
while(go) {
this_thread::sleep_for(100us);
lock_guard<mutex> lg(m);
this_thread::sleep_for(1ms);
++t1Counter;
}
});
thread t2([&] {
while(!go);
while(go) {
lock_guard<mutex> lg(m);
this_thread::sleep_for(1ms);
++t2Counter;
}
});

go = true;
this_thread::sleep_for(1s);
go = false;
t1.join();
t2.join();

cout << t1Counter << " " << t2Counter << endl;
}






c++ multithreading mutex






share|improve this question















share|improve this question













share|improve this question




share|improve this question








edited Nov 22 at 17:41









mrflash818

6441018




6441018










asked Nov 22 at 16:14









sebi707

605




605












  • The search terms you are looking for are "fairness" and "starvation". Windows mutex doesn't guarantee fairness. I'm not familiar with Linux; looks like its implementation is more fair.
    – Igor Tandetnik
    Nov 22 at 16:28












  • Calling std::this_thread::yield between unlock and relock might change the picture somewhat.
    – Igor Tandetnik
    Nov 22 at 16:34










  • @IgorTandetnik Doesn't really change much. Thread t1 runs about ~5 times instead of only once, but t2 still runs hundred of times.
    – sebi707
    Nov 22 at 17:26






  • 1




    Accidental synchronization problem, those sleep_for() calls are not innocent. You can tell something is amiss from the t2Counter value. If t2 runs (nearly) unimpeded then the value should be close to a 1000. My crystal ball says that you see something closer to 60 or 95. On Windows, a sleep can only complete at the clock tick interrupt, it ticks 64 times per second by default.
    – Hans Passant
    Nov 22 at 18:04










  • @HansPassant You are right. Replacing the sleep_for with some busy waiting sleep does give much more sensible results.
    – sebi707
    Nov 22 at 19:26


















  • The search terms you are looking for are "fairness" and "starvation". Windows mutex doesn't guarantee fairness. I'm not familiar with Linux; looks like its implementation is more fair.
    – Igor Tandetnik
    Nov 22 at 16:28












  • Calling std::this_thread::yield between unlock and relock might change the picture somewhat.
    – Igor Tandetnik
    Nov 22 at 16:34










  • @IgorTandetnik Doesn't really change much. Thread t1 runs about ~5 times instead of only once, but t2 still runs hundred of times.
    – sebi707
    Nov 22 at 17:26






  • 1




    Accidental synchronization problem, those sleep_for() calls are not innocent. You can tell something is amiss from the t2Counter value. If t2 runs (nearly) unimpeded then the value should be close to a 1000. My crystal ball says that you see something closer to 60 or 95. On Windows, a sleep can only complete at the clock tick interrupt, it ticks 64 times per second by default.
    – Hans Passant
    Nov 22 at 18:04










  • @HansPassant You are right. Replacing the sleep_for with some busy waiting sleep does give much more sensible results.
    – sebi707
    Nov 22 at 19:26
















The search terms you are looking for are "fairness" and "starvation". Windows mutex doesn't guarantee fairness. I'm not familiar with Linux; looks like its implementation is more fair.
– Igor Tandetnik
Nov 22 at 16:28






The search terms you are looking for are "fairness" and "starvation". Windows mutex doesn't guarantee fairness. I'm not familiar with Linux; looks like its implementation is more fair.
– Igor Tandetnik
Nov 22 at 16:28














Calling std::this_thread::yield between unlock and relock might change the picture somewhat.
– Igor Tandetnik
Nov 22 at 16:34




Calling std::this_thread::yield between unlock and relock might change the picture somewhat.
– Igor Tandetnik
Nov 22 at 16:34












@IgorTandetnik Doesn't really change much. Thread t1 runs about ~5 times instead of only once, but t2 still runs hundred of times.
– sebi707
Nov 22 at 17:26




@IgorTandetnik Doesn't really change much. Thread t1 runs about ~5 times instead of only once, but t2 still runs hundred of times.
– sebi707
Nov 22 at 17:26




1




1




Accidental synchronization problem, those sleep_for() calls are not innocent. You can tell something is amiss from the t2Counter value. If t2 runs (nearly) unimpeded then the value should be close to a 1000. My crystal ball says that you see something closer to 60 or 95. On Windows, a sleep can only complete at the clock tick interrupt, it ticks 64 times per second by default.
– Hans Passant
Nov 22 at 18:04




Accidental synchronization problem, those sleep_for() calls are not innocent. You can tell something is amiss from the t2Counter value. If t2 runs (nearly) unimpeded then the value should be close to a 1000. My crystal ball says that you see something closer to 60 or 95. On Windows, a sleep can only complete at the clock tick interrupt, it ticks 64 times per second by default.
– Hans Passant
Nov 22 at 18:04












@HansPassant You are right. Replacing the sleep_for with some busy waiting sleep does give much more sensible results.
– sebi707
Nov 22 at 19:26




@HansPassant You are right. Replacing the sleep_for with some busy waiting sleep does give much more sensible results.
– sebi707
Nov 22 at 19:26












1 Answer
1






active

oldest

votes

















up vote
3
down vote



accepted










On Windows, std::mutex is implemented using a slim reader/writer lock. This lock implementation is not fair (meaning that it provides no guarantees about the order in which waiting threads will acquire the lock). Windows moved away from fair locks some time ago because Microsoft deemed lock convoys a more serious problem than thread starvation.



You can read more about slim reader/writer locks on Microsoft docs: Slim Reader/Writer (SRW) Locks



Joe Duffy has also blogged about the fairness versus lock convoy issue: Anti-convoy locks in Windows Server 2003 SP1 and Windows Vista






share|improve this answer























    Your Answer






    StackExchange.ifUsing("editor", function () {
    StackExchange.using("externalEditor", function () {
    StackExchange.using("snippets", function () {
    StackExchange.snippets.init();
    });
    });
    }, "code-snippets");

    StackExchange.ready(function() {
    var channelOptions = {
    tags: "".split(" "),
    id: "1"
    };
    initTagRenderer("".split(" "), "".split(" "), channelOptions);

    StackExchange.using("externalEditor", function() {
    // Have to fire editor after snippets, if snippets enabled
    if (StackExchange.settings.snippets.snippetsEnabled) {
    StackExchange.using("snippets", function() {
    createEditor();
    });
    }
    else {
    createEditor();
    }
    });

    function createEditor() {
    StackExchange.prepareEditor({
    heartbeatType: 'answer',
    convertImagesToLinks: true,
    noModals: true,
    showLowRepImageUploadWarning: true,
    reputationToPostImages: 10,
    bindNavPrevention: true,
    postfix: "",
    imageUploader: {
    brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
    contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
    allowUrls: true
    },
    onDemand: true,
    discardSelector: ".discard-answer"
    ,immediatelyShowMarkdownHelp:true
    });


    }
    });














    draft saved

    draft discarded


















    StackExchange.ready(
    function () {
    StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53434834%2fthread-scheduling-when-releasing-lock%23new-answer', 'question_page');
    }
    );

    Post as a guest















    Required, but never shown

























    1 Answer
    1






    active

    oldest

    votes








    1 Answer
    1






    active

    oldest

    votes









    active

    oldest

    votes






    active

    oldest

    votes








    up vote
    3
    down vote



    accepted










    On Windows, std::mutex is implemented using a slim reader/writer lock. This lock implementation is not fair (meaning that it provides no guarantees about the order in which waiting threads will acquire the lock). Windows moved away from fair locks some time ago because Microsoft deemed lock convoys a more serious problem than thread starvation.



    You can read more about slim reader/writer locks on Microsoft docs: Slim Reader/Writer (SRW) Locks



    Joe Duffy has also blogged about the fairness versus lock convoy issue: Anti-convoy locks in Windows Server 2003 SP1 and Windows Vista






    share|improve this answer



























      up vote
      3
      down vote



      accepted










      On Windows, std::mutex is implemented using a slim reader/writer lock. This lock implementation is not fair (meaning that it provides no guarantees about the order in which waiting threads will acquire the lock). Windows moved away from fair locks some time ago because Microsoft deemed lock convoys a more serious problem than thread starvation.



      You can read more about slim reader/writer locks on Microsoft docs: Slim Reader/Writer (SRW) Locks



      Joe Duffy has also blogged about the fairness versus lock convoy issue: Anti-convoy locks in Windows Server 2003 SP1 and Windows Vista






      share|improve this answer

























        up vote
        3
        down vote



        accepted







        up vote
        3
        down vote



        accepted






        On Windows, std::mutex is implemented using a slim reader/writer lock. This lock implementation is not fair (meaning that it provides no guarantees about the order in which waiting threads will acquire the lock). Windows moved away from fair locks some time ago because Microsoft deemed lock convoys a more serious problem than thread starvation.



        You can read more about slim reader/writer locks on Microsoft docs: Slim Reader/Writer (SRW) Locks



        Joe Duffy has also blogged about the fairness versus lock convoy issue: Anti-convoy locks in Windows Server 2003 SP1 and Windows Vista






        share|improve this answer














        On Windows, std::mutex is implemented using a slim reader/writer lock. This lock implementation is not fair (meaning that it provides no guarantees about the order in which waiting threads will acquire the lock). Windows moved away from fair locks some time ago because Microsoft deemed lock convoys a more serious problem than thread starvation.



        You can read more about slim reader/writer locks on Microsoft docs: Slim Reader/Writer (SRW) Locks



        Joe Duffy has also blogged about the fairness versus lock convoy issue: Anti-convoy locks in Windows Server 2003 SP1 and Windows Vista







        share|improve this answer














        share|improve this answer



        share|improve this answer








        edited Nov 22 at 19:32

























        answered Nov 22 at 16:37









        Peter Ruderman

        10.1k2352




        10.1k2352






























            draft saved

            draft discarded




















































            Thanks for contributing an answer to Stack Overflow!


            • Please be sure to answer the question. Provide details and share your research!

            But avoid



            • Asking for help, clarification, or responding to other answers.

            • Making statements based on opinion; back them up with references or personal experience.


            To learn more, see our tips on writing great answers.





            Some of your past answers have not been well-received, and you're in danger of being blocked from answering.


            Please pay close attention to the following guidance:


            • Please be sure to answer the question. Provide details and share your research!

            But avoid



            • Asking for help, clarification, or responding to other answers.

            • Making statements based on opinion; back them up with references or personal experience.


            To learn more, see our tips on writing great answers.




            draft saved


            draft discarded














            StackExchange.ready(
            function () {
            StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53434834%2fthread-scheduling-when-releasing-lock%23new-answer', 'question_page');
            }
            );

            Post as a guest















            Required, but never shown





















































            Required, but never shown














            Required, but never shown












            Required, but never shown







            Required, but never shown

































            Required, but never shown














            Required, but never shown












            Required, but never shown







            Required, but never shown







            Popular posts from this blog

            What visual should I use to simply compare current year value vs last year in Power BI desktop

            How to ignore python UserWarning in pytest?

            Alexandru Averescu