0

I was working on a small combat-code for fun trying to learn to use threads in c++ but stumpled upon a bug which i can't figure out.

I use a mutext for synchronisation:

mutex mux1;

and a threaded function:

void dueler(Player &player, Player &enemy)
{
     do
     {
          Sleep(player.get_as());
          mux1.lock();
          cout << player.get_name()<< " hit " << enemy.get_name() << " for " << player.get_str() << " damage!" << endl;
          enemy.dmg(player.get_str());
          mux1.unlock();
     } while ((enemy.ask_dead() != true) && (player.ask_dead() != true));
 }

which I call in main (full code: http://pastebin.com/1FBf2FCQ):

int main()
{
    Player Player1("kasper", 100, 5, 200);
    Player Player2("Bagger", 150, 8, 3000);
    thread dueler1(dueler,Player1,Player2);
    thread dueler2(dueler, Player2, Player1);
    dueler1.join();
    dueler2.join();
    cout <<endl<< "battle is over!";
}

The threaded function makes to Players (http://pastebin.com/ZCTfUYiS) fight against each other:

class Player
{
public:
    Player(string name_, int maxhp_, int strenght_, int attackspeed_) {
        name = name_; 
        maxhp = maxhp_; 
        hp = maxhp;
        strenght = strenght_;
        attackspeed = attackspeed_;
        isdead = false;
    }
    void fullhp() {  hp = maxhp;  }
    void dmg(int dmg) {
        hp -= dmg;
        if (hp < 0) dead();
    }
    void dead() {
        isdead = true;
        cout << name <<" died like a pessant";
    }
    string get_name() { return name; }
    int get_hp()    { return hp; }
    int get_maxhp() { return maxhp; }
    int get_str()   { return strenght; }
    int get_as()    { return attackspeed; }
    bool ask_dead() {return isdead; }
private:
    string name;
    int maxhp;
    int hp;
    int strenght;
    int attackspeed;
    bool isdead;
};

The problem happens when Player2 gets killed:

  • thread 1 ( which handles the damage dealt by player 1) stops as is should,
  • thread 2 ( which handles Player2 who's dead) continues to run, and completly ignores the while statement until Player1 is dead.

I would have expected the while to end because of the condition (player.ask_dead() != true) should fail.

Any suggestions?

Christophe
  • 68,716
  • 7
  • 72
  • 138
kaspertorp
  • 188
  • 10
  • Welcome to Stack Overflow! Please provide a [minimal, complete, and verifiable example](http://stackoverflow.com/help/mcve). – Julian Mar 17 '15 at 17:50
  • Using threads to such tasks isn't a good idea. –  Mar 17 '15 at 17:54
  • What is "a small combat-code"? – Lightness Races in Orbit Mar 17 '15 at 18:02
  • You may need a few `volatile`s in here to avoid some of those conditions getting optimised out. – Lightness Races in Orbit Mar 17 '15 at 18:03
  • @Fireho: I dunno, it looks okay for this example. – Lightness Races in Orbit Mar 17 '15 at 18:04
  • @LightnessRacesinOrbit That's not a good idea. Any optimizations the compiler can make can also be made by other parts of the system, so preventing things from being optimized out is guaranteed to do anything. – David Schwartz Mar 17 '15 at 18:05
  • @DavidSchwartz: I don't follow you at all. What "other parts of the system"? I'm talking about the compiler deciding that the result of, say, `player.ask_dead()` can never change while that function executes. If all those functions get inlined, and there's no reason to think that they won't with this small program, I would absolutely expect that to happen. And `volatile` is literally made precisely for this purpose: to indicate that the object may be mutated externally and therefore such assumptions may not hold. – Lightness Races in Orbit Mar 17 '15 at 18:07
  • OP you misspelt "strength". – Lightness Races in Orbit Mar 17 '15 at 18:10
  • 1
    @LightnessRacesinOrbit The processor caches can make the same decision though as can the memory controller or any other component of the system. If `volatile` is necessary, it's not sufficient. No, `volatile` is not made precisely for that purpose, it's made for interrupts and hardware, not inter-thread synchronization. (Show me the standard that says what `volatile` does for inter-thread synchronization. It cannot be done.) – David Schwartz Mar 17 '15 at 18:21
  • @DavidSchwartz: When did I say that it was for inter-thread synchronization? You're completely misinterpreting my statements. Please read them again. – Lightness Races in Orbit Mar 17 '15 at 18:22
  • @LightnessRacesinOrbit His problem is inter-thread synchronization. If you're not saying `volatile` is useful for that purpose, why are you suggesting it?! – David Schwartz Mar 17 '15 at 18:23
  • @DavidSchwartz: You're confused. There is a large extent of optimisations that a compiler can apply to a sequence of statements that read from and write to variables within a single function. When those variables are also expected to be mutated externally (from another thread, by hardware, whatever), then you need to use `volatile` to indicate this so that the compiler does not eliminate what it perceives to be purposeless reads. This is not synchronisation: that is what the mutex is for. Indeed, he has no problem with synchronisation whatsoever as far as I can tell from the code. – Lightness Races in Orbit Mar 17 '15 at 18:24
  • 1
    @LightnessRacesinOrbit You're missing the point. Any optimizations the compiler can do can also be done by other parts of the system. So if you need `volatile` to stop them, it's not sufficient because they can also be done elsewhere. And you don't *need* to use `volatile` to indicate that because there are other mechanisms (such as mutexes, atomic operations, memory barriers and so on) that do what you actually do need. If it's neither necessary nor sufficient, why do it? – David Schwartz Mar 17 '15 at 18:26
  • Unfortunately, though, I cannot reproduce this [under GCC](http://coliru.stacked-crooked.com/a/31eef111db64b2f7) (only tried one version with one set of build flags) or VS2012 in the default release configuration. So I cannot show that `volatile` will actually be sufficient to resolve this problem. The OP needs to post an MCVE. – Lightness Races in Orbit Mar 17 '15 at 18:26
  • 1
    @LightnessRacesinOrbit You cannot possibly show that `volatile` is sufficient to solve the problem. To do that, you'd have to show some relevant standard that guarantees that volatile has inter-thread memory synchronization semantics, and no such standard exists. – David Schwartz Mar 17 '15 at 18:27
  • @David _This has nothing to do with synchronisation_. I won't say it again. – Lightness Races in Orbit Mar 17 '15 at 18:28
  • 1
    @LightnessRacesinOrbit I hope nobody listens to you because you are confidently spouting complete nonsense. – David Schwartz Mar 17 '15 at 18:28
  • 1
    @LightnessRacesinOrbit I said it was for hardware. You're saying it's for multithreading. Read [this](http://stackoverflow.com/a/4558031/721269) and [this](https://software.intel.com/en-us/blogs/2007/11/30/volatile-almost-useless-for-multi-threaded-programming). And stop confidently stating total nonsense. – David Schwartz Mar 17 '15 at 18:30
  • @David: Irrelevant links that do not contradict what I've been saying. The first one talks about `volatile` being useless for _synchronisation_, which is true, not under argument, and not related to what I'm saying. Try [this one](http://stackoverflow.com/a/2485177/560648): "For thread-safe accesses to shared data, we need a guarantee that: the read/write actually happens, _[..]_ volatile does guarantee the first point" That's the guarantee I'm talking about. That's it. Nothing more. Not synchronisation, not hardware, and certainly not "confident nonsense"! – Lightness Races in Orbit Mar 17 '15 at 18:52
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/73198/discussion-between-david-schwartz-and-lightness-races-in-orbit). – David Schwartz Mar 17 '15 at 18:56
  • @David: Everything you need is here: http://stackoverflow.com/q/29107888/560648 :) Note that the need to consider synchronisation is _already taken care of_ in the OP's code, by his mutex. – Lightness Races in Orbit Mar 17 '15 at 19:21

1 Answers1

2

You have done synchronization properly by putting a mutext to avoid concurrent access to your object. That's already a very good start !

The problem:

The problem is that when you create a thread, it makes a private copy of the arguments you pass (see also this related SO question)

You can easily verify experimentally the references by adding this first statment in dueller() :

cout << "dueller with player " << (void*)&player << " and ennemy " << (void*)&enemy << endl;

When dueler() is called with arguments by reference, the reference refers not to the original object (local variable in main()) but to its clone.

The solution:

You have to use std::ref() to make sure that all the threads really refer to the same objects:

thread dueler1(dueler, std::ref(Player1), std::ref(Player2));
thread dueler2(dueler, std::ref(Player2), std::ref(Player1));

Other remarks:

When a player dies, his dueler() function is most probably waiting for the mutex. So when the mutex becomes free, he performs damages, despite being dead.

Ok ! in a game you could argue that the player is dead but still running with this sword to the victim... But we can also fix it by adding a condition when entering in the protected area:

    mux1.lock();  // we were waiting, a certain time
    if (!player.ask_dead()) {  // so first check if we are still alive
        cout << player.get_name() << " hit " << enemy.get_name() << " for " << player.get_str() << " damage!" << endl;
        enemy.dmg(player.get_str());
    }
    mux1.unlock();
Community
  • 1
  • 1
Christophe
  • 68,716
  • 7
  • 72
  • 138
  • thank you! i got it to work! i though i already was doing this, but from reading this and testing, i found out that i didn't do it correctly, thanks man, saved my night! – kaspertorp Mar 17 '15 at 20:36