2015-08-16 24 views
5

Bối cảnh

Một khách hàng đã yêu cầu tôi tìm hiểu lý do tại sao ứng dụng C# của họ (chúng tôi gọi XXX là do một nhà tư vấn đã bỏ chạy) rất lung lay và sửa nó. Ứng dụng điều khiển thiết bị đo lường qua kết nối nối tiếp. Đôi khi thiết bị cung cấp các kết quả đọc liên tục (được hiển thị trên màn hình) và đôi khi ứng dụng cần dừng các phép đo liên tục và chuyển sang chế độ phản hồi lệnh.C# - Làm thế nào để loại bỏ chủ đề nào đọc từ cổng nối tiếp?

Làm thế nào KHÔNG để làm điều đó

Đối với các phép đo liên tục, sử dụng XXX System.Timers.Timer cho xử lý nền của đầu vào nối tiếp. Khi bộ đếm thời gian kích hoạt, C# chạy bộ đếm thời gian ElapsedEventHandler của bộ hẹn giờ bằng cách sử dụng một số luồng từ nhóm của nó. Trình xử lý sự kiện của XXX sử dụng chặn commPort.ReadLine() với thời gian chờ một vài giây, sau đó gọi lại cho người được ủy quyền khi một phép đo hữu ích đến trên cổng nối tiếp. Tuy nhiên, phần này hoạt động tốt, tuy nhiên ...

Khi thời gian dừng đo thời gian thực và yêu cầu thiết bị thực hiện điều gì đó khác, ứng dụng sẽ cố tạm dừng xử lý nền từ luồng GUI bằng cách đặt Enabled = false của bộ hẹn giờ. Tất nhiên, điều đó chỉ thiết lập một cờ ngăn chặn các sự kiện tiếp theo, và một luồng nền đã chờ đợi cho đầu vào nối tiếp tiếp tục chờ đợi. Chủ đề GUI sau đó gửi một lệnh tới thiết bị và cố gắng đọc trả lời - nhưng câu trả lời được nhận bởi chuỗi nền. Bây giờ các chủ đề nền trở nên bối rối vì nó không phải là đo lường dự kiến. Các thread GUI trong khi đó trở nên bối rối vì nó không nhận được câu trả lời lệnh dự kiến. Bây giờ chúng tôi biết lý do tại sao XXX rất dễ hỏng.

thể Phương pháp 1

Trong một ứng dụng tương tự, tôi đã sử dụng một sợi System.ComponentModel.BackgroundWorker cho phép đo tự chạy. Đình chỉ xử lý nền tôi đã làm hai điều trong thread GUI:

  1. gọi các CancelAsync phương pháp trên thread, và
  2. gọi commPort.DiscardInBuffer(), gây ra một cấp phát (bị chặn, chờ đợi) cư đọc trong thread nền ném một số System.IO.IOException "The I/O operation has been aborted because of either a thread exit or an application request.\r\n".

Trong chủ đề nền, tôi bắt ngoại lệ này và dọn sạch kịp thời và tất cả hoạt động như dự định. Thật không may DiscardInBuffer kích hoạt ngoại lệ trong đọc chặn của một luồng khác không phải là hành vi được ghi lại ở bất cứ nơi nào tôi có thể tìm thấy và tôi ghét dựa vào hành vi không có giấy tờ. Nó hoạt động vì nội bộ DiscardInBuffer gọi API Win32 PurgeComm, làm gián đoạn việc chặn đọc (hành vi được ghi lại).

thể Phương pháp 2

Trực tiếp sử dụng phương pháp BaseClass Stream.ReadAsync, với một mã thông báo màn hủy, sử dụng một cách hỗ trợ gián đoạn nền IO.

Bởi vì số lượng ký tự được nhận là biến (chấm dứt bởi một dòng mới), và không có phương pháp ReadAsyncLine tồn tại trong khuôn khổ, tôi không biết nếu điều này là có thể. Tôi có thể xử lý từng ký tự riêng lẻ nhưng sẽ có hiệu năng (có thể không hoạt động trên các máy chậm, trừ khi tất nhiên bit kết thúc dòng đã được triển khai trong C# trong khung công tác).

Phương thức có thể 3

Tạo khóa "Tôi có cổng nối tiếp". Không ai đọc, viết hoặc loại bỏ đầu vào từ cổng trừ khi họ có khóa (bao gồm lặp lại việc chặn đọc trong chủ đề nền). Cắt các giá trị thời gian chờ trong chuỗi nền đến 1/4 giây để có thể chấp nhận được hiệu ứng GUI mà không cần quá nhiều chi phí.

Câu hỏi

Có ai có giải pháp đã được chứng minh để giải quyết vấn đề này không? Làm cách nào để có thể ngừng xử lý nền của cổng nối tiếp? Tôi đã googled và đọc hàng chục bài viết bemoaning C# SerialPort lớp học, nhưng đã không tìm thấy một giải pháp tốt.

Cảm ơn trước!

+0

Bạn chưa tập trung vào các vấn đề thực sự, nó là System.Timers.Timer. Loại bỏ nó và sử dụng một bộ đếm thời gian đồng bộ thay thế. –

+0

Xin lỗi Hans, tôi không theo. Không có phương pháp nào có thể 1-3 sử dụng System.Timers.Timer; Bạn gợi ý gì? –

Trả lời

2

MSDN bài viết cho SerialPort Lớp nêu rõ:

Nếu một đối tượng SerialPort bị tắc nghẽn trong một hoạt động đọc, không hủy bỏ thread. Thay vào đó, hãy đóng luồng cơ sở hoặc vứt bỏ đối tượng SerialPort.

Vì vậy, cách tiếp cận tốt nhất, từ quan điểm của tôi, là thứ hai, với async đọc và từng bước kiểm tra ký tự kết thúc dòng. Như bạn đã nói, kiểm tra cho mỗi char là mất hiệu suất rất lớn, tôi đề nghị bạn điều tra các ReadLine implementation cho một số ý tưởng làm thế nào để thực hiện điều này nhanh hơn. Lưu ý rằng họ sử dụng tài sản NewLine của lớp SerialPort.

Tôi cũng muốn lưu ý rằng không có phương pháp ReadLineAsync theo mặc định as the MSDN states:

Theo mặc định, phương pháp ReadLine sẽ chặn cho đến khi một dòng nhận được. Nếu hành vi này là không mong muốn, hãy đặt thuộc tính ReadTimeout thành bất kỳ giá trị khác 0 để buộc phương thức ReadLine để ném TimeoutException nếu một dòng không có sẵn trên cổng.

Vì vậy, có thể, trong trình bao bọc của bạn, bạn có thể thực hiện logic tương tự, vì vậy Task của bạn sẽ hủy nếu không có kết thúc cuối cùng trong một thời gian nhất định. Ngoài ra, bạn nên lưu ý điều này:

SerialPort đệm lớp dữ liệu, và các dòng chứa trong các BaseStream tài sản không, hai có thể mâu thuẫn về cách nhiều byte có sẵn để đọc. Thuộc tính BytesToRead thể chỉ ra rằng có những byte để đọc, nhưng các byte có thể không phải truy cập vào dòng chứa trong BaseStream tài sản vì họ đã được đệm lớp SerialPort.

Vì vậy, một lần nữa, tôi đề nghị bạn thực hiện một số logic wrapper với không đồng bộ đọc và kiểm tra sau mỗi lần đọc, đang có dòng-end hay không, mà phải được ngăn chặn, và quấn nó bên async phương pháp, mà sẽ hủy Task sau một thời gian.

Hy vọng điều này sẽ hữu ích.

0

OK, đây là những gì tôi đã làm ... Nhận xét sẽ được đánh giá cao vì C# vẫn còn hơi mới đối với tôi!

Thật điên rồ khi có nhiều chuỗi cố gắng truy cập vào cổng nối tiếp đồng thời (hoặc bất kỳ tài nguyên nào, đặc biệt là tài nguyên không đồng bộ). Để khắc phục lên ứng dụng này mà không cần viết lại hoàn toàn, tôi đã giới thiệu một khóa SerialPortLockObject để đảm bảo độc quyền truy cập cổng nối tiếp như sau:

  • GUI chủ đề giữ SerialPortLockObject trừ khi nó có một hoạt động nền chạy.
  • Lớp SerialPort được bọc để bất kỳ đọc hoặc viết nào bằng một chủ đề không giữ SerialPortLockObject ném một ngoại lệ (đã giúp tìm một số lỗi tranh chấp).
  • Lớp bộ hẹn giờ được gói (lớp SerialOperationTimer) sao cho chức năng của nhân viên nền được gọi là được đặt trong ngoặc đơn bằng cách mua SerialPortLockObject. SerialOperationTimer chỉ cho phép một bộ hẹn giờ chạy cùng một lúc (giúp tìm một số lỗi trong đó GUI quên ngừng xử lý nền trước khi bắt đầu hẹn giờ khác). Điều này có thể được cải thiện bằng cách sử dụng một chủ đề cụ thể cho công việc hẹn giờ, với chủ đề đó giữ khóa cho toàn bộ thời gian bộ đếm thời gian hoạt động (nhưng vẫn còn nhiều công việc hơn, như được mã hóa System.Timers.Timer chạy chức năng công nhân từ nhóm chủ đề).
  • Khi dừng SerialOperationTimer, nó sẽ vô hiệu hóa bộ đếm thời gian cơ bản và xóa bộ đệm cổng nối tiếp (kích hoạt ngoại lệ từ bất kỳ hoạt động cổng nối tiếp bị chặn nào, như được giải thích trong phương pháp 1 ở trên). Sau đó, SerialPortLockObject được phản ứng bởi luồng GUI.

Đây là wrapper cho SerialPort:

/// <summary> CheckedSerialPort class checks that read and write operations are only performed by the thread owning the lock on the serial port </summary> 
// Just check reads and writes (not basic properties, opening/closing, or buffer discards). 
public class CheckedSerialPort : SafePort /* derived in turn from SerialPort */ 
{ 
    private void checkOwnership() 
    { 
     try 
     { 
      if (Monitor.IsEntered(XXX_Conn.SerialPortLockObject)) return; // the thread running this code has the lock; all set! 
      // Ooops... 
      throw new Exception("Serial IO attempted without lock ownership"); 
     } 
     catch (Exception ex) 
     { 
      StringBuilder sb = new StringBuilder(""); 
      sb.AppendFormat("Message: {0}\n", ex.Message); 
      sb.AppendFormat("Exception Type: {0}\n", ex.GetType().FullName); 
      sb.AppendFormat("Source: {0}\n", ex.Source); 
      sb.AppendFormat("StackTrace: {0}\n", ex.StackTrace); 
      sb.AppendFormat("TargetSite: {0}", ex.TargetSite); 
      Console.Write(sb.ToString()); 
      Debug.Assert(false); // lets have a look in the debugger NOW... 
      throw; 
     } 
    } 
    public new int ReadByte()          { checkOwnership(); return base.ReadByte(); } 
    public new string ReadTo(string value)       { checkOwnership(); return base.ReadTo(value); } 
    public new string ReadExisting()        { checkOwnership(); return base.ReadExisting(); } 
    public new void Write(string text)        { checkOwnership(); base.Write(text); } 
    public new void WriteLine(string text)       { checkOwnership(); base.WriteLine(text); } 
    public new void Write(byte[] buffer, int offset, int count)  { checkOwnership(); base.Write(buffer, offset, count); } 
    public new void Write(char[] buffer, int offset, int count)  { checkOwnership(); base.Write(buffer, offset, count); } 
} 

Và đây là wrapper cho System.Timers.Timer:

/// <summary> Wrap System.Timers.Timer class to provide safer exclusive access to serial port </summary> 
class SerialOperationTimer 
{ 
    private static SerialOperationTimer runningTimer = null; // there should only be one! 
    private string name; // for diagnostics 
    // Delegate TYPE for user's callback function (user callback function to make async measurements) 
    public delegate void SerialOperationTimerWorkerFunc_T(object source, System.Timers.ElapsedEventArgs e); 
    private SerialOperationTimerWorkerFunc_T workerFunc; // application function to call for this timer 
    private System.Timers.Timer timer; 
    private object workerEnteredLock = new object(); 
    private bool workerAlreadyEntered = false; 

    public SerialOperationTimer(string _name, int msecDelay, SerialOperationTimerWorkerFunc_T func) 
    { 
     name = _name; 
     workerFunc = func; 
     timer = new System.Timers.Timer(msecDelay); 
     timer.Elapsed += new System.Timers.ElapsedEventHandler(SerialOperationTimer_Tick); 
    } 

    private void SerialOperationTimer_Tick(object source, System.Timers.ElapsedEventArgs eventArgs) 
    { 
     lock (workerEnteredLock) 
     { 
      if (workerAlreadyEntered) return; // don't launch multiple copies of worker if timer set too fast; just ignore this tick 
      workerAlreadyEntered = true; 
     } 
     bool lockTaken = false; 
     try 
     { 
      // Acquire the serial lock prior calling the worker 
      Monitor.TryEnter(XXX_Conn.SerialPortLockObject, ref lockTaken); 
      if (!lockTaken) 
       throw new System.Exception("SerialOperationTimer " + name + ": Failed to get serial lock"); 
      // Debug.WriteLine("SerialOperationTimer " + name + ": Got serial lock"); 
      workerFunc(source, eventArgs); 
     } 
     finally 
     { 
      // release serial lock 
      if (lockTaken) 
      { 
       Monitor.Exit(XXX_Conn.SerialPortLockObject); 
       // Debug.WriteLine("SerialOperationTimer " + name + ": released serial lock"); 
      } 
      workerAlreadyEntered = false; 
     } 
    } 

    public void Start() 
    { 
     Debug.Assert(Form1.GUIthreadHashcode == Thread.CurrentThread.GetHashCode()); // should ONLY be called from GUI thread 
     Debug.Assert(!timer.Enabled); // successive Start or Stop calls are BAD 
     Debug.WriteLine("SerialOperationTimer " + name + ": Start"); 
     if (runningTimer != null) 
     { 
      Debug.Assert(false); // Lets have a look in the debugger NOW 
      throw new System.Exception("SerialOperationTimer " + name + ": Attempted 'Start' while " + runningTimer.name + " is still running"); 
     } 
     // Start background processing 
     // Release GUI thread's lock on the serial port, so background thread can grab it 
     Monitor.Exit(XXX_Conn.SerialPortLockObject); 
     runningTimer = this; 
     timer.Enabled = true; 
    } 

    public void Stop() 
    { 
     Debug.Assert(Form1.GUIthreadHashcode == Thread.CurrentThread.GetHashCode()); // should ONLY be called from GUI thread 
     Debug.Assert(timer.Enabled); // successive Start or Stop calls are BAD 
     Debug.WriteLine("SerialOperationTimer " + name + ": Stop"); 

     if (runningTimer != this) 
     { 
      Debug.Assert(false); // Lets have a look in the debugger NOW 
      throw new System.Exception("SerialOperationTimer " + name + ": Attempted 'Stop' while not running"); 
     } 
     // Stop further background processing from being initiated, 
     timer.Enabled = false; // but, background processing may still be in progress from the last timer tick... 
     runningTimer = null; 
     // Purge serial input and output buffers. Clearing input buf causes any blocking read in progress in background thread to throw 
     // System.IO.IOException "The I/O operation has been aborted because of either a thread exit or an application request.\r\n" 
     if(Form1.xxConnection.PortIsOpen) Form1.xxConnection.CiCommDiscardBothBuffers(); 
     bool lockTaken = false; 
     // Now, GUI thread needs the lock back. 
     // 3 sec REALLY should be enough time for background thread to cleanup and release the lock: 
     Monitor.TryEnter(XXX_Conn.SerialPortLockObject, 3000, ref lockTaken); 
     if (!lockTaken) 
      throw new Exception("Serial port lock not yet released by background timer thread "+name); 
     if (Form1.xxConnection.PortIsOpen) 
     { 
      // Its possible there's still stuff in transit from device (for example, background thread just completed 
      // sending an ACQ command as it was stopped). So, sync up with the device... 
      int r = Form1.xxConnection.CiSync(); 
      Debug.Assert(r == XXX_Conn.CI_OK); 
      if (r != XXX_Conn.CI_OK) 
       throw new Exception("Cannot re-sync with device after disabling timer thread " + name); 
     } 
    } 

    /// <summary> SerialOperationTimer.StopAllBackgroundTimers() - Stop all background activity </summary> 
    public static void StopAllBackgroundTimers() 
    { 
     if (runningTimer != null) runningTimer.Stop(); 
    } 

    public double Interval 
    { 
     get { return timer.Interval; } 
     set { timer.Interval = value; } 
    } 

} // class SerialOperationTimer 
+0

Bạn có ý tưởng đúng, nhưng làm cho đối tượng bạn đang khóa trên công chúng được coi là một mô hình chống và vì lý do chính đáng (vì bất kỳ mã nào có thể chiếm đoạt màn hình). Thay vào đó, tôi sẽ tạo một kiểu SafePort tùy chỉnh là thread an toàn và thực hiện khóa riêng của nó, và cấu trúc lại tất cả mã để buộc truy cập vào cổng khỏa thân phải sử dụng các phương thức công khai của loại SafePort đó (với tất cả các khóa riêng tư). Sạch hơn nhiều, và không cần phải xác minh truy cập vào màn hình trông rất lạc hậu, vv Để biết thêm về luồng nguyên thủy trong C# và .NET, hãy kiểm tra tài nguyên tuyệt vời này: www.albahari.com/threading/ – Mahol25

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