2010-04-15 19 views
4

Supposons que nous avons le code suivant:multithreading .NET, le modèle et la mémoire volatile

class Program 
{ 
    static volatile bool flag1; 
    static volatile bool flag2; 
    static volatile int val; 
    static void Main(string[] args) 
    { 
     for (int i = 0; i < 10000 * 10000; i++) 
     { 
     if (i % 500000 == 0) 
     { 
      Console.WriteLine("{0:#,0}",i); 
     } 

     flag1 = false; 
     flag2 = false; 
     val = 0; 

     Parallel.Invoke(A1, A2); 

     if (val == 0) 
      throw new Exception(string.Format("{0:#,0}: {1}, {2}", i, flag1, flag2)); 
     } 
    } 

    static void A1() 
    { 
     flag2 = true; 
     if (flag1) 
     val = 1; 
    } 
    static void A2() 
    { 
     flag1 = true; 
     if (flag2) 
     val = 2; 
    } 
    } 
} 

C'est la faute! La principale question est pourquoi ... Je suppose que les opérations de réorganisation du processeur avec flag1 = true; et si l'instruction (flag2), mais les variables flag1 et flag2 marquées comme champs volatiles ...

Répondre

4

Dans le modèle de mémoire .NET, l'exécution (CLI) assurera que les modifications apportées aux champs volatiles ne sont pas mises en cache dans les registres, donc un changement sur n'importe quel thread est immédiatement vu sur d'autres threads (NB ce n'est pas vrai dans d'autres modèles de mémoire, y compris Java).

Mais cela ne dit rien sur l'ordre relatif des opérations entre plusieurs champs, volatiles ou non.

Pour assurer un classement cohérent sur plusieurs champs, vous devez utiliser un verrou (ou une barrière de mémoire, explicitement ou implicitement avec l'une des méthodes qui incluent une barrière de mémoire).

Pour plus de détails, voir "Concurrent Programming on Windows", Joe Duffy, AW, 2008

+1

Merci beaucoup! Mais dans l'article à propos de.modèle de mémoire net ("Comprendre l'impact des techniques à faible verrouillage dans les applications multithread" http://msdn.microsoft.com/en-us/magazine/cc163715.aspx) dans la section "Un modèle détendu: ECMA" nous pouvons voir , 1. Les lectures et écritures ne peuvent pas bouger avant une lecture volatile. 2. Les lectures et les écritures ne peuvent pas bouger après une écriture volatile. Est-ce que c'est faux? Je peux juste comprendre ... Peut être dans l'article signifie quelque chose d'autre? ... –

+0

+1 pour mentionner le livre - actuellement en train de le lire moi-même (déjà à mi-chemin ;-) –

+0

@ fedor-serdukov: Je me souviens peut-être - mais j'essaie d'être nettement plus sûr que nécessaire pour éviter un comportement inattendu (surtout après quelques changements de maintenance). De plus, je ne pense pas que votre code soit hors service pour que l'exception soit lancée. 'A1' et' A2' pourraient s'intercaler de sorte que les deux drapeaux soient positionnés avant que l'une ou l'autre condition soit vérifiée, en fait une fois que le pool de threads est lancé, je m'attendrais à ce que cela arrive de temps en temps sur un système multicœur. – Richard

0

spécification ECMA-335 dit:

Une lecture volatile a « acquérir la sémantique » ce qui signifie que la lecture est garanti de se produire avant toute référence à mémoire qui se produit après l'instruction de lecture dans la séquence d'instructions CIL. Une écriture volatile a « sémantique » libération sens que l'écriture est garanti de se produire après des références de mémoire avant l'écriture instruction dans la séquence d'instructions de CIL. Une implémentation conforme de l'interface CLI doit garantir cette sémantique des opérations volatiles. Cela garantit que tous les threads observeront les écritures volatiles effectuées par n'importe quel autre thread dans l'ordre dans lequel elles ont été exécutées. Mais une implémentation conforme est non requise pour fournir un ordre total unique des écritures volatiles comme vu de tous les threads d'exécution.

Tirons à quoi il ressemble:

enter image description here

Donc, nous avons deux demi-clôtures: une pour l'écriture volatile et une pour la lecture volatile. Et ils ne nous protègent pas de la réorganisation des instructions entre eux.
En outre, même sur une architecture aussi stricte comme AMD64 (x86-64) it is allowed stores to be reordered after loads.
Et pour d'autres architectures avec un modèle de mémoire matérielle plus faible, vous pouvez observer encore plus de choses amusantes. Sur ARM, vous pouvez obtenir un objet partiellement construit observé si la référence a été affectée de manière non volatile.

Pour fixer votre exemple, vous devez simplement mettre Thread.MemoryBarrier() appels entre l'affectation et si la clause:

static void A1() 
{ 
    flag2 = true; 
    Thread.MemoryBarrier(); 
    if (flag1) 
    val = 1; 
} 
static void A2() 
{ 
    flag1 = true; 
    Thread.MemoryBarrier(); 
    if (flag2) 
    val = 2; 
} 

Cela nous protège de réordonnancement de ces instructions en ajoutant-clôture pleine.