43 votes

Un Double contrôle de Verrouillage de Singleton en C++11

Est la suivante singleton mise en œuvre des données de course gratuit?

static std::atomic<Tp *> m_instance;
...

static Tp &
instance()
{
    if (!m_instance.load(std::memory_order_relaxed))
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        if (!m_instance.load(std::memory_order_acquire))
        {
            Tp * i = new Tp;
            m_instance.store(i, std::memory_order_release);    
        }    
    }

    return * m_instance.load(std::memory_order_relaxed);
}

Est l' std::memory_model_acquire de l'opération de chargement superflu? Est-il possible de se détendre à la fois de la charge et des opérations de banque par commutation à std::memory_order_relaxed? Dans ce cas, est l'acquisition/diffusion sémantique std::mutex suffisant pour garantir son exactitude, ou un autre std::atomic_thread_fence(std::memory_order_release) est également nécessaire pour s'assurer que les écritures dans la mémoire du constructeur se produire avant que l'atmosphère détendue de la boutique? Pourtant, c'est l'utilisation de la clôture de l'équivalent d'avoir le magasin avec memory_order_release?

EDIT: Merci pour la réponse de Jean, je suis venu avec la suite de la mise en œuvre qui devraient être données à la course libre. Même si l'intérieur de la charge pourrait être non-atomique à tous, j'ai décidé de partir une ambiance détendue charge en ce qu'elle n'affecte pas les performances. En comparaison, afin de toujours avoir une charge externe avec l'acquisition de la mémoire de commande, le thread_local machines améliore les performances de l'accès à l'instance d'environ un ordre de grandeur.

static Tp &
instance()
{
    static thread_local Tp *instance;

    if (!instance && 
        !(instance = m_instance.load(std::memory_order_acquire)))
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        if (!(instance = m_instance.load(std::memory_order_relaxed)))
        {
            instance = new Tp; 
            m_instance.store(instance, std::memory_order_release);    
        }    
    }
    return *instance;
}

27voto

deft_code Points 19418

Je pense que c'est un très bonne question et John Calsbeek a la réponse correcte.

Cependant, juste pour être clair, un paresseux singleton est le mieux mis en œuvre à l'aide de la classique Meyers singleton. Il a garanti sémantique correcte en C++11.

§ 6.7.4

... Si le contrôle d'entrée la déclaration simultanément tandis que la variable est initialisée, la concurrente de l'exécution doit attendre l'achèvement de la phase d'initialisation. ...

L'Meyer singleton est préféré en ce que le compilateur peut agressivement optimiser le code simultané. Le compilateur serait plus restreint, si elle avait pour préserver la sémantique d'un std::mutex. En outre, l'Meyer singleton est 2 lignes , et presque impossible de se tromper.

Voici un exemple classique de Meyer singleton. Simple, élégant, et cassé en c++03. Mais simple, élégant et puissant en c++11.

class Foo
{
public:
   static Foo& instance( void )
   {
      static Foo s_instance;
      return s_instance;
   }
};

20voto

John Calsbeek Points 19381

Que la mise en œuvre n'est pas de la course libre. L'atomic magasin du singleton, alors qu'il utilise la libération de la sémantique, ne se synchroniser avec le correspondant d'acquérir de l'opération, l'opération de chargement est déjà surveillé par le mutex.

Il est possible que l'extérieur détendu charge lecture d'un pointeur non null avant le verrouillage fil terminé l'initialisation du singleton.

L'acquisition, qui est gardé par la serrure, d'autre part, est redondant. Il permettra de synchroniser avec n'importe quel magasin, avec la libération de la sémantique sur un autre thread, mais à ce moment (merci pour le mutex) le seul fil qui peut éventuellement magasin est le thread courant. Cette charge n'a même pas besoin d'être atomique-pas de magasins peut se produire à partir d'un autre thread.

Voir Anthony Williams de la série sur le C++0x multithreading.

7voto

MaHuJa Points 890

Voir aussi call_once. Où vous avais déjà utiliser un singleton à faire quelque chose, mais pas réellement l'objet retourné pour rien, call_once peut être la meilleure solution. Pour un singleton, vous pourriez faire call_once pour définir un (mondial?) variable, puis de retour cette variable...

Simplifié pour des raisons de concision:

template< class Function, class... Args>
void call_once( std::once_flag& flag, Function&& f, Args&& args...);
  • Exactement une exécution exacte de l'une des fonctions, passé comme f pour les invocations dans le groupe (même drapeau objet), est effectuée.

  • Aucune invocation dans le groupe revient avant que le précitées de l'exécution de la fonction sélectionnée est terminée avec succès

1voto

ToBeFrank Points 22

Oui, la première solution est sûr (je suis en désaccord avec la accepté de répondre). À l'aide de memory_order_release pour le magasin de m_instance signifie qu'aucun écrit dans le thread qui effectue le magasin peut être réorganisées après le magasin. Cela signifie que si un atomique de charge de m_instance est considéré comme non-NULLE (dans n'importe quel thread), il est pointé à un objet construit.

Le seul problème que je vois dans ce code, c'est qu'il est possible pour la première memory_order_relaxed charge rater m_instance a été créé par un autre thread. Cependant, ce n'est pas un problème parce que le mutex et memory_order_acquire charge de s'assurer que le magasin de l'autre thread va maintenant être vu dans ce fil, et vous n'aurez pas réinitialiser m_instance. Ainsi, vous avez un mutex lock quand elle n'était pas strictement nécessaire.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X