Să presupunem că există o clasă care conține un câmp public int counter accesibil de mai multe fire de execuție, iar acest număr va crește sau scădea doar.
La adăugarea acestui câmp, care dintre următoarele scheme ar trebui folosite și de ce?
- lock(this.locker) this.counter++;
- Interlocked.Increment(ref this.counter);
- Schimbă modificatorul de acces al contorului la volatil public
Cel mai rău (niciunul dintre ele nu funcționează cu adevărat)
Schimbă modificatorul de acces al contorului la volatil public
Această abordare nu este deloc sigură, iar ideea despre volatile este că mai multe fire care rulează pe mai multe procesoare stochează datele și rearanjează instrucțiunile executate.
Dacă este non-volatil, când CPU-ul A crește cu o valoare, CPU-ul B trebuie să aștepte ceva timp pentru a vedea valoarea crescută, ceea ce poate duce la probleme.
Dacă este volatil, asigură că ambele procesoare văd aceeași valoare în același timp. Dar nu evită operațiunile de citire și scriere transversale.
Adăugarea de valoare unei variabile implică de fapt trei pași
1. Citit, 2. Adaugă 3. Scrie
Să presupunem că firul A citește valoarea contorului ca 1 și nu este pregătit să crească, atunci firul B citește de asemenea valoarea contorului ca 1, iar ambele fire încep să efectueze operații incrementale și de scriere. Valoarea contorului final este 2. Acest lucru nu este corect, ambele fire au făcut o operație de creștere, iar rezultatul corect ar trebui să fie 3. Așadar, etichetarea lor ca fiind volatilă este pur și simplu nesigură.
E mai bine
lock(this.locker) this.counter++;
Astfel este sigur (amintește-ți să încuii oriunde vrei să accesezi acest cont, desigur). Împiedică orice alt fir de execuție codul blocat. Și previne și problema secvențierii instrucțiunilor multi-CPU menționată mai sus. Problema este că blocarea are performanță lentă, iar dacă folosești blocarea în alte locuri fără legătură, poate bloca celelalte fire de execuție.
Cele mai bune
Interlocked.Increment(ref this.counter);
Este sigur și foarte eficient. Efectuează citire, creștere, scriere a trei operații într-un atom fără a fi întrerupt la mijloc. Pentru că nu afectează alte coduri, nu trebuie să-ți amintești lacătele din alte părți. Și este, de asemenea, foarte rapid (așa cum spune MSDN, pe procesoarele de azi, este adesea doar o instrucțiune).
Dar nu sunt complet sigur dacă poate rezolva și problema ordinii instrucțiunilor a CPU-ului sau dacă trebuie folosit împreună cu volatile și această creștere.
Supliment: Ce probleme rezolvă bine volatilele?
Deoarece volatilul nu poate preveni multithreading-ul, ce poate face? Un exemplu bun este că ai două fire, unul scrie întotdeauna pe o variabilă, să zicem că această variabilă este queneLength, iar celălalt citește întotdeauna datele de la această variabilă. Dacă lungimea cozii nu este volatilă, firul A poate citi de 5 ori, dar firul B poate vedea date întârziate sau chiar date în ordinea greșită. O soluție este să folosești blocare, dar în acest caz poți folosi și volatile. Acest lucru asigură că firul B vede întotdeauna cele mai recente date scrise de firul A, dar această logică funcționează doar dacă nu le citești când le scrii și dacă nu le scrii când le citești. Odată ce mai multe fire vor să facă operații de citire-modificare-scriere, trebuie să folosești Interlocked sau lock.
|