Streamsichere Signale, die sehr praktisch zu verwenden sind

Es gibt viele Bibliotheken auf der Welt, die Signale in C ++ implementieren. Leider haben alle Implementierungen, auf die ich gestoßen bin, einige Probleme, die mich daran hindern, einfachen Multithread-Code mit diesen Bibliotheken zu schreiben. Hier werde ich über diese Probleme sprechen und wie man sie löst.

Was sind Signale?


Ich denke, viele kennen dieses Konzept bereits, aber für den Fall, dass ich es schreibe.

Ein Signal ist eine Möglichkeit, eine Benachrichtigung über ein beliebiges Ereignis an Empfänger zu senden, die sich unabhängig voneinander registrieren können. Wenn Sie möchten, Rückruf mit vielen Empfängern. Oder für diejenigen, die mit .NET gearbeitet haben, ein Multicast-Delegierter.

Ein paar Beispiele mit boost :: signals2
Signalansage:

struct Button
{
    boost::signals2::signal OnClick;
};

Verbindung zu einem Signal und Trennung davon:

void ClickHandler()
{ cout << “Button clicked” << endl; }
// ...
boost::signals2::connection c = button->OnClick.connect(&ClickHandler);
// ...
c.disconnect();

Anruf signalisieren:

struct Button
{
    boost::signals2::signal OnClick;
private:
    void MouseDownHandler()
    {
        OnClick();
    }
};


Nun zu den Problemen


In Single-Threaded-Code sieht alles gut aus und funktioniert ziemlich gut, aber was ist mit Multi-Threaded?

Hier gibt es leider drei Probleme, die verschiedenen Implementierungen gemeinsam sind:

  1. Es gibt keine Möglichkeit, sich atomar mit einem Signal zu verbinden und einen gebundenen Zustand zu erhalten
  2. Nicht blockierende Trennung von einem Signal
  3. Durch das Deaktivieren eines asynchronen Handlers werden keine Anrufe abgebrochen, die sich bereits in der Thread-Warteschlange befinden

Betrachten wir jeden von ihnen im Detail. Dazu schreiben wir den Firmware-Teil einer Set-Top-Box für imaginäre Medien, nämlich drei Klassen:

  • StorageManager - eine Klasse, die auf Flash-Laufwerke, DVDs und andere Medien reagiert, die der Benutzer in die Konsole eingefügt hat
  • MediaScanner - Eine Klasse, die auf jedem dieser Geräte nach Mediendateien sucht
  • MediaUiModel - ein Modell zum Anzeigen dieser Mediendateien in einem imaginären Modell-Ansicht-Alles-Framework

Ich muss gleich sagen, dass der Code, den Sie hier sehen, extrem vereinfacht ist und nichts Überflüssiges enthält, damit wir uns auf diese Probleme konzentrieren können. Sie sehen auch Typen vom Typ TypePtr . Dies ist nur std :: shared_ptrSei nicht beunruhigt.

Es gibt keine Möglichkeit, sich atomar mit einem Signal zu verbinden und einen gebundenen Zustand zu erhalten


Also, StorageManager . Wir benötigen einen Getter für die Medien, die bereits in der Konsole eingelegt sind, und ein Signal, um Sie über neue zu benachrichtigen.

class StorageManager
{
public:
    std::vector GetStorages() const;
    boost::signals2::signal OnStorageAdded;
    // ...
};

Leider kann eine solche Schnittstelle nicht verwendet werden, ohne eine Racebedingung zu erhalten.

Funktioniert nicht in dieser Reihenfolge ...

storageManager->OnStorageAdded.connect(&StorageHandler);
// Если пользователь вставляет флэшку до цикла, она будет обработана дважды
for (auto&& storage : storageManager->GetStorages())
    StorageHandler(storage);

... und funktioniert nicht in dieser Reihenfolge.

for (auto&& storage : storageManager->GetStorages())
    StorageHandler(storage);
// Если пользователь вставляет флэшку до подключения к сигналу, она не будет обработана совсем
storageManager->OnStorageAdded.connect(&StorageHandler);

Gemeinsame Lösung


Da wir eine Racebedingung haben, brauchen wir natürlich einen Mutex.

class StorageManager
{
    mutable std::recursive_mutex   _mutex;
    std::vector        _storages;
public:
    StorageManager()
    { /* ... */ }
    boost::signals2::signal OnStorageAdded;
    std::recursive_mutex& GetMutex() const
    { return _mutex; }
    std::vector GetStorages() const
    {
        std::lock_guard l(_mutex);
        return _storages;
    }
private:
    void ReportNewStorage(const StoragePtr& storage)
    {
        std::lock_guard l(_mutex);
        _storages.push_back(storage);
        OnStorageAdded(storage);
    }
};
// ...
{
    std::lock_guard l(storageManager->GetMutex());
    storageManager->OnStorageAdded.connect(&StorageHandler);
    for (auto&& storage : storageManager->GetStorages())
        StorageHandler(storage);
}

Dieser Code wird funktionieren, hat aber mehrere Nachteile:

  • Wenn Sie std :: mutex anstelle von std :: recursive_mutex verwenden möchten , verlieren Sie die Fähigkeit, es in der GetStorages- Methode zu erfassen , wodurch die StorageManager- Klasse nicht mehr threadsicher wird
  • Sie können das Kopieren einer Sammlung in GetStorages nicht loswerden, ohne die Thread-Sicherheit von StorageManager zu verlieren .
  • Sie müssen den Typ std :: vector anzeigenobwohl es sich in Wirklichkeit nur um Implementierungsdetails handelt
  • Ein ziemlich umfangreicher Code zum Verbinden mit einem Signal und zum Empfangen des aktuellen Zustands, der in diesem Fall für verschiedene Signale fast gleich ist

Wie geht es besser?


Lassen Sie uns alles, was wir rund um den Connect- Aufruf tun (den Mutex ergreifen und die Sammlung durchlaufen), nach innen übertragen.

Es ist wichtig zu verstehen, dass der Algorithmus zum Erhalten des aktuellen Zustands von der Art dieses Zustands selbst abhängt. Wenn es sich um eine Auflistung handelt, müssen Sie den Handler für jedes Element aufrufen. Wenn es sich beispielsweise um eine Auflistung handelt, müssen Sie den Handler genau einmal aufrufen. Dementsprechend brauchen wir eine Abstraktion.

Fügen Sie dem Signal einen Populator hinzu - eine Funktion, die den jetzt verbundenen Handler akzeptiert und den Signalbesitzer (in unserem Fall StorageManager) bestimmen lässt, wie der aktuelle Status an diesen Handler gesendet wird.

template < typename Signature >
class signal
{
    using populator_type = std::function&)>;
    mutable std::mutex                       _mutex;
    std::list >     _handlers;
    populator_type                           _populator;
public:
    signal(populator_type populator)
        : _populator(std::move(populator))
    { }
    std::mutex& get_mutex() const { return _mutex; }
    signal_connection connect(std::function handler)
    {
        std::lock_guard l(_mutex);
        _populator(handler); // Владелец сигнала определяет конкретный алгоритм получения состояния
        _handlers.push_back(std::move(handler));
        return signal_connection([&]() { /* удаляем обработчик из _handlers */ } );
    }
    // ...
};

Die signal_connection- Klasse akzeptiert derzeit eine Lambda-Funktion, die den Handler aus der Liste im Signal entfernt. Ich werde später einen etwas vollständigeren Code geben.

Wir haben StorageManager mit diesem neuen Konzept neu geschrieben:

class StorageManager
{
    std::vector        _storages;
public:
    StorageManager()
        : _storages([&](const std::function& h) { for (auto&& s : _storages) h(s); })
    { /* ... */ }
    signal OnStorageAdded;
private:
    void ReportNewStorage(const StoragePtr& storage)
    {
        // Мы должны захватить мьютекс именно тут, а не внутри вызова сигнала,
        // потому что он защищает в том числе и коллекцию _storages
        std::lock_guard l(OnStorageAdded.get_mutex());
        _storages.push_back(storage);
        OnStorageAdded(storage);
    }
};

Wenn Sie C ++ 14 verwenden, kann der Populator ziemlich kurz sein:

StorageManager()
    : _storages([&](auto&& h) { for (auto&& s : _storages) h(s); })
{ }

Beachten Sie, dass beim Aufrufen des Populators der Mutex in der signal :: connect- Methode erfasst wird , sodass dies im Body des Populators nicht erforderlich ist.

Der Client-Code wird sehr kurz:

storageManager->OnStorageAdded.connect(&StorageHandler);

Mit einer Leitung verbinden wir uns gleichzeitig mit dem Signal und erhalten den aktuellen Zustand des Objekts. Großartig!

Nicht blockierende Trennung von einem Signal


Jetzt ist es Zeit, MediaScanner zu schreiben . Stellen Sie im Konstruktor eine Verbindung zum Signal StorageManager :: OnStorageAdded her , und trennen Sie im Destruktor die Verbindung.

class MediaScanner
{
private:
    boost::signals2::connection _connection;
public:
    MediaScanner(const StorageManagerPtr& storageManager)
    { _connection = storageManager->OnStorageAdded.connect([&](const StoragePtr& s) { this->StorageHandler(s); }); }
    ~MediaScanner()
    {
        _connection.disconnect();
        // Обработчик сигнала может всё ещё исполняться в потоке, вызвавшем сигнал.
        // В этом случае, далее он будет обращаться к разрушенному объекту MediaScanner.
    }
private:
    void StorageHandler(const StoragePtr& storage)
    { /* Здесь что-то долгое */ }
};

Leider wird dieser Code von Zeit zu Zeit fallen. Der Grund ist, wie die Trennungsmethode in allen mir bekannten Implementierungen funktioniert . Es stellt sicher, dass der entsprechende Handler beim nächsten Aufruf des Signals nicht mehr funktioniert. Wenn der Handler zu diesem Zeitpunkt in einem anderen Thread ausgeführt wird, wird er in diesem Fall nicht unterbrochen und arbeitet mit dem zerstörten MediaScanner- Objekt weiter .

Lösung in Qt


In Qt gehört jedes Objekt zu einem bestimmten Thread, und seine Handler werden ausschließlich in diesem Thread aufgerufen. Um die Verbindung zum Signal sicher zu trennen, sollten Sie die QObject :: deleteLater-Methode aufrufen , mit der sichergestellt wird, dass die eigentliche Löschung aus dem gewünschten Thread erfolgt und nach der Löschung kein Handler aufgerufen wird.

mediaScanner->deleteLater();

Dies ist eine gute Option, wenn Sie bereit sind, Qt vollständig zu integrieren (geben Sie std :: thread im Kern Ihres Programms zugunsten von QObject, QThread usw. auf).

Lösung in boost :: signals2


Um dieses Problem zu lösen, schlägt boost die Verwendung der Methoden track / track_foreign in einem Slot (d. H. Einem Handler) vor. Diese Methoden nehmen weak_ptr für ein beliebiges Objekt und die Verbindung des Handlers mit dem Signal besteht, während jedes der Objekte lebt, was der Slot „beobachtet“.

Dies funktioniert ganz einfach: In jedem Slot gibt es eine Sammlung von weak_ptrs für überwachte Objekte, die für die Dauer des Handlers "sperren" (sorry). Somit ist nicht garantiert, dass diese Objekte zerstört werden, solange der Handler-Code Zugriff auf sie hat. Wenn eines der Objekte bereits zerstört wurde, ist die Verbindung unterbrochen.

Das Problem ist, dass wir hierzu weak_ptr benötigenauf dem zu signierenden Objekt. Meiner Meinung nach ist der beste Weg, dies zu erreichen, eine Factory-Methode in der MediaScanner- Klasse zu erstellen , in der das erstellte Objekt mit allen Signalen signiert wird , an denen es interessiert ist:

class MediaScanner
{
public:
    static std::shared_ptr Create(const StorageManagerPtr& storageManager)
    {
        std::lock_guard l(storageManager->GetMutex());
        MediaScannerPtr result(new MediaScanner);
        boost::signals2::signal::slot_type
            slot(bind(&MediaScanner::StorageHandler, result.get(), _1));
        slot.track_foreign(result);
        storageManager->OnStorageAdded.connect(slot);
        for (auto&& storage : storageManager->GetStorages())
            result->StorageHandler(storage);
        return result;
    }
private:
    MediaScanner() // приватный конструктор!
    { /* Проинициализировать всё, кроме обработчиков сигналов */ }
    void StorageHandler(const StoragePtr& storage);
    { /* Здесь что-то долгое */ }
};

Die Nachteile sind also:

  • Sehr viel Code, den Sie jedes Mal kopieren
  • Die Initialisierung von MediaScanner gliedert sich in zwei Teile: das Abonnieren von Signalen in der Create- Methode und alles andere im Konstruktor
  • Sie sind erforderlich , um eine Verwendung Shared_ptr zu halten MediaScanner
  • Sie sind nicht sicher, ob MediaScanner gelöscht wurde, als Sie den letzten externen Link dazu freigegeben haben. Dies kann ein Problem sein, wenn eine begrenzte Ressource verwendet wird, die Sie nach der Freigabe von MediaScanner wiederverwenden möchten.

Wie geht es besser?


Nehmen wir den Disconnect- Methodenblock vor , damit uns garantiert wird, dass es möglich ist, alles zu zerstören, auf das der Signalhandler Zugriff hatte, nachdem er die Kontrolle zurückgegeben hat. So etwas wie die std :: thread :: join- Methode .

Mit Blick auf die Zukunft werde ich sagen, dass wir hierfür drei Klassen benötigen:

  • life_token - Steuert die Lebensdauer des Handlers , ermöglicht es Ihnen, ihn als "sterbend" zu markieren und gegebenenfalls auf das Ende der Ausführung zu warten
  • life_token :: checker - Wird im Signal neben dem Handler gespeichert und bezieht sich auf dessen life_token
  • life_token :: checker :: execution_guard - wird für die Dauer der Ausführung des Handlers auf dem Stack erstellt, blockiert das entsprechende life_token und ermöglicht es Ihnen zu überprüfen, ob der Handler früher "gestorben" ist

Code Klasse signal_connection :

class signal_connection
{
    life_token               _token;
    std::function    _eraseHandlerFunc;
public:
    signal_connection(life_token token, std::function eraseHandlerFunc)
        : _token(token), _eraseHandlerFunc(eraseHandlerFunc)
    { }
    ~signal_connection();
    { disconnect(); }
    void disconnect()
    {
        if (_token.released())
            return;
        _token.release(); // Тут мы ждём, если обработчик сейчас заблокирован (т. е. исполняется)
        _eraseHandler(); // Та самая лямбда-функция, которая удалит обработчик из списка
    }
};

Hier muss ich sagen, dass ich ein Unterstützer des RAII-Verbindungsobjekts bin. Ich werde nicht im Detail darauf eingehen, sondern nur sagen, dass es in diesem Zusammenhang nicht von Bedeutung ist.

Auch die Signalklasse wird sich etwas ändern:

template < typename Signature >
class signal
{
    using populator_type = std::function&)>;
    struct handler
    {
        std::function    handler_func;
        life_token::checker         life_checker;
    };
    mutable std::mutex            _mutex;
    std::list            _handlers;
    populator_type                _populator;
public:
    // ...
    signal_connection connect(std::function handler)
    {
        std::lock_guard l(_mutex);
        life_token token;
        _populator(handler);
        _handlers.push_back(Handler{std::move(handler), life_token::checker(token)});
        return signal_connection(token, [&]() { /* удаляем обработчик из _handlers */ } );
    }
    template < typename... Args >
    void operator() (Args&&... args) const
    {
        for (auto&& handler : _handlers)
        {
            life_token::checker::execution_guard g(handler.life_checker);
            if (g.is_alive())
                handler.handler_func(forward(args)...);
        }
    }
};

Jetzt haben wir zu jedem Handler nächstes ist ein Objekt life_token :: Kontrolleur , der bezieht sich life_token , liegt in signal_connection . Wir erfassen es für die Dauer des Handlers mit dem Objekt life_token :: checker :: execution_guard

Ich werde die Implementierung dieser Objekte unter dem Spoiler verstecken. Wenn Sie müde sind, können Sie überspringen.
In life_token benötigen wir folgende Dinge:

  • Eine Art primitives Betriebssystem, auf das in life_token :: release gewartet werden soll (hier nehmen wir der Einfachheit halber einen Mutex)
  • Live / Dead Flag
  • Zähler durch execution_guard sperren (hier der Einfachheit halber weggelassen)

class life_token
{
    struct impl
    {
        std::mutex              mutex;
        bool                    alive = true;
    };
    std::shared_ptr       _impl;
public:
    life_token() : _impl(std::make_shared()) { }
    ~life_token() { release(); }
    bool released() const { return !_impl; }
    void release()
    {
        if (released())
            return;
        std::lock_guard l(_impl->mutex);
        _impl->alive = false;
        _impl.reset();
    }
    class checker
    {
        shared_ptr	_impl;
    public:
        checker(const life_token& t) : _impl(t._impl) { }
        class execution_guard
        {
            shared_ptr	_impl;
        public:
            execution_guard(const checker& c) : _impl(c._impl) { _impl->mutex.lock(); }
            ~execution_guard() { _impl->mutex.unlock(); }
            bool is_alive() const { return _impl->alive; }
        };
    };
};

Ein Mutex wird für die Lebensdauer von execution_guard erfasst . Wenn die Methode life_token :: release zu diesem Zeitpunkt in einem anderen Thread aufgerufen wird, blockiert sie dementsprechend die Erfassung desselben Mutex und wartet, bis der Signalhandler die Ausführung abgeschlossen hat. Danach wird das Aktivitäts- Flag gelöscht , und alle nachfolgenden Anrufe an das Signal führen nicht zu einem Anruf an den Handler.

Wie sieht MediaScanner- Code jetzt aus ? Genau so, wie wir es am Anfang schreiben wollten:

class MediaScanner
{
private:
    signals_connection    _connection;
public:
    MediaScanner(const StorageManagerPtr& storageManager)
    { _connection = storageManager->OnStorageAdded.connect([&](const StoragePtr& s) { this->StorageHandler(s); }); }
    ~MediaScanner()
    { _connection.disconnect(); }
private:
    void StorageHandler(const StoragePtr& storage)
    { /* Здесь что-то долгое */ }
};

Durch das Deaktivieren eines asynchronen Handlers werden keine Anrufe abgebrochen, die sich bereits in der Thread-Warteschlange befinden


Wir schreiben MediaUiModel , das auf die gefundenen Mediendateien reagiert und Zeilen hinzufügt, um sie anzuzeigen. Fügen

Sie dazu das folgende Signal zu MediaScanner hinzu :

signal OnMediaFound;

Hier gibt es zwei wichtige Dinge:

  • Ein Modell ist ein Objekt einer UI-Bibliothek, daher sollten alle damit verbundenen Aktionen über einen UI-Stream ausgeführt werden.
  • Häufig verwenden UI-Bibliotheken ihre eigene Besitzhierarchie. Daher können wir shared_ptr nicht zum Speichern des Modells verwenden. Dementsprechend funktioniert der Fokus mit track / track_foreign hier nicht, aber das ist jetzt nicht die Hauptsache, also geben wir vor, dass alles in Ordnung ist

class MediaUiModel : public UiModel
{
private:
    boost::io_service&             _uiThread;
    boost::signals2::connection    _connection;
public:
    MediaUiModel(boost::io_service& uiThread, const MediaScanner& scanner)
        : _uiThread(uiThread)
    {
        std::lock_guard l(scanner.GetMutex());
        scanner.OnMediaFound.connect([&](const MediaPtr& m) { this->MediaHandler(m); });
        for (auto&& m : scanner.GetMedia())
            AppendRow(MediaUiModelRow(m))
    }
    ~MediaUiModel()
    { _connection.disconnect(); }
private:
    // Этот метод выполняется в потоке MediaScanner'а, и всю реальную работу перебрасывает в поток UI.
    void MediaHandler(const MediaPtr& m)
    { _uiThread.post([&]() { this->AppendRow(MediaUiModelRow(m)); }); }
};

Zusätzlich zum vorherigen Problem gibt es noch eines. Jedes Mal, wenn ein Signal ausgelöst wird, übertragen wir den Handler an den UI-Stream. Wenn wir das Modell irgendwann löschen (zum Beispiel die Galerie-Anwendung verlassen), gelangen alle diese Handler später zum toten Objekt. Und wieder der Herbst.

Lösung in Qt


Alle der gleichen deleteLater , mit den gleichen Eigenschaften.

Lösung in boost :: signals2


Wenn Sie Glück haben und Ihr UI-Framework es Ihnen ermöglicht, deleteLater- Modelle mitzuteilen , werden Sie gespeichert. Sie müssen nur eine öffentliche Methode erstellen , die zuerst das Modell von den Signalen trennt und dann deleteLater aufruft , und Sie erhalten ungefähr das gleiche Verhalten wie in Qt. Richtig, Sie müssen das vorherige Problem noch lösen. Zu diesem Zweck erstellen Sie wahrscheinlich ein shared_ptr- Modell in einem Modell , das Signale abonniert. Der Code ist nicht sehr klein, aber dies ist eine Frage der Technologie.

Wenn Sie Pech haben und Ihr UI-Framework das Löschen des Modells genau zum gewünschten Zeitpunkt erfordert, erfinden Sie Ihr life_token .

Zum Beispiel so etwas (auch besser nicht zu lesen, wenn Sie müde sind).
template < typename Signature_ >
class AsyncToUiHandlerWrapper
{
private:
    boost::io_service&          _uiThread;
    std::function   _realHandler;
    bool                        _released;
    mutable std::mutex          _mutex;
public:
    AsyncToUiHandlerWrapper(boost::io_service& uiThread, std::function realHandler)
        : _uiThread(uiThread), _realHandler(realHandler), _released(false)
    { }
    void Release()
    {
        std::lock_guard l(_mutex);
        _released = true;
    }
    template < typename... Args_ >
    static void AsyncHandler(const std::weak_ptr& selfWeak, Args_&&... args)
    {
        auto self = selfWeak.lock();
        std::lock_guard l(self->_mutex);
        if (!self->_released) // AsyncToUiHandlerWrapper не был освобождён, значит _uiThread всё ещё ссылается на живой объект
            self->_uiThread.post(std::bind(&AsyncToUiHandlerWrapper::UiThreadHandler, selfWeak, std::forward(args)...)));
    }
private:
    template < typename... Args_ >
    static void UiThreadHandler(const std::weak_ptr& selfWeak, Args_&&... args)
    {
        auto self = selfWeak.lock();
        if (!self)
            return;
        if (!self->_released) // AsyncToUiHandlerWrapper не был освобождён, значит, объекты, доступные _realHandler, ещё живы
            self->_realHandler(std::forward(args)...);
    }
};
class MediaUiModel : public UiModel
{
private:
    using AsyncMediaHandler = AsyncToUiHandlerWrapper;
private:
    std::shared_ptr    _asyncHandler;
public:
    MediaUiModel(boost::io_service& uiThread, const MediaScanner& scanner)
    {
        try
        {
            _asyncHandler = std::make_shared(std::ref(uiThread), [&](const MediaPtr& m) { this->AppendRow(MediaUiModelRow(m)); });
            std::lock_guard l(scanner.GetMutex());
            boost::signals2::signal::slot_type
                slot(std::bind(&AsyncMediaHandler::AsyncHandler, std::weak_ptr(_asyncHandler), std::placeholders::_1));
            slot.track_foreign(_asyncHandler);
            scanner.OnMediaFound.connect(slot);
            for (auto&& m : scanner.GetMedia())
                AppendRow(MediaUiModelRow(m));
        }
        catch (...)
        {
            Destroy();
            throw;
        }
    }
    ~MediaUiModel()
    { Destroy(); }
private:
    void Destroy()
    {
        if (_asyncHandler)
            _asyncHandler->Release(); // Асинхронный код не обращается к MediaUiModel после этой строки, так что можно окончательно разрушать объект
        _asyncHandler.reset();
    }
};

Ich werde diesen Code nicht einmal kommentieren, lasst uns nur ein bisschen traurig werden.

Wie geht es besser?


Sehr einfach. Erstellen Sie zunächst eine Schnittstelle für den Thread als Taskwarteschlange:

struct task_executor
{
    virtual ~task_executor() { }
    virtual void add_task(const std::function& task) = 0;
};

Zweitens erstellen Sie eine überladene Verbindungsmethode im Signal , die den Stream akzeptiert:

signal_connection connect(const std::shared_ptr& worker, std::function handler);

Legen Sie bei dieser Methode einen Wrapper über den Handler in der _handlers- Auflistung , der beim Aufrufen ein Paar des Handlers und des entsprechenden life_token :: checkers an den gewünschten Stream überträgt . Um den echten Handler im letzten Thread aufzurufen, verwenden wir execution_guard auf die gleiche Weise wie zuvor.

So garantiert uns die Methode disconnect unter anderem, dass auch nach dem Trennen der Verbindung zum Signal keine asynchronen Handler aufgerufen werden. Ich werde den

Code für den Wrapper und die überladene Verbindungsmethode hier nicht bereitstellen. Ich denke die Idee ist klar und so.

Der Modellcode wird sehr einfach:

class MediaUiModel : public UiModel
{
private:
    signal_connection    _connection;
public:
    MediaUiModel(const std::shared_ptr& uiThread, const MediaScanner& scanner)
    { _connection = scanner.OnMediaFound.connect(uiThread, [&](const MediaPtr& m) { this->AppendRow(MediaUiModelRow(m)); }); }
    ~MediaUiModel()
    { _connection.reset(); }
};

In diesem Fall wird die AppendRow- Methode im UI-Thread nur so lange aufgerufen, bis die Verbindung getrennt wird.

Um es zusammenzufassen


Es gibt also drei wichtige Dinge, mit denen Sie mithilfe von Signalen viel einfacheren Code schreiben können:

  1. Mit Populatoren können Sie bequem den aktuellen Status abrufen, während Sie mit dem Signal verbunden sind
  2. Mit der Methode zum Sperren der Trennung können Sie das Abonnement eines Objekts in einem eigenen Destruktor aufheben
  3. Damit das vorherige Element für asynchrone Handler gültig ist, muss die Unterbrechung auch die Aufrufe markieren, die bereits in der Stream-Warteschlange liegen, als "irrelevant".

Natürlich ist der Signalcode, den ich hierher gebracht habe, sehr einfach und primitiv und funktioniert nicht sehr schnell. Mein Ziel war es, über einen alternativen Ansatz zu sprechen, der mir heute attraktiver erscheint als die vorherrschenden. In Wirklichkeit können all diese Dinge viel effizienter geschrieben werden.

Wir verwenden diesen Ansatz in unserem Projekt seit ungefähr fünf Jahren und sind sehr glücklich.

Fertig Implementierung


Ich habe mit C ++ 11 die Signale, die wir hatten, von Grund auf neu geschrieben und die Teile der Implementierung verbessert, die es seit langem wert waren, verbessert zu werden.
Verwendung für die Gesundheit: https://github.com/koplyarov/wigwag .

Mini FAQ


Gemessen an der Reaktion der Leute auf reddit und auf Twitter betreffen drei Fragen alle:

F: Sie müssen life_token sofort blockieren, um jeden Handler aufzurufen. Wäre es langsam?
A: Seltsamerweise nein. Sie können atomare Variablen anstelle des Mutex verwenden. Wenn zum Zeitpunkt der Ausführung des Handlers immer noch der Disconnect- Aufruf angezeigt wird, warten Sie auf std :: condition_variable . Dann ist das Ergebnis genau umgekehrt: Aufgrund des fehlenden Overheads in Form von track / track_foreign (der das Arbeiten mit weak_ptr- Auflistungen erfordert ) lässt diese Implementierung memory :: speed2 in Bezug auf Speicher und Geschwindigkeit weit hinter sich und übertrifft sogar Qt.
Benchmarks finden Sie hier .

F: Wird es aufgrund der Methode zum Sperren der Verbindung zu Deadlocks kommen?
A: Ja, Deadlocks sind wirklich ein bisschen einfacher zu bekommen als in Boost und Qt. Meiner Meinung nach zahlt sich dies mit einem einfacheren Code für die Verwendung von Signalen und einer höheren Arbeitsgeschwindigkeit aus. Wenn Sie außerdem genau überwachen, wer wem folgt, sind solche Situationen eher die Ausnahme.

Deadlocks müssen natürlich gefangen und repariert werden. Unter Linux empfehle ich dafür Helgrind . Für Windows bieten Intel Inspector und CHESS eine zweiminütige Google-Suche an .

Wenn Sie sich aus irgendeinem Grund keine der oben genannten Möglichkeiten leisten können (z. B. wenn Ihre Plattform nicht über genügend Arbeitsspeicher verfügt, um Helgrind oder ein Randbetriebssystem auszuführen), gibt es eine Krücke in Form dieser (erneut vereinfachten) Mutex-Klasse :

class mutex
{
private:
    std::timed_mutex    _m;
public:
    void lock()
    {
        if (_m.try_lock())
            return;
        while (!_m.try_lock_for(std::chrono::seconds(10)))
            Logger::Warning() << "Could not lock mutex " << (void*)this << " for a long time:\n" << get_backtrace_string();
    }
    // ...
};

Sowohl Visual Studio als auch GCC verfügen über Funktionen zum Abrufen von Rückverfolgungen im Code. Hinzu kommt ein guter Libunwind.
Mit diesem Ansatz werden die meisten Ihrer Deadlocks von der Qualitätssicherung erfasst, und auf einen Blick sehen Sie in den Protokollen, wo alles blockiert ist. Es muss nur noch repariert werden.

F: Kann ein Mutex für mehrere Signale verwendet werden? Ist es möglich, Ausnahmen so zu behandeln, wie ich es möchte? Ist es möglich, keine Synchronisation zu verwenden und schnelle Singlethread-Signale zu erhalten?
A: Sie können, Sie können, Sie können. Hierfür gibt es Template-Strategien. Lesen Sie mehr in der Dokumentation.

Jetzt auch beliebt: