Febbraio 2019

Volume 34 Numero 2

Il presente articolo è stato tradotto automaticamente.

[C#]

Riduci al minimo la complessità nel codice C# con multithreading

By Thomas Hansen | February 2019

Fork o programmazione multithreading, sono tra i risultati più difficili da ottenere corretto durante la programmazione. Ciò è dovuto loro natura parallela, che richiede una prospettiva completamente diversa dalla programmazione lineare con un singolo thread. Una valida analogia per il problema è un giocoliere, chi deve mantenere sfere più in modalità wireless senza prevedere negativamente interferiscono tra loro. È una sfida importante. Tuttavia, con gli strumenti giusti e prepararli, è semplice da gestire.

In questo articolo approfondiscono alcuni degli strumenti di cui ho messo a punto per semplificare la programmazione multithreading e per evitare problemi, ad esempio le condizioni di competizione, deadlock e altri problemi. La toolchain dipende, in effetti zucchero sintattico e delegati magic. Tuttavia, per citare il musicista jazz eccezionale Miles Davis, "La musica, inattività è più importante dell'audio". La magia avviene tra il rumore.

In altre parole, non è necessariamente su ciò che puoi scrivere codice, ma ciò che è possibile scegliere di non, poiché si desidera creare un po' di magic tra le righe. Un'offerta da Bill Gates viene in mente: "Per misurare la qualità del lavoro in base al numero di righe di codice, è come misurare la qualità di un aereo per il rispettivo peso." Quindi, invece di insegnamento delle modalità per creare il codice più, spero che consentono di meno codice.

La richiesta di sincronizzazione

Il primo problema che si incontrano con la programmazione multithreading è la sincronizzazione dell'accesso a una risorsa condivisa. Problemi si verificano quando due o più thread condividere l'accesso a un oggetto e sia potenzialmente potrebbe provare a modificare l'oggetto nello stesso momento. Quando C# è stato inizialmente rilasciato, l'istruzione lock implementato un modo semplice per garantire che solo un thread è stato possibile accedere a una risorsa specificata, ad esempio un file di dati e ha funzionato correttamente. La parola chiave lock di C# viene così facili da comprendere che gestendo autonomamente rivoluzionato il modo in cui abbiamo pensato di informazioni su questo problema.

Tuttavia, un semplice blocco presenta un grave difetto: Accesso in sola lettura non discriminazione dall'accesso in scrittura. Ad esempio, potrebbe essere 10 diversi thread che desidera leggere da un oggetto condiviso e questi thread possono essere assegnati l'accesso simultaneo all'istanza senza causare problemi tramite la classe ReaderWriterLockSlim nello spazio dei nomi System. Threading. A differenza dell'istruzione di blocco, questa classe consente di specificare se il codice è la scrittura all'oggetto o semplicemente la lettura dall'oggetto. Ciò consente di accedere più lettori allo stesso tempo, ma impedisce qualsiasi accesso al codice di scrittura fino a tutti gli altri in lettura e di thread di scrittura vengono eseguite esegue esattamente.

A questo punto il problema: La sintassi quando si utilizza la classe ReaderWriterLock questa operazione risulta noiosa, con una grande quantità di codice ripetitivo che riduce la leggibilità e rende più difficile manutenzione nel tempo e il codice diventa spesso sparsi con try più blocchi e finally. Un semplice errore di digitazione può inoltre generare effetti disastrosi che in alcuni casi sono estremamente difficili da individuare in un secondo momento. 

Incapsulando il ReaderWriterLockSlim in una classe semplice, improvvisamente risolve il problema senza codice ripetitivo, riducendo al contempo il rischio che un errore di digitazione secondaria verrà potrebbero alterare il giorno. La classe, come illustrato nella figura 1, è interamente basato su trucco lambda. È probabilmente sufficiente zucchero sintattico intorno dei delegati, presupponendo la presenza di due interfacce. Più importanti, consentono di rendere il codice molto più DRY (ad esempio, "t Repeat Yourself").

Figura 1 che incapsula ReaderWriterLockSlim

public class Synchronizer<TImpl, TIRead, TIWrite> where TImpl : TIWrite, TIRead
{
  ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
  TImpl _shared;

  public Synchronizer(TImpl shared)
  {
    _shared = shared;
  }

  public void Read(Action<TIRead> functor)
  {
    _lock.EnterReadLock();
    try {
      functor(_shared);
    } finally {
      _lock.ExitReadLock();
    }
  }

  public void Write(Action<TIWrite> functor)
  {
    _lock.EnterWriteLock();
    try {
      functor(_shared);
    } finally {
      _lock.ExitWriteLock();
    }
  }
}

Sono disponibili solo 27 righe di codice nel figura 1, offrendo un modo elegante e conciso per garantire che gli oggetti vengono sincronizzate tra più thread. La classe si presuppone un'interfaccia per la lettura e un'interfaccia di scrittura per il tipo. È possibile anche usarlo ripetendo la classe modello stesso tre volte, se per qualche motivo non è possibile modificare l'implementazione della classe sottostante a cui è necessario sincronizzare l'accesso. Utilizzo di base potrebbe essere simile a quello illustrato nella figura 2.

Figura 2 usando la classe di programma di sincronizzazione

interface IReadFromShared
{
  string GetValue();
}

interface IWriteToShared
{
  void SetValue(string value);
}

class MySharedClass : IReadFromShared, IWriteToShared
{
  string _foo;

  public string GetValue()
  {
    return _foo;
  }

  public void SetValue(string value)
  {
    _foo = value;
  }
}

void Foo(Synchronizer<MySharedClass, IReadFromShared, IWriteToShared> sync)
{
  sync.Write(x => {
    x.SetValue("new value");
  });
  sync.Read(x => {
    Console.WriteLine(x.GetValue());
  })
}

Nel codice figura 2, indipendentemente dal numero di thread è in esecuzione del metodo, Foo nessuna scrittura fino a quando un'altra lettura o scrittura dal metodo, viene richiamato metodo. Tuttavia, più metodi di lettura possono essere richiamati contemporaneamente, senza la necessità di separazione del codice con più istruzioni try/catch/finally o ripetuti più volte lo stesso codice. Per il record, all'utilizzo dello stesso con una stringa semplice è significativo, poiché System. String non è modificabile. Usare un oggetto stringa semplice qui per semplificare l'esempio.

L'idea di base è che tutti i metodi che è possono modificare lo stato dell'istanza devono essere aggiunta all'interfaccia IWriteToShared. Allo stesso tempo, solo leggere dall'istanza di tutti i metodi devono essere aggiunti all'interfaccia IReadFromShared. Per separare le problematiche simile al seguente in due interfacce distinte e implementare entrambe le interfacce per il tipo sottostante, è quindi possibile utilizzare la classe alla sincronizzazione per sincronizzare l'accesso all'istanza. In modo analogo, l'arte della sincronizzazione dell'accesso al codice dei diventa molto più semplice ed è possibile farlo per la maggior parte in modo molto più dichiarativo.

Quando si tratta di multithreading zucchero sintattico, programmazione potrebbe essere la differenza tra l'esito positivo e negativo. Debug di codice multithread è spesso molto difficile, e la creazione di unit test per gli oggetti di sincronizzazione può essere un esercizio di futility.

Se si desidera, è possibile creare un tipo di overload con un solo argomento generico, ereditando dalla classe sincronizzatore originale e passando sul relativo singolo argomento generico come argomento di tipo tre volte per la classe di base. In questo modo, non è necessario le interfacce di lettura o scrittura, poiché è possibile utilizzare semplicemente l'implementazione concreta del tipo in uso. Questo approccio richiede tuttavia manualmente estrema attenzione tali parti che devono usare il metodo scrittura o lettura. È inoltre leggermente meno sicura, ma consentono di eseguire il wrapping in un'istanza di gateway di sincronizzazione non è possibile modificare le classi.

Lambda raccolte per i fork

Dopo aver eseguito il primo esegue il bello delle espressioni lambda (o delegati, come vengono chiamati C#), non è difficile immaginare che è possibile eseguire altre con essi. Ad esempio, un tema ricorrente comune nel multithreading consiste nell'avere più thread di raggiungere gli altri server per recuperare i dati e restituire i dati al chiamante.

L'esempio più elementare potrebbe essere un'applicazione che legge dati da 20 pagine Web e, quando completata restituisce il codice HTML a un singolo thread che crea una sorta di risultato aggregato in base al contenuto di tutte le pagine. A meno che non si crea un thread per ognuno dei metodi di recupero, questo codice sarà molto più lento desiderato, ovvero il 99% del tempo di esecuzione totale verrebbe probabilmente impiegato nell'attesa della richiesta HTTP da restituire.

L'esecuzione di questo codice in un singolo thread è inefficiente e la sintassi per la creazione di un thread è difficile fare in modo corretto. Strutture composite sfida quando si supportano più thread e i relativi oggetti supervisore, imporre agli sviluppatori da ripetere se stessi durante la scrittura del codice. Una volta capito che è possibile creare una raccolta di delegati e una classe per eseguirne il wrapping, è quindi possibile creare tutti i thread con una chiamata al metodo singolo. Creazione di thread in modo analogo, diventa molto meno più complesso.

Nelle figura 3 si troverà un frammento di codice che crea due tali espressioni lambda che vengono eseguiti in parallelo. Si noti che questo codice è effettivamente dagli unit test del primo rilascio del linguaggio di scripting Lizzie, che trova in bit.ly/2FfH5y8.

Figura 3 Creazione le espressioni lambda

public void ExecuteParallel_1()
{
  var sync = new Synchronizer<string, string, string>("initial_");

  var actions = new Actions();
  actions.Add(() => sync.Assign((res) => res + "foo"));
  actions.Add(() => sync.Assign((res) => res + "bar"));

  actions.ExecuteParallel();

  string result = null;
  sync.Read(delegate (string val) { result = val; });
  Assert.AreEqual(true, "initial_foobar" == result || result == "initial_barfoo");
}

Se si esamina attentamente questo codice, si noterà che il risultato della valutazione non presume che prima l'altro è in esecuzione uno dei personali le espressioni lambda. L'ordine di esecuzione non è specificato in modo esplicito e queste espressioni lambda vengono eseguite su thread separati. Infatti, le azioni di classe nel figura 3consente di aggiungere i delegati a esso, è possibile stabilire in un secondo momento se si desidera eseguire i delegati in parallelo o in modo sequenziale.

A tale scopo, è necessario creare una serie di espressioni lambda ed eseguirli usando il meccanismo preferito. È possibile vedere la classe sincronizzatore menzionata in precedenza in figura 3, la sincronizzazione dell'accesso alla risorsa stringa condiviso. Tuttavia, Usa un nuovo metodo nel programma di sincronizzazione, denominato assegnare, che non ho incluso nell'elenco in figura 1 per la classe di questa impostazione predefinita. Il metodo di assegnazione Usa la stessa "lambda trucco" che descritto in precedenza nei metodi Write e Read.

Se si desidera esaminare l'implementazione della classe di azioni, si noti che è importante scaricare la versione 0.1 di Lizzie, come riscritto completamente il codice per diventare un autonomo programming language nelle versioni successive.

Il linguaggio di programmazione funzionaleC#

La maggior parte degli sviluppatori tendono a pensare a C# come quasi sinonimi di, o almeno strettamente correlate a, programmazione a oggetti (OOP), e ovviamente è. Tuttavia, se rethinking come si utilizzano C#, e per approfondire gli aspetti funzionali, diventa molto più semplice risolvere alcuni problemi. OOP nella sua forma attuale è semplicemente non molto riutilizzare descrittivo e molti il motivo sono che è fortemente tipizzata.

Riutilizza una singola classe forza, ad esempio, è possibile riusare ogni singola classe che fa riferimento la classe iniziale, sia quelli utilizzati tramite la composizione e tramite l'ereditarietà. Riutilizzo di classe forza, inoltre, è possibile riutilizzare tutte le classi che fanno riferimento a queste classi di terze parti e così via. E se queste classi vengono implementate in assembly diversi, è necessario includere un'ampia gamma di assembly è sufficiente effettuare l'accesso a un singolo metodo su un singolo tipo.

Ho letto una sola volta un'analogia che illustra questo problema: "Si desidera una banana, ma si otterrà un gorilla, che include una banana e le foreste in cui si trova il gorilla". Confronta questa situazione con il riutilizzo in un linguaggio più dinamico, ad esempio JavaScript, che non si occupa di tipo, purché implementa le funzioni che delle funzioni fanno uso. Un approccio leggermente più debolmente tipizzato produce codice più flessibile e più facilmente riutilizzato. I delegati consentono di eseguire questa operazione.

È possibile usare C# in modo che migliora il riutilizzo del codice tra più progetti. È sufficiente tenere presente che una funzione o un delegato può anche essere un oggetto e che sia possibile modificare le raccolte di questi oggetti in modo debole tipizzato.

Le idee per i delegati presente in questo articolo create su quelle articolato in un precedente articolo ho scritto, "Creare il proprio Script Language con simbolico delegati," nel numero di novembre 2018 di MSDN Magazine (msdn.com/magazine/mt830373). Questo articolo sono stati introdotti Lizzie, il linguaggio di scripting di homebrew dovuto l'esistenza di questo modello mentale incentrato sul delegato. Se avevo creato Lizzie usando regole OOP, secondo me è che potrebbe essere presente almeno un ordine di grandezza di dimensioni maggiori.

Naturalmente, OOP e la tipizzazione forte è in una posizione tale dominante subito che è praticamente impossibile trovare una descrizione del processo che non fosse ancora capito come le competenze necessarie primaria. Per il record, ho creato codice OOP per più di 25 anni, in modo che ho lavorato come responsabile come tutti gli utenti di una distorsione fortemente tipizzata. Oggi, tuttavia, sono più pragmatic nella mio approccio alla scrittura di codice e minore interessato nel modo in cui la gerarchia di classi termina.

Non è che non per essere venuto una gerarchia di classi interessanti, ma esistono riduttivi. Le altre classi aggiunge a una gerarchia, meno elegante diventa, fino a quando non viene compressa in un proprio peso. In alcuni casi, la progettazione di livello superiore include alcuni metodi, classi di un numero minore e principalmente a regime di funzioni, che consente il codice essere facilmente esteso, senza la necessità di "portare le gorilla e foreste del".

È possibile tornare al tema ricorrente di questo articolo, traendo ispirazione dall'approccio di Miles Davis musica, in cui minore è più e "inattività è più importante dell'audio". Il codice è uguale a questo, troppo. Il magic si trova spesso tra le righe e le migliori soluzioni possono venire misurate più da ciò che non di codice, invece di operazioni. Qualsiasi principiante possibile se la fanno scappare un tromba e rumori, ma alcuni possono creare musica da quest'ultimo. E un valore più basso può rendere magic, il modo in cui è stato miglia.


Thomas Hansen opera nel settore della Tecnofinanza e ForEx come sviluppatore software e si trova in Cipro.


Discutere di questo articolo nel forum di MSDN Magazine