2009-07-25 32 views
26

Tôi đang sử dụng Thư viện Doanh nghiệp 4 trên một trong các dự án của tôi để ghi nhật ký (và các mục đích khác). Tôi đã nhận thấy rằng có một số chi phí cho việc ghi nhật ký mà tôi đang làm mà tôi có thể giảm thiểu bằng cách ghi nhật ký trên một chuỗi riêng biệt.Cách đăng nhập có hiệu quả không đồng bộ?

Cách tôi đang làm điều này bây giờ là tôi tạo ra một đối tượng LogEntry và sau đó tôi gọi BeginInvoke trên một đại biểu gọi Logger.Write.

new Action<LogEntry>(Logger.Write).BeginInvoke(le, null, null); 

Điều tôi thực sự muốn làm là thêm thông điệp tường trình vào hàng đợi và sau đó có một chuỗi kéo các đối tượng LogEntry ra khỏi hàng đợi và thực hiện thao tác nhật ký. Lợi ích của việc này là việc ghi nhật ký không can thiệp vào hoạt động thực thi và không phải mọi thao tác ghi nhật ký đều dẫn đến công việc bị ném vào nhóm luồng.

Làm cách nào để tạo hàng đợi được chia sẻ hỗ trợ nhiều người viết và một người đọc theo cách an toàn? Một số ví dụ về việc thực hiện hàng đợi được thiết kế để hỗ trợ nhiều người viết (không gây ra đồng bộ/chặn) và một người đọc duy nhất sẽ được đánh giá cao.

Đề xuất về phương pháp tiếp cận thay thế cũng sẽ được đánh giá cao, tôi không quan tâm đến việc thay đổi khung đăng nhập.

+0

@spoon, tôi đã thêm một phiên bản được cải thiện một chút, hãy nhớ rằng bạn sẽ phải thử nghiệm nó rất nhiều trước khi sử dụng nó, vì tôi chỉ cần gõ nó lên. –

+1

Mặc dù không áp dụng trực tiếp cho câu hỏi này (vì nó đang nói EntLib 4), Thư viện doanh nghiệp 6 hiện hỗ trợ Ghi nhật ký không đồng bộ ra khỏi hộp. –

Trả lời

28

Tôi đã viết mã này một thời gian trở lại, vui lòng sử dụng nó.

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

Một số ưu điểm:

  • Nó giữ các logger nền sống, vì vậy nó không cần phải quay lên và quay xuống chủ đề.
  • Nó sử dụng một chuỗi duy nhất để phục vụ hàng đợi, có nghĩa là sẽ không bao giờ có một tình huống mà 100 chủ đề đang phục vụ hàng đợi.
  • Nó sao chép hàng đợi để đảm bảo hàng đợi không bị chặn trong khi các hoạt động đăng nhập được thực hiện
  • Nó sử dụng một AutoResetEvent để đảm bảo sợi bg đang trong tình trạng chờ đợi
  • Đó là, IMHO, rất dễ dàng để làm theo

Đây là phiên bản được cải thiện một chút, hãy nhớ rằng tôi đã thực hiện rất ít thử nghiệm trên nó, nhưng nó giải quyết một số vấn đề nhỏ.

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

Ưu điểm so với bản gốc:

  • Đó là dùng một lần, vì vậy bạn có thể thoát khỏi logger async
  • Ngữ nghĩa tuôn ra được cải thiện
  • Nó sẽ đáp ứng tốt hơn một chút đến sự bùng nổ sau bởi im lặng
+2

Cờ "chờ" của bạn sẽ dễ bay hơi nếu nó được sử dụng từ nhiều luồng - hoặc sử dụng khóa khi truy cập. Tôi không chắc tôi thấy điểm chờ đợi trong 10 giây thay vì chỉ chờ đợi mà không có thời gian chờ. Tôi cũng gọi Set trong khi bạn vẫn giữ khóa - nó sẽ không thực sự tạo ra nhiều khác biệt, nhưng bạn có thể nhận được hai luồng cả hai sự kiện thêm, sau đó một chủ đề thiết lập sự kiện, người đọc hoàn tất việc chờ và sao chép dữ liệu, * sau đó * người viết khác lại đặt lại sự kiện. Không có tổn thất lớn, thừa nhận. Tại sao bạn có biến đối tượng loggingThread? –

+1

10 giây chờ đợi, là để tránh đói, trong trường hợp bạn nhận được một loạt các sự kiện được phục vụ một phần. (nhận sự kiện ngay sau khi hàng đợi được sao chép) Tôi có lẽ nên thay đổi điều này thành sự kiện đặt lại thủ công và có thể thêm một số logic để phục vụ lại hàng đợi cho đến khi thực sự trống, trước khi chờ đợi lại. Các chủ đề var chỉ là một chút nhỏ trong tương lai bằng chứng đó là không thực sự cần thiết (trong trường hợp tôi muốn nhận được id của thread hoặc một cái gì đó). –

+0

sửa đổi mã để giải thích các nhận xét của Jon, Cảm ơn! –

0

Mức độ gián tiếp bổ sung có thể trợ giúp tại đây.

Cuộc gọi phương thức async đầu tiên của bạn có thể đặt thư vào hàng đợi được đồng bộ hóa và đặt sự kiện - vì vậy các khóa đang diễn ra trong nhóm chủ đề chứ không phải trên chuỗi công việc của bạn - và sau đó có một chuỗi khác kéo thư ra khỏi xếp hàng khi sự kiện được nâng lên.

0

Nếu bạn đăng nhập nội dung nào đó trên một chuỗi riêng biệt, thư có thể không được viết nếu ứng dụng gặp sự cố, điều này làm cho ứng dụng trở nên vô dụng.

Lý do tại sao bạn nên luôn luôn tuôn ra sau mỗi mục nhập bằng văn bản.

+0

Đó là sự thật, tôi không sử dụng phương pháp này để ghi nhật ký ngoại lệ hoặc ghi nhật ký quan trọng khác. Tôi chỉ sử dụng nó để ghi nhật ký thông báo trạng thái thông tin mô tả các hoạt động đã thực hiện thành công. Những thông điệp này có xu hướng xảy ra thường xuyên hơn và tôi cần phải giảm thiểu chi phí của chúng. –

9

Có, bạn cần một nhà sản xuất/hàng tiêu dùng. Tôi có một ví dụ về điều này trong hướng dẫn luồng của tôi - nếu bạn xem trang "deadlocks/monitor methods" của mình, bạn sẽ tìm thấy mã trong nửa thứ hai.

Có rất nhiều ví dụ khác trực tuyến, tất nhiên - và .NET 4.0 sẽ xuất xưởng cùng với một trong khung làm việc (thay vì hoàn toàn nổi bật hơn của tôi!). Trong .NET 4.0 bạn có thể quấn ConcurrentQueue<T> trong một BlockingCollection<T>.

Phiên bản trên trang đó không phải là chung chung (được viết dài thời gian trước đây) nhưng bạn có thể muốn làm cho nó chung chung - nó sẽ là tầm thường để làm.

Bạn sẽ gọi số Produce từ mỗi chuỗi "bình thường" và Consume từ một chuỗi, chỉ lặp vòng và ghi lại bất kỳ thứ gì tiêu thụ. Nó có lẽ là dễ nhất chỉ để làm cho người tiêu dùng thread một chủ đề nền, vì vậy bạn không cần phải lo lắng về "dừng" hàng đợi khi ứng dụng của bạn thoát. Điều đó có nghĩa là có một khả năng từ xa thiếu mục nhập nhật ký cuối cùng (nếu đó là một nửa thông qua viết nó khi ứng dụng thoát) - hoặc thậm chí nhiều hơn nếu bạn đang sản xuất nhanh hơn nó có thể tiêu thụ/log.

+0

Bạn có thể liên kết đến tài liệu .NET 4 mô tả điều này không? –

+0

@ spoon16: Xong. –

+0

cảm ơn, đánh giá cao những suy nghĩ bổ sung cũng như –

0

Nếu những gì bạn có trong tâm trí là một hàng đợi được chia sẻ, sau đó tôi nghĩ bạn sẽ phải đồng bộ hóa ghi vào nó, đẩy và bật.

Nhưng, tôi vẫn nghĩ rằng nó đáng để nhắm vào thiết kế hàng đợi được chia sẻ.So với IO ghi nhật ký và có thể so với công việc khác mà ứng dụng của bạn đang thực hiện, số lượng chặn ngắn cho các lần đẩy và các lần xuất hiện có thể sẽ không đáng kể.

2

Tôi đề nghị để bắt đầu với đo lường tác động hiệu quả thực tế của khai thác gỗ trên toàn bộ hệ thống (tức là bằng cách chạy profiler) và tùy chọn chuyển sang một cái gì đó nhanh như log4net (cá nhân tôi đã di cư đến nó từ EntLib đăng nhập một thời gian dài trước đây).

Nếu điều này không làm việc, bạn có thể thử sử dụng phương pháp đơn giản này từ .NET Framework:

ThreadPool.QueueUserWorkItem 

Queues một phương pháp để thực hiện. Phương thức này thực hiện khi một luồng thread thread trở nên khả dụng.

MSDN Details

Nếu điều này không làm việc, hoặc sau đó bạn có thể dùng đến một cái gì đó giống như John Skeet đã đề nghị và thực sự mã hóa khuôn khổ async khai thác gỗ cho mình.

+1

Tôi đã xác định rằng có lợi ích cho việc ghi nhật ký không đồng bộ. Ngoài ra tôi không ở trong một vị trí để chuyển đổi khung công tác ghi nhật ký khi chúng tôi sử dụng ghi nhật ký EntLib rộng rãi. Nó sẽ không phải là tầm thường hoặc một sự thay đổi đột phá chào đón bởi phần còn lại của đội :). ThreadPool.QueueUserWorkItem về cơ bản là những gì các cuộc gọi BeginInvoke mà tôi đang sử dụng bây giờ không. Tôi nghĩ câu trả lời của John là câu trả lời thích hợp nhất cho đến nay. –

+1

Việc bỏ một mục công việc không đảm bảo bất kỳ thứ tự nào, vì vậy nếu bạn quan tâm đến thứ tự các thông điệp được ghi lại thì đó sẽ không phải là một cách tiếp cận khả thi IMO. –

2

Đây là những gì tôi đã đưa ra ... cũng xem câu trả lời của Sam Saffron. Câu trả lời này là wiki cộng đồng trong trường hợp có bất kỳ vấn đề nào mà mọi người nhìn thấy trong mã và muốn cập nhật.

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

Dường như bạn định làm cho lớp AsyncLogger thành một singleton. Tôi muốn làm cho các nhà xây dựng tư nhân trong trường hợp đó, nhưng nếu không có vẻ như nó được công việc làm. –

+0

cảm ơn, bắt tốt –

1

Để trả lời bài đăng của Sam Safrons, tôi muốn gọi xả và đảm bảo rằng erything đã thực sự hoàn thành writting.Trong trường hợp của tôi, tôi đang viết cho một cơ sở dữ liệu trong chuỗi xếp hàng và tất cả các sự kiện đăng nhập của tôi đã được xếp hàng đợi nhưng đôi khi ứng dụng đã dừng trước khi mọi thứ đã hoàn thành bằng văn bản, điều này không thể chấp nhận được trong tình huống của tôi. Tôi đã thay đổi một số đoạn mã của bạn nhưng điều chính tôi muốn chia sẻ là tuôn ra:

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

Tôi hy vọng rằng sẽ tiết kiệm cho một số người thất vọng. Nó đặc biệt rõ ràng trong quá trình song song ghi lại rất nhiều dữ liệu.

Cảm ơn bạn đã chia sẻ giải pháp của mình, nó giúp tôi trở thành một hướng đi tốt!

--Johnny S

1

Tôi muốn nói rằng bài đăng trước đó của tôi là vô dụng. Bạn chỉ có thể đặt AutoFlush thành true và bạn sẽ không phải lặp qua tất cả các trình nghe. Tuy nhiên, tôi vẫn còn có vấn đề điên rồ với các chủ đề song song cố gắng để tuôn ra các logger. Tôi phải tạo một boolean khác được đặt thành true trong quá trình sao chép hàng đợi và thực thi LogEntry viết và sau đó trong chế độ xả, tôi phải kiểm tra boolean đó để chắc chắn rằng một cái gì đó chưa có trong hàng đợi và không có gì được xử lý Trước khi trở về.

Bây giờ nhiều chủ đề song song có thể nhấn vào điều này và khi tôi gọi tuôn ra, tôi biết nó thực sự đỏ ửng.

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

Chào mừng bạn đến với Stack Overflow! Chúng tôi không hoạt động như một diễn đàn truyền thống ở đây. Nếu bạn có bản cập nhật, bạn có thể (và nên) chỉnh sửa bài đăng gốc của mình bằng thông tin/chỉnh sửa mới thay vì thêm câu trả lời mới. – Pops

1

Chỉ cần một bản cập nhật:

Sử dụng Xí nghiệp thư viện 5.0 với .NET 4.0 nó có thể dễ dàng được thực hiện bằng cách:

static public void LogMessageAsync(LogEntry logEntry) 
{ 
    Task.Factory.StartNew(() => LogMessage(logEntry)); 
} 

Xem: http://randypaulo.wordpress.com/2011/07/28/c-enterprise-library-asynchronous-logging/

+2

Điều này thêm một chủ đề cho mỗi tin nhắn đăng nhập * có thể * rất tốn kém và phản tác dụng. Như @ spoon16 được đề cập trong câu hỏi, xếp hàng được viết theo cách hàng loạt. –

+0

@ M.Babcock khi bạn đang nói điều này thêm một chủ đề cho mỗi thông điệp tường trình, bạn có nghĩa là chủ đề mới? – Mukund

+0

@Mukund - Có, Nếu khai thác gỗ được tìm thấy mất hơn 200-300 chu kỳ CPU, nó chắc chắn nhất được thực hiện trong một chuỗi mới. – Lalman

Các vấn đề liên quan