2017-04-05 1 views
1

Dans un article sur A scalable reader/writer scheme with optimistic retry il est un exemple de code:C# Est-il possible de réécrire les instructions d'un bloc finally dans try block?

using System; 
using System.Threading; 

public class OptimisticSynchronizer 
{ 
    private volatile int m_version1; 
    private volatile int m_version2; 

    public void BeforeWrite() { 
     ++m_version1; 
    } 

    public void AfterWrite() { 
     ++m_version2; 
    } 

    public ReadMark GetReadMark() { 
     return new ReadMark(this, m_version2); 
    } 

    public struct ReadMark 
    { 
     private OptimisticSynchronizer m_sync; 
     private int m_version; 

     internal ReadMark(OptimisticSynchronizer sync, int version) { 
      m_sync = sync; 
      m_version = version; 
     } 

     public bool IsValid { 
      get { return m_sync.m_version1 == m_version; } 
     } 
    } 

    public void DoWrite(Action writer) { 
     BeforeWrite(); 
     try { 
      writer(); // this is inlined, method call just for example 
     } finally { 
      AfterWrite(); 
     } 
    } 

    public T DoRead<T>(Func<T> reader) { 
     T value = default(T); 

     SpinWait sw = new SpinWait(); 
     while (true) { 
      ReadMark mark = GetReadMark(); 

      value = reader(); 

      if (mark.IsValid) { 
       break; 
      } 

      sw.SpinOnce(); 
     } 

     return value; 
    } 
} 

Si je fais m_version1 et m_version2non volatile mais utilise le code:

public void DoWrite(Action writer) { 
    Thread.MemoryBarrier(); // always there, acquiring write lock with Interlocked method 
    Volatile.Write(ref m_version1, m_version1 + 1); // NB we are inside a writer lock, atomic increment is not needed 
    try { 
     writer(); 
    } finally { 
     // is a barrier needed here to avoid the increment reordered with writer instructions? 
     // Volatile.Write(ref m_version2, m_version2 + 1); // is this needed instead of the next line? 
     m_version2 = m_version2 + 1; // NB we are inside a writer lock, atomic increment is not needed 
     Thread.MemoryBarrier(); // always there, releasing write lock with Interlocked method 
    } 
} 

Peut-instructions de la ligne m_version2 = m_version2 + 1 être réorganisées à partir de finally en try bloquer? Il est important qu'un écrivain finisse avant que m_version2 ne soit incrémenté.

Logiquement finally est exécuté après try, mais le bloc finally n'est pas mentionné dans le list of implicit memory barriers. Il serait assez déroutant si les instructions de finally pouvaient être déplacées avant celles de try, mais les optimisations CPU au niveau des instructions sont toujours une magie noire pour moi. Je pourrais mettre Thread.MemoryBarrier(); avant la ligne m_version2 = m_version2 + 1 (ou utiliser Volatile.Write), mais la question est si cela est vraiment nécessaire?

Les MemoryBarrier présentés dans l'exemple sont implicites et générés par les méthodes Interlocked d'un verrou d'écriture, de sorte qu'ils sont toujours présents. Le danger est qu'un lecteur puisse voir m_version2 incrémenté avant la fin de l'écrivain.

+0

J'ai lu http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-335.pdf après avoir posté ceci. Il ressemble à Volatile.Write est nécessaire, il n'y a pas de traitement spécial de 'finally' (section I.12.6-7). A moins d'avoir manqué quelque chose de très subtil à propos des CERs. –

Répondre

1

Je ne ai rien trouvé dans la spécification qui limiteront, donc je l'ai vérifié avec un dispositif de CPU ARM (en utilisant Xamarin, doivent vérifier sur le Core CLR) ...
fil One exécutait cette Code:

try 
{ 
    person = new Person(); 
} 
finally 
{ 
    isFinallyExecuted = true; 
} 

Et le second fil attendait isFinallyExecuted être true avec ce code:

while (!Volatile.Read(ref isFinallyExecuted)) 
    ; 

Ensuite, le second fil w en exécutant le code suivant:

if (!person.IsInitialized()) 
{ 
    failCount++; 
    Log.Error("m08pvv", $"Reordered from finally: {failCount}, ok: {okCount}"); 
} 
else 
{ 
    okCount++; 
} 

IsInitialized méthode vérifie que tous les champs sont correctement setted, de sorte qu'il retourne false pour les objets partiellement construits.

C'est ce que je suis arrivé dans le journal:

12-25 17: 00: 55,294: E/m08pvv (11592): Réordonnancement de finalement: 48, ok: 12-25 17 : 00: 56.750: E/m08pvv (11592): Réorganisé à partir de enfin: 49, ok: 686534
12-25 17: 00: 56.830: E/m08pvv (11592): Réorganisé à partir de: 50, ok: 686821
12-25 17: 00: 57.310: E/m08pvv (11592): Réorganisé à partir de la fin: 51, ok: 688002
12-25 17: 01: 12.191: E/m08pvv (11592): Réordonné à partir de la fin: 52, ok: 12-25 17: 01: 12.708: E/m08pvv (11592): Réorganisé à partir de enfin: 53, ok: 735338
12-25 17:01:13.722: E/m08pvv (11592): finalement commandée chez: 54, ok: 738839
12-25 17: 01: 25,240: E/m08pvv (11592): enfin commandée chez: 55, ok: 775645

Cela signifie que pour 775645 exécutions réussies de ce code, 55 fois j'ai obtenu isFinallyExecuted est égal à trueet objet partiellement construit. C'est possible parce que je n'utilise pas Volatile.Write pour stocker new Person() ou volatile mot-clé sur un person. Donc, si vous avez des courses de données, vous les affronterez.