Tout en essayant de comprendre comment SubmissionPublisher
( code source dans Java SE 10, OpenJDK | docs ), une nouvelle classe ajoutée à Java SE dans la version 9, a été implémentée, je suis tombé sur quelques appels d'API dont VarHandle
je n'étais pas au courant auparavant:
fullFence
, acquireFence
, releaseFence
, loadLoadFence
Et storeStoreFence
.
Après avoir fait quelques recherches, en particulier en ce qui concerne le concept de barrières / clôtures de mémoire (j'en ai entendu parler auparavant, oui; mais je ne les ai jamais utilisées, donc je ne connaissais pas bien leur sémantique), je pense avoir une compréhension de base de ce à quoi elles servent. . Néanmoins, comme mes questions pourraient découler d'une idée fausse, je veux m'assurer d'avoir bien compris en premier lieu:
Les barrières mémoire réorganisent les contraintes concernant les opérations de lecture et d'écriture.
Les barrières mémoire peuvent être classées en deux catégories principales: les barrières mémoire unidirectionnelles et bidirectionnelles, selon qu'elles définissent des contraintes sur les lectures ou les écritures ou sur les deux.
C ++ prend en charge une variété de barrières de mémoire , cependant, celles-ci ne correspondent pas à celles fournies par
VarHandle
. Cependant, certaines des barrières de mémoire disponiblesVarHandle
fournir des effets de commande qui sont compatibles à leurs barrières de mémoire C ++ correspondant.#fullFence
est compatible avecatomic_thread_fence(memory_order_seq_cst)
#acquireFence
est compatible avecatomic_thread_fence(memory_order_acquire)
#releaseFence
est compatible avecatomic_thread_fence(memory_order_release)
#loadLoadFence
et#storeStoreFence
n'ont pas de contrepartie C ++ compatible
Le mot compatible semble vraiment important ici car la sémantique diffère clairement en ce qui concerne les détails. Par exemple, toutes les barrières C ++ sont bidirectionnelles, contrairement aux barrières Java (nécessairement).
- La plupart des barrières mémoire ont également des effets de synchronisation. Celles-ci dépendent en particulier du type de barrière utilisé et des instructions de barrière précédemment exécutées dans d'autres threads. Comme les implications complètes d'une instruction de barrière sont spécifiques au matériel, je m'en tiendrai aux barrières de niveau supérieur (C ++). En C ++, par exemple, les modifications apportées avant une instruction de barrière de libération sont visibles par un thread exécutant une instruction de barrière d' acquisition .
Mes hypothèses sont-elles correctes? Si oui, mes questions résultantes sont:
Les barrières de mémoire disponibles
VarHandle
provoquent-elles une sorte de synchronisation de la mémoire?Qu'elles provoquent ou non une synchronisation de la mémoire, à quoi peuvent servir les contraintes de réorganisation en Java? Le modèle de mémoire Java donne déjà des garanties très solides concernant l'ordre lorsque des champs volatils, des verrous ou des
VarHandle
opérations similaires#compareAndSet
sont impliqués.
Dans le cas où vous cherchez un exemple: ce qui précède BufferedSubscription
, une classe interne de SubmissionPublisher
(source liée ci-dessus), a établi une clôture complète dans la ligne 1079 (fonction growAndAdd
; car le site Web lié ne prend pas en charge les identificateurs de fragment, juste CTRL + F pour cela ). Cependant, je ne sais pas à quoi cela sert.
plain -> opaque -> release/acquire -> volatile (sequential consistency)
.Réponses:
C'est principalement une non-réponse, vraiment (au départ, je voulais en faire un commentaire, mais comme vous pouvez le voir, c'est beaucoup trop long). C'est juste que je me suis beaucoup posé des questions, j'ai fait beaucoup de lecture et de recherche et à ce stade, je peux dire en toute sécurité: c'est compliqué. J'ai même écrit plusieurs tests avec jcstress pour savoir comment ils travaillent vraiment (en regardant le code assembleur généré) et alors que certains d'entre eux d'une certaine manière , le sujet en général pas facile du sens.
La toute première chose que vous devez comprendre:
Ce travail est en cours.
Deuxièmement, si vous voulez vraiment gratter la surface ici, c'est la toute première chose à regarder . Le discours est incroyable. Ma partie préférée est quand Herb Sutter lève ses 5 doigts et dit: "C'est le nombre de personnes qui peuvent vraiment et correctement travailler avec ces derniers." Cela devrait vous donner un aperçu de la complexité impliquée. Néanmoins, il existe des exemples triviaux faciles à saisir (comme un compteur mis à jour par plusieurs threads qui ne se soucie pas des autres garanties de mémoire, mais se soucie seulement qu'il est lui-même incrémenté correctement).
Un autre exemple est quand (en java) vous voulez qu'un
volatile
drapeau contrôle les threads pour s'arrêter / démarrer. Vous savez, le classique:Si vous travaillez avec java, vous sauriez que sans
volatile
ce code est cassé (vous pouvez lire pourquoi le verrouillage de double vérification est cassé sans lui par exemple). Mais savez-vous aussi que pour certaines personnes qui écrivent du code haute performance, c'est trop?volatile
la lecture / écriture garantit également la cohérence séquentielle - qui a de solides garanties et certaines personnes veulent une version plus faible de cela.Et vous vous demandez pourquoi quelqu'un pourrait en avoir besoin, par exemple? Tout le monde n'est pas intéressé par tous les changements soutenus par a
volatile
.Voyons comment nous y parviendrons en java. Tout d' abord, ces exotiques choses existaient déjà dans l'API:
AtomicInteger::lazySet
. Ceci n'est pas spécifié dans le modèle de mémoire Java et n'a pas de définition claire ; les gens l'utilisaient encore (LMAX, afaik ou ceci pour plus de lecture ). À mon humble avis,AtomicInteger::lazySet
estVarHandle::releaseFence
(ouVarHandle::storeStoreFence
).Essayons de répondre à la raison pour laquelle quelqu'un en a besoin ?
JMM a essentiellement deux façons d'accéder à un champ: simple et volatile (ce qui garantit la cohérence séquentielle ). Toutes ces méthodes que vous mentionnez sont là pour apporter quelque chose entre les deux - libérer / acquérir la sémantique ; il y a des cas, je suppose, où les gens en ont réellement besoin.
Un relâchement encore plus de la libération / acquisition serait opaque , ce que j'essaie toujours de comprendre pleinement .
Donc, résultat (votre compréhension est assez correcte, btw): si vous prévoyez d'utiliser cela en java - ils n'ont pas de spécification pour le moment, faites-le à vos risques et périls. Si vous voulez les comprendre, leurs modes équivalents C ++ sont le point de départ.
la source
lazySet
en reliant des réponses anciennes, la documentation actuelle dit précisément ce que cela signifie, de nos jours. De plus, il est trompeur de dire que le JMM n'a que deux modes d'accès. Nous avons une lecture et une écriture volatiles , qui ensemble peuvent établir une relation qui se produit avant .volatile
mot - clé était C99, cinq ans après Java, mais elle manquait toujours de sémantique utile, même C ++ 03 n'a pas de modèle de mémoire. Les choses que C ++ appelle "atomiques" sont également beaucoup plus jeunes que Java. Et levolatile
mot-clé n'implique même pas de mises à jour atomiques. Alors pourquoi devrait-il être nommé ainsi.restrict
, cependant, je me souviens des moments où j'ai dû écrire__volatile
pour utiliser une extension de compilateur sans mot-clé. Alors peut-être, il n'a pas complètement implémenté C89? Ne me dis pas que je suis si vieux. Avant Java 5,volatile
était beaucoup plus proche de C. Mais Java n'avait pas de MMIO, donc son objectif était toujours le multi-threading, mais la sémantique pré-Java 5 n'était pas très utile pour cela. Donc, la version / acquisition comme la sémantique a été ajoutée, mais ce n'est pas atomique (les mises à jour atomiques sont une fonctionnalité supplémentaire construite au-dessus).