Pergunta

Aqui estou eu novamente com perguntas sobre multi-threading e um exercício da minha Programação Concorrente classe.

Eu tenho um servidor multi-threaded - implementado utilizando .NET Asynchronous Programming Modelo - com GET ( download ) e PUT ( Enviar ) serviços de arquivo. Esta parte é feito e testado.

Acontece que a declaração do problema diz que este servidor deve ter login atividade com o mínimo impacto sobre o tempo de resposta do servidor, e deve ser apoiada por uma baixa prioridade fio - fio logger - criado para este efeito. Todos Registro mensagens serão transmitidas pela tópicos que eles produzem a esta fio logger , usando um mecanismo de comunicação que não pode bloquear o fio que o invoca (além do bloqueio necessário para garantir a exclusão mútua) e assumindo que alguns registrar mensagens pode ser ignorado.

Aqui está a minha solução atual, por favor, ajuda validar se este se destaca como uma solução para o problema indicado:

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++; }

}

Basicamente, aqui estão os pontos principais deste código:

  1. Inicie uma baixa prioridade fio que circula o logging fila e impressões pendentes registros para a saída. Depois disso, o fio é suspenso até nova log chega;
  2. Quando um log chega, o segmento madeireiro é despertar e faz isso de trabalho.

É esta solução thread-safe ? Eu tenho lido Produtores-Consumidores problema ea solução algoritmo, mas neste problema, embora eu tenho vários produtores, eu só tenho um leitor.

Agradecemos antecipadamente por toda a sua atenção.

Foi útil?

Solução

Parece que deveria estar trabalhando. Produtores-consumidores não devem mudar muito em caso de único consumidor. Pequenos nitpicks:

  • aquisição de bloqueio pode ser uma operação cara (como diz @Vitaliy Lipchinsky). Eu recomendo para aferir o seu logger contra ingênuo 'write-through' operações interligadas logger e logger usando. Outra alternativa seria troca de fila existente com um vazio no GetLog e deixando seção crítica imediatamente. Desta forma, nenhum dos produtores não será bloqueado por operações longos nos consumidores.

  • fazer tipo de referência LogObj (classe). Não há nenhum ponto em fazê-lo struct desde que você está boxe de qualquer maneira. ou campo _queue fazer mais para ser do tipo LogObj[] (que é melhor de qualquer maneira).

  • fazer o seu fundo fio para que ele não vai impedir de fechar seu programa se Stop não será chamado.

  • Lave seu TextWriter. Ou então você está arriscando a perder até mesmo os registros que conseguiram fila de ajuste (10 ítens é um pequeno IMHO bit)

  • Implementar IDisposable e / ou finalizador. Seu logger possui fio e texto escritor e aqueles que deveriam ser libertados (e corada - veja acima).

Outras dicas

Ei lá. Tinha um olhar rápido, e enquanto parece ser thread-safe, eu não acredito que isso é particularmente ideal. Gostaria de sugerir uma solução ao longo destas linhas

NOTA: apenas ler as outras respostas. O que se segue é uma solução de bloqueio bastante ideal, otimista baseado em seu próprio país. As principais diferenças é o encaixe sobre uma classe interna, minimizando 'secções críticas', e fornecendo terminação rosca elegante. Se você quiser evitar o bloqueio por completo, então você pode experimentar alguns dos que volátil "sem bloqueio" coisas lista encadeada como @Vitaliy Lipchinsky sugere.

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;
}

Na verdade, você está introduzindo bloqueio aqui. Você bloqueio enquanto empurra uma entrada de registo para a fila (método Log): se 10 tópicos empurrada simultaneamente 10 itens em fila e acordou o fio Logger, em seguida, 11 segmento irá aguardar até que o segmento madeireiro registrar todos os itens ...

Se você quiser algo realmente escalável - implementar fila de livre-lock (exemplo é abaixo). Com mecanismo de sincronização fila de livre-lock será realmente imediatamente (você ainda pode usar identificador de espera único para notificações).

Se você não conseguir encontrar implementação da fila livre-lock na web, aqui está uma ideia de como fazer isso: Use lista ligada para uma implementação. Cada nó na lista vinculada contém um valor e uma referência volátil para o próximo nó. portanto, para as operações de enfileirar e retirar da fila que você pode usar o método Interlocked.CompareExchange. Espero que, a idéia é clara. Se não -. Deixe-me saber e eu vou fornecer mais detalhes

Eu estou apenas fazendo um experimento de pensamento aqui, já que eu não tenho tempo para realmente tentar código agora, mas acho que você pode fazer isso sem bloqueios em todos, se você for criativo.

Tenha sua classe de log contêm um método que aloca uma fila e um semáforo cada vez que ele é chamado (e outro que desaloca da fila e do semáforo quando o fio é feito). Os tópicos que querem fazer o registo vai chamar esse método quando eles começam. Quando eles querem fazer logon, eles empurram a mensagem para sua própria fila e definir o semáforo. O fio tem um registador de grande loop que atravessa as filas e as verificações semáforos associados. Se o semáforo associado à fila é maior do que zero, então a fila fica bateu fora e o semáforo diminuído.

Porque você não está tentando pop coisas fora da fila até que o semáforo está definido, e você não está definindo o semáforo até depois que você empurrou as coisas para a fila, eu pensar este será segura. De acordo com a documentação do MSDN para a classe fila, se você está enumerando a fila e outro segmento modifica a coleção, uma exceção é lançada. Capturar essa exceção e você deve ser bom.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top