J'entends que const
signifie thread-safe in C++ 11. Est-ce vrai?
Est-ce à dire que const
est maintenant l'équivalent de Javasynchronized
?
Sont-ils à court de mots clés?
J'entends que
const
signifie thread-safe dans C++ 11. Est-ce vrai?
C'est quelque peu vrai ...
Voici ce que le langage standard a à dire sur la sécurité des threads:
[1.10/4]Deux évaluations d'expression conflit si l'une d'elles modifie un emplacement mémoire (1.7) et l'autre accède ou modifie le même emplacement mémoire.
[1.10/21]L'exécution d'un programme contient un course de données s'il contient deux actions conflictuelles dans des threads différents, dont au moins un n'est pas atomique , et ni l'un ni l'autre ne se produit avant l'autre. Une telle course de données entraîne un comportement indéfini.
ce qui n'est rien d'autre que la condition suffisante pour qu'un course de données se produise:
La bibliothèque standard s'appuie sur cela, en allant un peu plus loin:
[17.6.5.9/1]Cette section spécifie les exigences que les implémentations doivent respecter pour empêcher les courses de données (1.10). Chaque fonction de bibliothèque standard doit répondre à chaque exigence, sauf indication contraire. Les implémentations peuvent empêcher les courses de données dans des cas autres que ceux spécifiés ci-dessous.
[17.6.5.9/3]Une fonction de bibliothèque standard C++ ne doit pas modifier directement ou indirectement des objets (1.10) accessibles par des threads autres que le thread courant, sauf si les objets sont accessibles directement ou indirectement via les arguments non - const de la fonction, y compris
this
.
qui, en termes simples, indique qu'il s'attend à ce que les opérations sur les objets const
soient thread-safe. Cela signifie que la bibliothèque standard n'introduira pas de course de données tant que les opérations sur les objets const
de vos propres types non plus
Si cette attente ne s'applique pas à l'un de vos types, son utilisation directe ou indirecte avec n'importe quel composant de la bibliothèque standard peut entraîner une course de données. En conclusion, const
signifie thread-safe du point de vue Standard Library. Il est important de noter qu'il s'agit simplement d'un contrat et qu'il ne sera pas appliqué par le compilateur, si vous le cassez, vous obtenez comportement indéfini et vous êtes seul. Que const
soit présent ou non n'affectera pas la génération de code - du moins pas en ce qui concerne courses de données--.
Cela signifie-t-il que
const
est désormais l'équivalent desynchronized
de Java)?
Non . Pas du tout...
Considérez la classe trop simplifiée suivante représentant un rectangle:
class rect {
int width = 0, height = 0;
public:
/*...*/
void set_size( int new_width, int new_height ) {
width = new_width;
height = new_height;
}
int area() const {
return width * height;
}
};
Fonction membrearea
est thread-safe; non pas parce que son const
, mais parce qu'il consiste entièrement en opérations de lecture. Aucune écriture n'est impliquée et au moins une écriture est nécessaire pour qu'une course de données se produise. Cela signifie que vous pouvez appeler area
depuis autant de threads que vous le souhaitez et vous obtiendrez des résultats corrects tout le temps.
Notez que cela ne signifie pas que rect
est thread-safe. En fait, il est facile de voir comment un appel à area
devait se produire en même temps qu'un appel à set_size
sur un rect
donné, puis area
pourrait finir par calculer son résultat sur la base d'une ancienne largeur et d'une nouvelle hauteur (ou même sur des valeurs tronquées).
Mais ça va, rect
n'est pas const
donc on ne s'attend même pas à ce qu'il soit thread-safe après tout. Un objet déclaré const rect
, en revanche, serait thread-safe car aucune écriture n'est possible (et si vous envisagez const_cast
- quelque chose à l'origine déclaré const
alors vous obtenez comportement indéfini et c'est tout).
Alors qu'est-ce que cela signifie alors?
Supposons - pour les besoins de l'argumentation - que les opérations de multiplication sont extrêmement coûteuses et que nous les évitons mieux lorsque cela est possible. Nous pourrions calculer la zone uniquement si elle est demandée, puis la mettre en cache au cas où elle serait demandée à l'avenir:
class rect {
int width = 0, height = 0;
mutable int cached_area = 0;
mutable bool cached_area_valid = true;
public:
/*...*/
void set_size( int new_width, int new_height ) {
cached_area_valid = ( width == new_width && height == new_height );
width = new_width;
height = new_height;
}
int area() const {
if( !cached_area_valid ) {
cached_area = width;
cached_area *= height;
cached_area_valid = true;
}
return cached_area;
}
};
[Si cet exemple semble trop artificiel, vous pouvez remplacer mentalement int
par un très grand entier alloué dynamiquement qui est intrinsèquement non thread-safe et pour lequel les multiplications sont extrêmement cher.]
Fonction membrearea
n'est plus thread-safe, il effectue des écritures maintenant et n'est pas synchronisé en interne. C'est un problème? L'appel à area
peut se produire dans le cadre d'un constructeur de copie d'un autre objet, tel constructeur aurait pu être appelé par une opération sur un conteneur standard, et à ce stade, la bibliothèque standard s'attend à ce que cette opération se comporte comme une lecture en ce qui concerne courses de données. Mais nous faisons des écritures!
Dès que nous mettons un rect
dans un conteneur standard - directement ou indirectement-- nous entrons un contrat avec le bibliothèque standard . Pour continuer à écrire dans une fonction const
tout en respectant ce contrat, nous devons synchroniser ces écritures en interne:
class rect {
int width = 0, height = 0;
mutable std::mutex cache_mutex;
mutable int cached_area = 0;
mutable bool cached_area_valid = true;
public:
/*...*/
void set_size( int new_width, int new_height ) {
if( new_width != width || new_height != height )
{
std::lock_guard< std::mutex > guard( cache_mutex );
cached_area_valid = false;
}
width = new_width;
height = new_height;
}
int area() const {
std::lock_guard< std::mutex > guard( cache_mutex );
if( !cached_area_valid ) {
cached_area = width;
cached_area *= height;
cached_area_valid = true;
}
return cached_area;
}
};
Notez que nous avons créé la fonction area
thread-safe, mais que rect
n'est toujours pas thread-safe. Un appel à area
se produit en même temps qu'un appel à set_size
peut encore finir par calculer la mauvaise valeur, car les affectations à width
et height
ne sont pas protégées par le mutex.
Si nous voulions vraiment un thread-saferect
, nous utiliserions une primitive de synchronisation pour protéger le non thread-saferect
.
Manquent-ils de mots-clés?
Oui, ils sont. Ils manquent de mots-clés depuis le premier jour.
Source: Vous ne connaissez pas const
et mutable
- Herb Sutter