1

Here is a simplified observer pattern:

  1. one creator creates a profile when it starts and "destroy" it when it is done.
  2. zero, one or more observers try to "look at" the profile at any time.

To implement it, the trick is that observers shall refcnt profile, so the last observer (or creator) can safely destroy it.

I can do it without shared_ptr/weak_ptr, but I wonder if using them can avoid re-inventing wheels.

Here is my code:

#include <iostream>
#include <memory>
#include <thread>
#include <cassert>

volatile bool playing = true;

class Profile {
public:
    int a_;
    Profile(int v) {a_ = v;}
};

std::shared_ptr<Profile> g_profile{ nullptr };

void observer() {
    do {
        // observe profile if I can
        std::weak_ptr<Profile> weak = g_profile;
        if (auto prof = weak.lock()) {
            auto a = prof->a_;
            // if prof is stable, I shall see the same a_
            assert(a == prof->a_);
        }
        else {
            std::cout << ".";
        }
    } while (playing);
}

void creator() {
    do {
        // create profile when I start
        g_profile.reset(new Profile(std::rand()));
        std::weak_ptr<Profile> weak = g_profile;
        assert(weak.lock() != nullptr);
        
        // doing some work ...

        // destroy profile when I am done
        g_profile.reset();
    } while (playing);
}

void timer() {
    std::this_thread::sleep_for(std::chrono::seconds(10));
    playing = false;
}

int main() {
    std::thread cr{ creator };
    std::thread ob{ observer };
    std::thread tm{ timer };
    cr.join();ob.join();tm.join();

    // no memory leak
}

But the program crashes either at std::weak_ptr<Profile> weak = g_profile or assert(a == prof->a_). So here are my questions:

  1. do you have a pointer implementing observer pattern (or variant) with shared_ptr/weak_ptr?
  2. what's wrong with the above code? Can you make it right?
fatun
  • 23
  • 1
  • 5
  • You may be reinventing Boost [Signals2](https://www.boost.org/doc/libs/1_73_0/doc/html/signals2.html). (Signals1 was for C++98. Signals2 is for C++11.) – Eljay Jul 22 '20 at 01:14
  • @Eljay, I am not sure if boost signals is the one I am looking for. I am looking for shared_ptr/weak_ptr implementation of above usage. – fatun Jul 22 '20 at 18:52
  • Is that a correct check: `if (auto prof = weak.lock()) {` looks like you are assigning in the `if`. – Ilian Zapryanov Jul 22 '20 at 19:01
  • @Ilian Zapryanov, the check is a simplified form of ``if (nullptr != (...))`` – fatun Jul 22 '20 at 19:10
  • @IlianZapryanov • a variable can be declared in an `if`, [here](https://medium.com/@winwardo/what-if-declaring-variables-in-if-statements-and-the-curiosities-of-scope-that-follows-d22ec9d49d97) for a blog post about it. – Eljay Jul 22 '20 at 22:20
  • The assignment is always `true` you should `if ((auto prof = weak.lock()) != nullptr)) {...}` check the value after assignment. – Ilian Zapryanov Jul 23 '20 at 04:10

1 Answers1

0

You have undefined bahavior when one thread reads from the shared pointer g_profile (observer) while the other thread writes to it (when creator calls std::shared_ptr::reset)

If you want to use the shared_ptr from two threads you'll have to use a lock or atomic_shared_ptr.

Also volatile does not guarantee any synchronization as it does in java. See this answer.

Mike van Dyke
  • 2,724
  • 3
  • 16
  • 31
  • Thanks - looks like shared_ptr is not designed for read/write mix. ``volatile`` usage here shall be ok since it is served as a signal status among threads. – fatun Jul 23 '20 at 17:02
  • In that very particular case, volatile was sufficient as it's a cancel flag. Bad taste though. – curiousguy Jul 23 '20 at 18:02
  • @curiousguy, could you please share your solution for the cancel flag? – fatun Jul 23 '20 at 21:12
  • @curiousguy Are you sure on that? [cppref](https://en.cppreference.com/w/cpp/language/cv) says "that is, within a single thread of execution, volatile accesses cannot be optimized out or reordered with another visible side effect that is sequenced-before or sequenced-after the volatile access. This makes volatile objects suitable for communication with a signal handler, but not with another thread of execution, see std::memory_order". – Mike van Dyke Jul 24 '20 at 06:56
  • @fatun The proper way is to use atomic integer. The standard class is `std::atomic`. You can use relaxed operations (`memory_order_relax`). Search the [tag:stdatomic] tag. – curiousguy Jul 24 '20 at 10:04
  • @MikevanDyke 100%. How could it fail? It's a cancel notification. It indicates only that cancellation was requested, that's all. Cancellation can either be requested or not. There is nothing to properly "order". Cancellation is inherently asynchronous, it does not happen after the completion of a task or before the start of another. It's literally the opposite of a synchronization device, or a mutual exclusion device. Most proper uses of `std::atomic` are for mutual exclusion. – curiousguy Jul 24 '20 at 10:08
  • @curiousguy It is not a question about how it could fail. As much as I know, it's not defined in the standard and thus, it's undefined behaviour. So it might fail or it might not. – Mike van Dyke Jul 24 '20 at 11:58
  • @MikevanDyke If there is no way it can fail, then it can't. It isn't more UB than anything re: threads. All uses of threads are UB. The std does not define non sequential execution. Or any program. **Intuition is more relevant than the std.** Anyone seriously involved w/ standardisation would confirm that. – curiousguy Jul 24 '20 at 17:34