c# - ¿Cómo iniciar sesión de manera asincrónica?
multithreading logging (10)
En respuesta a la publicación de Sam Safrons, quería llamar a color y asegurarme de que todo estaba realmente terminado de escribir. En mi caso, estoy escribiendo en una base de datos en el hilo de cola y todos mis eventos de registro se pusieron en cola, pero a veces la aplicación se detuvo antes de que todo se terminara de escribir, lo que no es aceptable en mi situación. Cambié varios fragmentos de tu código, pero lo principal que quería compartir era el color:
public static void FlushLogs()
{
bool queueHasValues = true;
while (queueHasValues)
{
//wait for the current iteration to complete
m_waitingThreadEvent.WaitOne();
lock (m_loggerQueueSync)
{
queueHasValues = m_loggerQueue.Count > 0;
}
}
//force MEL to flush all its listeners
foreach (MEL.LogSource logSource in MEL.Logger.Writer.TraceSources.Values)
{
foreach (TraceListener listener in logSource.Listeners)
{
listener.Flush();
}
}
}
Espero que eso le ahorre a alguien algo de frustración. Es especialmente evidente en procesos paralelos que registran muchos datos.
Gracias por compartir tu solución, ¡me puso en una buena dirección!
- Johnny S
Estoy utilizando Enterprise Library 4 en uno de mis proyectos para el registro (y otros fines). Me he dado cuenta de que hay un cierto costo para el registro que estoy haciendo que puedo mitigar al hacer el registro en un hilo separado.
La forma en que lo hago ahora es crear un objeto LogEntry y luego invocar a BeginInvoke en un delegado que llama a Logger.Write.
new Action<LogEntry>(Logger.Write).BeginInvoke(le, null, null);
Lo que realmente me gustaría hacer es agregar el mensaje de registro a una cola y luego tener un único hilo que quite las instancias LogEntry de la cola y realice la operación de registro. El beneficio de esto sería que el registro no está interfiriendo con la operación de ejecución y no todas las operaciones de registro dan como resultado que se genere un trabajo en el grupo de subprocesos.
¿Cómo puedo crear una cola compartida que admita muchos escritores y un lector de una manera segura? Algunos ejemplos de una implementación de cola diseñada para admitir muchos escritores (sin causar sincronización / bloqueo) y un solo lector serían realmente apreciados.
La recomendación con respecto a los enfoques alternativos también se apreciaría, aunque no estoy interesado en cambiar los marcos de registro.
Escribí este código hace un tiempo, no dude en usarlo.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace MediaBrowser.Library.Logging {
public abstract class ThreadedLogger : LoggerBase {
Queue<Action> queue = new Queue<Action>();
AutoResetEvent hasNewItems = new AutoResetEvent(false);
volatile bool waiting = false;
public ThreadedLogger() : base() {
Thread loggingThread = new Thread(new ThreadStart(ProcessQueue));
loggingThread.IsBackground = true;
loggingThread.Start();
}
void ProcessQueue() {
while (true) {
waiting = true;
hasNewItems.WaitOne(10000,true);
waiting = false;
Queue<Action> queueCopy;
lock (queue) {
queueCopy = new Queue<Action>(queue);
queue.Clear();
}
foreach (var log in queueCopy) {
log();
}
}
}
public override void LogMessage(LogRow row) {
lock (queue) {
queue.Enqueue(() => AsyncLogMessage(row));
}
hasNewItems.Set();
}
protected abstract void AsyncLogMessage(LogRow row);
public override void Flush() {
while (!waiting) {
Thread.Sleep(1);
}
}
}
}
Algunas ventajas:
- Mantiene el registrador de fondo vivo, por lo que no necesita girar y girar los hilos.
- Utiliza un único hilo para dar servicio a la cola, lo que significa que nunca habrá una situación en la que 100 hilos estén dando servicio a la cola.
- Copia las colas para garantizar que la cola no se bloquee mientras se realiza la operación de registro
- Utiliza un AutoResetEvent para garantizar que el subproceso bg esté en estado de espera
- Es, en mi humilde opinión, muy fácil de seguir
Aquí hay una versión ligeramente mejorada, tenga en cuenta que realicé muy pocas pruebas al respecto, pero soluciona algunos problemas menores.
public abstract class ThreadedLogger : IDisposable {
Queue<Action> queue = new Queue<Action>();
ManualResetEvent hasNewItems = new ManualResetEvent(false);
ManualResetEvent terminate = new ManualResetEvent(false);
ManualResetEvent waiting = new ManualResetEvent(false);
Thread loggingThread;
public ThreadedLogger() {
loggingThread = new Thread(new ThreadStart(ProcessQueue));
loggingThread.IsBackground = true;
// this is performed from a bg thread, to ensure the queue is serviced from a single thread
loggingThread.Start();
}
void ProcessQueue() {
while (true) {
waiting.Set();
int i = ManualResetEvent.WaitAny(new WaitHandle[] { hasNewItems, terminate });
// terminate was signaled
if (i == 1) return;
hasNewItems.Reset();
waiting.Reset();
Queue<Action> queueCopy;
lock (queue) {
queueCopy = new Queue<Action>(queue);
queue.Clear();
}
foreach (var log in queueCopy) {
log();
}
}
}
public void LogMessage(LogRow row) {
lock (queue) {
queue.Enqueue(() => AsyncLogMessage(row));
}
hasNewItems.Set();
}
protected abstract void AsyncLogMessage(LogRow row);
public void Flush() {
waiting.WaitOne();
}
public void Dispose() {
terminate.Set();
loggingThread.Join();
}
}
Ventajas sobre el original:
- Es desechable, por lo que puede deshacerse del registrador asíncrono
- La semántica de color se mejora
- Responderá un poco mejor a una ráfaga seguida de silencio
Esto es lo que se me ocurrió ... también veo la respuesta de Sam Saffron. Esta respuesta es wiki de la comunidad en caso de que haya algún problema que las personas vean en el código y deseen actualizar.
/// <summary>
/// A singleton queue that manages writing log entries to the different logging sources (Enterprise Library Logging) off the executing thread.
/// This queue ensures that log entries are written in the order that they were executed and that logging is only utilizing one thread (backgroundworker) at any given time.
/// </summary>
public class AsyncLoggerQueue
{
//create singleton instance of logger queue
public static AsyncLoggerQueue Current = new AsyncLoggerQueue();
private static readonly object logEntryQueueLock = new object();
private Queue<LogEntry> _LogEntryQueue = new Queue<LogEntry>();
private BackgroundWorker _Logger = new BackgroundWorker();
private AsyncLoggerQueue()
{
//configure background worker
_Logger.WorkerSupportsCancellation = false;
_Logger.DoWork += new DoWorkEventHandler(_Logger_DoWork);
}
public void Enqueue(LogEntry le)
{
//lock during write
lock (logEntryQueueLock)
{
_LogEntryQueue.Enqueue(le);
//while locked check to see if the BW is running, if not start it
if (!_Logger.IsBusy)
_Logger.RunWorkerAsync();
}
}
private void _Logger_DoWork(object sender, DoWorkEventArgs e)
{
while (true)
{
LogEntry le = null;
bool skipEmptyCheck = false;
lock (logEntryQueueLock)
{
if (_LogEntryQueue.Count <= 0) //if queue is empty than BW is done
return;
else if (_LogEntryQueue.Count > 1) //if greater than 1 we can skip checking to see if anything has been enqueued during the logging operation
skipEmptyCheck = true;
//dequeue the LogEntry that will be written to the log
le = _LogEntryQueue.Dequeue();
}
//pass LogEntry to Enterprise Library
Logger.Write(le);
if (skipEmptyCheck) //if LogEntryQueue.Count was > 1 before we wrote the last LogEntry we know to continue without double checking
{
lock (logEntryQueueLock)
{
if (_LogEntryQueue.Count <= 0) //if queue is still empty than BW is done
return;
}
}
}
}
}
Quería decir que mi publicación anterior fue algo inútil. Simplemente puede establecer AutoFlush en verdadero y no tendrá que recorrer todos los oyentes. Sin embargo, todavía tenía un gran problema con los hilos paralelos que intentaban descargar el registrador. Tuve que crear otro booleano que se estableció en verdadero durante la copia de la cola y ejecutar las escrituras LogEntry y luego en la rutina flush tuve que verificar ese booleano para asegurarme de que algo no estaba ya en la cola y de que no se procesaba nada antes de volver.
Ahora, múltiples hilos en paralelo pueden golpear esta cosa y cuando llamo a Flush sé que realmente está enrojecido.
public static void FlushLogs()
{
int queueCount;
bool isProcessingLogs;
while (true)
{
//wait for the current iteration to complete
m_waitingThreadEvent.WaitOne();
//check to see if we are currently processing logs
lock (m_isProcessingLogsSync)
{
isProcessingLogs = m_isProcessingLogs;
}
//check to see if more events were added while the logger was processing the last batch
lock (m_loggerQueueSync)
{
queueCount = m_loggerQueue.Count;
}
if (queueCount == 0 && !isProcessingLogs)
break;
//since something is in the queue, reset the signal so we will not keep looping
Thread.Sleep(400);
}
}
Sí, necesita una cola de productor / consumidor. Tengo un ejemplo de esto en mi tutorial de subprocesos: si miras la página de "interbloqueos / métodos de monitoreo" encontrarás el código en la segunda mitad.
Hay muchos otros ejemplos en línea, por supuesto, y .NET 4.0 también incluirá uno en el marco (¡más completo que el mío!). En .NET 4.0 probablemente envolvería un ConcurrentQueue<T>
en un BlockingCollection<T>
.
La versión en esa página no es genérica (fue escrita hace mucho tiempo) pero probablemente quieras hacerla genérica, sería trivial.
Produce
a Produce
de cada hilo "normal" y Consume
desde un hilo, simplemente dando vueltas y registrando lo que sea que consuma. Probablemente sea más fácil simplemente hacer que el consumidor enhebre un hilo de fondo, para que no tenga que preocuparse por "detener" la cola cuando finalice su aplicación. Sin embargo, eso significa que existe la posibilidad remota de perder la entrada de registro final (si está a mitad de escribirla cuando la aplicación sale) o incluso más si está produciendo más rápido de lo que puede consumir / registrar.
Si lo que tienes en mente es una cola COMPARTIDA, entonces creo que vas a tener que sincronizar las escrituras, los empujes y los pops.
Pero, todavía creo que vale la pena apuntar al diseño de cola compartida. En comparación con el IO del registro y, probablemente, en comparación con el otro trabajo que está haciendo su aplicación, la breve cantidad de bloqueo para los empujes y los estallidos probablemente no será significativa.
Si registra algo en un hilo separado, el mensaje puede no escribirse si la aplicación falla, lo que lo hace bastante inútil.
La razón es por la que siempre debes enjuagar después de cada entrada escrita.
Solo una actualización:
El uso de la biblioteca enteprise 5.0 con .NET 4.0 se puede hacer fácilmente de la siguiente manera:
static public void LogMessageAsync(LogEntry logEntry)
{
Task.Factory.StartNew(() => LogMessage(logEntry));
}
Ver: http://randypaulo.wordpress.com/2011/07/28/c-enterprise-library-asynchronous-logging/
Sugiero comenzar por medir el impacto real en el rendimiento del inicio de sesión en el sistema general (es decir, ejecutando el generador de perfiles) y opcionalmente cambiar a algo más rápido como log4net (lo he migrado personalmente del registro EntLib hace mucho tiempo).
Si esto no funciona, puede intentar usar este método simple de .NET Framework:
ThreadPool.QueueUserWorkItem
Colas un método para la ejecución. El método se ejecuta cuando un hilo del grupo de subprocesos está disponible.
Si esto tampoco funciona, puede recurrir a algo como lo que John Skeet le ha ofrecido y, en realidad, codificar el marco de registro asincrónico.
Un nivel extra de indirección puede ayudar aquí.
La primera llamada al método asíncrono puede colocar mensajes en una Cola sincronizada y establecer un evento, para que los bloqueos ocurran en el grupo de subprocesos, no en los subprocesos de trabajo, y luego tener otro subproceso que saque los mensajes de la cola cuando el evento es elevado.