Accesso ai dati condivisi senza blocco in TPL

2

Sto scrivendo una classe che contiene dati. Espone metodi che consentono di interrogare i dati, mentre i dati vengono anche aggiornati da una fonte esterna (servizio web, ad esempio).

Tutti i metodi espongono le attività avviate su un thread di lavoro utilizzando l'utilità di pianificazione predefinita. Quindi la struttura della classe è la seguente:

public class A
{
    private object _myLock;

    public Task<int> GetSomeNumber(int aParameter)
    {
        return Task.Run(() =>
        {
            lock (_myLock)
            { 
                int result = 0;
                // calculate the result...
                // ...
                return result;
            }
        });
    }

    public Task<string> GetSomeText(int aParameter)
    {
        return Task.Run(() =>
        {
            lock (_myLock)
            {
                string result = "";

                // calculate the result...
                // ...
                return result;
            }
        });
    }
}

I metodi possono essere chiamati più volte, mentre i dati stessi vengono aggiornati, quindi i calcoli che richiedono i dati condivisi sono circondati da un'istruzione di blocco. Il problema è che questo codice blocca il thread in cui viene eseguita l'attività. Naturalmente, questi non sono thread dell'interfaccia utente, quindi l'interfaccia utente rimane reattiva, ma consuma thread e li mantiene bloccati. Preferirei che i thread di lavoro nel pool di thread restassero il più possibile disponibili.

Esiste un modo, una best practice di qualche tipo, che permetta di scrivere questo codice in modo che i dati condivisi siano thread-safe e allo stesso tempo le attività non siano bloccate? Se invece di aspettare un lock, le attività dovrebbero aspettare che qualcun altro finisca (un'attività), vorrei usare ContinueWith per creare un'attività di continuazione e restituirla, in modo che fosse programmata per essere eseguita quando il primo compito è completato. C'è un modo per fare lo stesso con il blocco, in modo che l'attività sia pianificata solo quando il blocco è disponibile?

Aggiorna Ho fatto alcune letture e ho scoperto diverse possibili opzioni che consentono la sincronizzazione di dati condivisi. Questi includono: SemaphoreSlim , AsyncLock e ConcurrentExclusiveSchedulerPair .

Mi piacerebbe vedere una raccomandazione per una procedura ottimale, di scrivere una classe di servizio che fornisca metodi di attività "sola lettura" che possano essere eseguiti simultaneamente con un blocco di lettura e metodi di scrittura "write" che richiedono il blocco della scrittura.

    
posta Kobi Hari 16.02.2015 - 00:20
fonte

3 risposte

2

Questa risposta si basa sulle informazioni aggiuntive sul caso d'uso menzionato nei commenti.

We have a web service the provides graph nodes. We hold a local copy of part of the graph and expose and async API to query the graph. If the area that you query is in the cache then we simply calculate and return the result, otherwise we start downloading the missing part and then perform the calculation and return the result. There is also a recurring task in the background that polls the server for changes in the node and applies the changes to the local cache.

Ci si dovrebbe rendere conto che se una richiesta non può essere completata immediatamente perché i dati necessari per soddisfare la richiesta semplicemente non sono disponibili (ad esempio non sono stati scaricati o memorizzati nella cache), ci sono solo alcune scelte:

  • Restituisci subito un errore. È simile al modello TryGet , che non blocca, ma l'attività non avrà esito positivo.
  • Restituisce subito un errore, ma indica anche che ha avviato una richiesta asincrona per recuperare i dati dal server esterno. In questo modo, il lettore non bloccherà, ma dovrà riprovare in un secondo momento.
  • Richiede al lettore di fornire una funzione di callback, che verrà chiamata più tardi quando i dati saranno disponibili. Questo può essere combinato con l'approccio di richiesta asincrona.
  • Blocca il lettore finché i dati non sono stati recuperati.

Ciascuna delle opzioni di cui sopra ha il proprio insieme di "buone pratiche" e schemi di integrazione con TPL. Sarebbe troppo per discuterne tutti qui.

Aiuto necessario: se qualcuno è a conoscenza di una buona risorsa online che copre le migliori pratiche per queste opzioni, pubblica i tuoi commenti.

Se decidi che bloccare il lettore è l'unica scelta data i requisiti della tua applicazione, tieni presente che:

  • Il pool di thread predefinito di .NET è elastico, ovvero avvierà più thread di lavoro se rileva che alcuni thread vengono bloccati. Questo è buono. Significa che il blocco di un numero qualsiasi di lettori non provocherà un errore a livello di sistema.
  • Se non è elastico, l'applicazione potrebbe bloccarsi a causa della fame di thread di lavoro (esaurimento).
  • Devi quindi mai modificare le impostazioni sul pool di thread predefinito che causerà la perdita dell'elasticità, a meno che tu non voglia riavviare il servizio ogni volta che ciò accade.
    • Ad esempio, non impostare un numero massimo di thread di lavoro.

Se vuoi che i lettori concorrenti non si blocchino mai (essendo privi di ostacoli), penso che tu abbia diverse scelte:

Buffering multiplo

Link all'articolo di Wikipedia

  • Il sistema conserva più copie del grafico e le ruota (ciclicamente) tutte le volte che è necessaria una modifica.
  • In qualsiasi momento, al massimo una copia verrà modificata. Tutte le altre copie saranno di sola lettura.
  • Quando si verifica una modifica, ogni lettore deve scegliere tra:
    • Lettura da una delle altre copie di sola lettura, leggermente obsolete del grafico. Più lettori possono accedervi contemporaneamente.
    • Attendi fino al completamento della modifica, per assicurarti che abbia accesso alla versione più aggiornata del grafico.

Algoritmi di aggiornamento grafico simultaneo

I dettagli esatti dipenderanno da quali operazioni e algoritmi sono necessari per l'applicazione.

Alcuni di essi potrebbero essere privi di blocco, utilizzando le istruzioni atomiche abilitate all'hardware. Altri potrebbero utilizzare vari tipi di blocchi, ma potrebbero cercare di minimizzare la probabilità o il periodo di tempo in cui i lettori potrebbero essere bloccati.

Si può usare una libreria esistente o implementare la propria.

Non ho familiarità con gli algoritmi di aggiornamento grafico simultanei, quindi non posso dare molti suggerimenti.

    
risposta data 21.03.2015 - 08:17
fonte
0

Che ne dici di:

// any child object should freeze when this object freezes...
interface IFreazableCloanable
{
    bool IsFrozen { get; }
    // Any further attempts to change the object should throw an Exception
    void Freeze();

    // Creates mutable Clone of the object
    IFreazableCloanable DeepCloneMutable();
}

class SyncedObject<T> where T : class, IFreazableCloanable
{
    SemaphoreSlim _writersLock = new SemaphoreSlim(0, 1);
    volatile T _internal;

    public SyncedObject(T thing) // if writes are unsynced then only the last one counts.
    {
        _internal = (T)thing.DeepCloneMutable();
    }
    public T Read()
    {
        T result = _internal;

        result.Freeze();

        return result;
    }

    public void Write(Func<T, T> writer)
    {
        _writersLock.Wait();

        T temp = (T)_internal.DeepCloneMutable();

        temp = writer(temp);

        _internal = temp;

        _writersLock.Release();
    }
}

class Data : IFreazableCloanable
{
    private string _data = "lala";
    public string MyData
    {
        get
        {
            return _data;
        }
        set
        {
            if (IsFrozen)
            {
                throw new Exception("Very Bad");
            }
            _data = value;
        }
    }
    public bool IsFrozen
    {
        get;
        private set;
    }

    public void Freeze()
    {
        IsFrozen = true;
    }

    public IFreazableCloanable DeepCloneMutable()
    {
        return new Data();
    }
}
    
risposta data 16.02.2015 - 19:29
fonte
0

Se ti aspettavi più letture di quanto scriva, una scommessa generalmente sicura è ReaderWriterLockSlim che consente ai thread concorrenti di leggere i dati, ma solo un thread può essere uno scrittore alla volta.

Hai menzionato elenchi e dizionari concorrenti, esiste una ragione per cui ConcurrentDictionary <, > non funziona per questa situazione? Ho trovato abbastanza performante e molto meno ingombrante da affrontare nella maggior parte delle situazioni.

Per alcune soluzioni critiche per le prestazioni Interlocked.CompareExcgange funziona abbastanza bene, ma questo dipende dai dati che devono essere sincronizzati, CompareExchange funziona bene con tipi numerici, ma vorrei evitare variabili basate su riferimenti.

    
risposta data 18.02.2015 - 21:24
fonte

Leggi altre domande sui tag