Supposons qu’il existe une classe contenant un champ public de compteur int accessible par plusieurs threads, et ce nombre ne fera qu’augmenter ou diminuer.
Lors de l’ajout de ce champ, lequel des schémas suivants doit être utilisé, et pourquoi ?
- lock(this.locker) this.counter++ ;
- Interlocked.Increment(cf this.counter) ;
- Changez le modificateur d’accès de compteur en volatile public
Pire (aucun ne fonctionne réellement)
Changez le modificateur d’accès de compteur en volatile public
Cette approche n’est en réalité pas du tout sécurisée, et le point avec le volatile est que plusieurs threads fonctionnant sur plusieurs CPU mettent en mémoire tampon les données et réarrangent les instructions exécutées.
Si elle est non volatile, lorsque le CPU A augmente d’une valeur, le CPU B doit attendre un certain temps pour voir cette valeur augmentée, ce qui peut entraîner des problèmes.
Si c’est volatil, cela garantit que les deux CPU voient la même valeur en même temps. Mais cela n’empêche pas les opérations de lecture et d’écriture transversales.
Ajouter de la valeur à une variable prend en réalité trois étapes
1. Lecture, 2. Ajouter 3. Écrire
Supposons que le thread A lit la valeur du compteur comme 1 et ne soit pas prêt à augmenter, alors le thread B lit également la valeur du compteur comme 1, et les deux threads commencent alors à effectuer des opérations incrémentales et d’écriture. La valeur du compteur final est de 2. Ce n’est pas correct, les deux threads ont effectué une opération d’augmentation, et le résultat correct devrait être 3. Donc le qualifier de volatil est tout simplement dangereux.
C’est mieux
lock(this.locker) this.counter++ ;
De cette façon, c’est sûr (n’oubliez pas de verrouiller partout où vous voulez accéder à ce compteur, bien sûr). Cela empêche tout autre thread d’exécuter le code verrouillé. Et cela évite aussi le problème de séquençage d’instructions multi-CPU mentionné plus haut. Le problème, c’est que le verrou est lent en performance, et si vous utilisez le verrou à d’autres endroits sans rapport, il peut bloquer vos autres threads.
Meilleurs
Interlocked.Increment(cf this.counter) ;
C’est sûr et très efficace. Il effectue la lecture, l’augmentation, l’écriture de trois opérations dans un atome sans être interrompu au milieu. Comme cela n’affecte pas d’autres codes, vous n’avez pas besoin de vous souvenir des serrures ailleurs. Et c’est aussi très rapide (comme le dit MSDN, sur les processeurs actuels, ce n’est souvent qu’une instruction).
Mais je ne suis pas tout à fait sûr que cela puisse aussi résoudre le problème d’ordre des instructions du CPU, ou s’il doit être utilisé en combinaison avec volatile et cet incrément.
Supplément : Quels problèmes les volatils résout-ils bien ?
Puisque le volatile ne peut pas empêcher le multithreading, que peut-il faire ? Un bon exemple est que vous avez deux threads, l’un écrit toujours sur une variable, disons que cette variable est queneLength, et l’autre lit toujours les données de cette variable. Si la longueur de file d’attente n’est pas volatile, le thread A peut être lu 5 fois, mais le thread B peut voir des données retardées, voire dans le mauvais ordre. Une solution est d’utiliser la serrure, mais dans ce cas, vous pouvez aussi utiliser volatile. Cela garantit que le thread B voit toujours les dernières données écrites par le thread A, mais cette logique ne fonctionne que si vous ne les lisez pas lors de l’écriture, et si vous ne l’écrivez pas lors de la lecture. Une fois que plusieurs threads veulent effectuer des opérations de lecture-modification-écriture, il faut utiliser Interlock ou lock.
|