Domanda

Eccomi di nuovo con domande sul multi-threading e un esercizio della mia classe Programmazione simultanea .

Ho un server multi-thread - implementato usando .NET Modello di programmazione asincrona - con GET ( download ) e PUT servizi di file ( upload ). Questa parte è stata eseguita e testata.

Succede che la dichiarazione del problema dice che questo server deve avere attività di registrazione con il minimo impatto sul tempo di risposta del server e dovrebbe essere supportato da un thread - thread logger - creato per questo effetto. Tutti i messaggi di registrazione devono essere passati dai thread che li producono a questo thread del logger , usando un meccanismo di comunicazione che non può blocca il thread che lo richiama (oltre al blocco necessario per garantire l'esclusione reciproca) e presupponendo che alcuni messaggi di log possano essere ignorati.

Ecco la mia soluzione attuale, per favore aiutatemi a validare se questo rappresenta una soluzione al problema dichiarato:

using System;
using System.IO;
using System.Threading;

// Multi-threaded Logger
public class Logger {
    // textwriter to use as logging output
    protected readonly TextWriter _output;
    // logger thread
    protected Thread _loggerThread;
    // logger thread wait timeout
    protected int _timeOut = 500; //500ms
    // amount of log requests attended
    protected volatile int reqNr = 0;
    // logging queue
    protected readonly object[] _queue;
    protected struct LogObj {
        public DateTime _start;
        public string _msg;
        public LogObj(string msg) {
            _start = DateTime.Now;
            _msg = msg;
        }
        public LogObj(DateTime start, string msg) {
            _start = start;
            _msg = msg;
        }
        public override string ToString() {
            return String.Format("{0}: {1}", _start, _msg);
        }
    }

    public Logger(int dimension,TextWriter output) {
        /// initialize queue with parameterized dimension
        this._queue = new object[dimension];
        // initialize logging output
        this._output = output;
        // initialize logger thread
        Start();
    }
    public Logger() {
        // initialize queue with 10 positions
        this._queue = new object[10];
        // initialize logging output to use console output
        this._output = Console.Out;
        // initialize logger thread
        Start();
    }

    public void Log(string msg) {
        lock (this) {
            for (int i = 0; i < _queue.Length; i++) {
                // seek for the first available position on queue
                if (_queue[i] == null) {
                    // insert pending log into queue position
                    _queue[i] = new LogObj(DateTime.Now, msg);
                    // notify logger thread for a pending log on the queue
                    Monitor.Pulse(this);
                    break;
                }
                // if there aren't any available positions on logging queue, this
                // log is not considered and the thread returns
            }
        }
    }

    public void GetLog() {
        lock (this) {
            while(true) {
                for (int i = 0; i < _queue.Length; i++) {
                    // seek all occupied positions on queue (those who have logs)
                    if (_queue[i] != null) {
                        // log
                        LogObj obj = (LogObj)_queue[i];
                        // makes this position available
                        _queue[i] = null;
                        // print log into output stream
                        _output.WriteLine(String.Format("[Thread #{0} | {1}ms] {2}",
                                                        Thread.CurrentThread.ManagedThreadId,
                                                        DateTime.Now.Subtract(obj._start).TotalMilliseconds,
                                                        obj.ToString()));
                    }
                }
                // after printing all pending log's (or if there aren't any pending log's),
                // the thread waits until another log arrives
                //Monitor.Wait(this, _timeOut);
                Monitor.Wait(this);
            }
        }
    }

    // Starts logger thread activity
    public void Start() {
        // Create the thread object, passing in the Logger.Start method
        // via a ThreadStart delegate. This does not start the thread.
        _loggerThread = new Thread(this.GetLog);
        _loggerThread.Priority = ThreadPriority.Lowest;
        _loggerThread.Start();
    }

    // Stops logger thread activity
    public void Stop() {
        _loggerThread.Abort();
        _loggerThread = null;
    }

    // Increments number of attended log requests
    public void IncReq() { reqNr++; }

}

Fondamentalmente, ecco i punti principali di questo codice:

  1. Avvia un thread a bassa priorità che esegue il loop della coda di registrazione e stampa i log in sospeso sull'output. Successivamente, il thread viene sospeso fino all'arrivo del nuovo log ;
  2. Quando arriva un registro, il thread del logger viene risvegliato e funziona.

Questa soluzione è thread-safe ? Ho letto l'algoritmo di problema e soluzione produttori-consumatori , ma in questo problema, sebbene abbia più produttori, ho solo un lettore.

Grazie in anticipo per tutta l'attenzione.

È stato utile?

Soluzione

Sembra che dovrebbe funzionare. I produttori-consumatori non dovrebbero cambiare molto nel caso del singolo consumatore. Piccoli pignoli:

  • l'acquisizione del blocco può essere un'operazione costosa (come dice @Vitaliy Lipchinsky). Ti consiglierei di confrontare il tuo logger con ingenuo logger 'write-through' e logger usando operazioni interbloccate. Un'altra alternativa sarebbe lo scambio di una coda esistente con una vuota in GetLog e lasciare immediatamente la sezione critica. In questo modo nessuno dei produttori non sarà bloccato da lunghe operazioni nei consumatori.

  • crea il tipo di riferimento LogObj (classe). Non ha senso farlo strutturare dato che lo stai inscatolando comunque. oppure fai in modo che il campo _queue sia del tipo LogObj [] (è comunque meglio).

  • crea il tuo thread in background in modo che non impedisca la chiusura del programma se Stop non verrà chiamato.

  • Svuota il tuo TextWriter . Altrimenti stai rischiando di perdere anche quei record che sono riusciti ad adattarsi alla coda (10 elementi sono un po 'piccoli IMHO)

  • Implementa IDisposable e / o finalizzatore. Il tuo logger possiede thread e text writer e questi dovrebbero essere liberati (e svuotati - vedi sopra).

Altri suggerimenti

Ciao. Ho dato una rapida occhiata e mentre sembra essere thread-safe, non credo sia particolarmente ottimale. Suggerirei una soluzione in tal senso

NOTA: leggi le altre risposte. Quella che segue è una soluzione di bloccaggio abbastanza ottimale e ottimistica basata sulla tua. Le principali differenze sono il blocco su una classe interna, la riduzione al minimo delle "sezioni critiche" e la chiusura dei thread. Se vuoi evitare del tutto il blocco, puoi provare alcuni di quei "non bloccanti" volatili cose dell'elenco collegato come suggerisce @Vitaliy Lipchinsky.

using System.Collections.Generic;
using System.Linq;
using System.Threading;

...

public class Logger
{
    // BEST PRACTICE: private synchronization object. 
    // lock on _syncRoot - you should have one for each critical
    // section - to avoid locking on public 'this' instance
    private readonly object _syncRoot = new object ();

    // synchronization device for stopping our log thread.
    // initialized to unsignaled state - when set to signaled
    // we stop!
    private readonly AutoResetEvent _isStopping = 
        new AutoResetEvent (false);

    // use a Queue<>, cleaner and less error prone than
    // manipulating an array. btw, check your indexing
    // on your array queue, while starvation will not
    // occur in your full pass, ordering is not preserved
    private readonly Queue<LogObj> _queue = new Queue<LogObj>();

    ...

    public void Log (string message)
    {
        // you want to lock ONLY when absolutely necessary
        // which in this case is accessing the ONE resource
        // of _queue.
        lock (_syncRoot)
        {
            _queue.Enqueue (new LogObj (DateTime.Now, message));
        }
    }

    public void GetLog ()
    {
        // while not stopping
        // 
        // NOTE: _loggerThread is polling. to increase poll
        // interval, increase wait period. for a more event
        // driven approach, consider using another
        // AutoResetEvent at end of loop, and signal it
        // from Log() method above
        for (; !_isStopping.WaitOne(1); )
        {
            List<LogObj> logs = null;
            // again lock ONLY when you need to. because our log
            // operations may be time-intensive, we do not want
            // to block pessimistically. what we really want is 
            // to dequeue all available messages and release the
            // shared resource.
            lock (_syncRoot)
            {
                // copy messages for local scope processing!
                // 
                // NOTE: .Net3.5 extension method. if not available
                // logs = new List<LogObj> (_queue);
                logs = _queue.ToList ();
                // clear the queue for new messages
                _queue.Clear ();
                // release!
            }
            foreach (LogObj log in logs)
            {
                // do your thang
                ...
            }
        }
    }
}
...
public void Stop ()
{
    // graceful thread termination. give threads a chance!
    _isStopping.Set ();
    _loggerThread.Join (100);
    if (_loggerThread.IsAlive)
    {
        _loggerThread.Abort ();
    }
    _loggerThread = null;
}

In realtà, stai introducendo il blocco qui. Hai il blocco mentre spingi una voce di registro nella coda (metodo Log): se 10 thread hanno simultaneamente inserito 10 elementi in coda e hanno svegliato il thread Logger, l'11 ° thread attenderà fino a quando il thread del logger non registra tutti gli elementi ...

Se vuoi qualcosa di veramente scalabile, implementa la coda senza blocchi (l'esempio è sotto). Con il meccanismo di sincronizzazione della coda senza blocchi sarà davvero immediato (puoi anche usare la maniglia di attesa singola per le notifiche).

Se non riuscirai a trovare un'implementazione della coda senza blocco nel Web, ecco un'idea su come eseguire questa operazione: Utilizzare l'elenco collegato per un'implementazione. Ogni nodo nell'elenco collegato contiene un valore e un riferimento volatile al nodo successivo. pertanto per le operazioni di accodamento e svuotamento è possibile utilizzare il metodo Interlocked.CompareExchange. Spero che l'idea sia chiara. In caso contrario, fammi sapere e fornirò maggiori dettagli.

Sto solo facendo un esperimento mentale, dal momento che non ho tempo di provare il codice in questo momento, ma penso che puoi farlo senza blocchi se sei creativo.

Chiedi alla tua classe di registrazione di contenere un metodo che alloca una coda e un semaforo ogni volta che viene chiamato (e un altro che alloca la coda e il semaforo al termine del thread). I thread che desiderano eseguire la registrazione chiameranno questo metodo all'avvio. Quando vogliono accedere, inseriscono il messaggio nella propria coda e impostano il semaforo. Il thread del logger ha un grande ciclo che attraversa le code e controlla i semafori associati. Se il semaforo associato alla coda è maggiore di zero, la coda viene espulsa e il semaforo diminuisce.

Poiché non stai tentando di far uscire le cose dalla coda fino a dopo l'impostazione del semaforo e non stai impostando il semaforo fino a quando non hai inserito le cose nella coda, penso questo sarà sicuro. Secondo la documentazione MSDN per la classe della coda, se si sta enumerando la coda e un altro thread modifica la raccolta, viene generata un'eccezione. Cattura quell'eccezione e dovresti essere bravo.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top