线程安全的,串口控制仪器的程序(C#)

35 阅读3分钟

1. 线程安全的单例类方案

csharp

using System;
using System.IO.Ports;
using System.Text;
using System.Threading;

public sealed class SerialInstrumentController : IDisposable
{
    // 使用Lazy<T>实现线程安全的单例模式
    private static readonly Lazy<SerialInstrumentController> _instance = 
        new Lazy<SerialInstrumentController>(() => new SerialInstrumentController());
    
    private readonly object _serialLock = new object();
    private SerialPort _serialPort;
    private bool _disposed = false;

    // 私有构造函数
    private SerialInstrumentController()
    {
        // 默认初始化
        InitializeSerialPort("COM1", 9600);
    }

    public static SerialInstrumentController Instance => _instance.Value;

    /// <summary>
    /// 初始化串口
    /// </summary>
    public void InitializeSerialPort(string portName, int baudRate)
    {
        lock (_serialLock)
        {
            ClosePort();
            
            _serialPort = new SerialPort
            {
                PortName = portName,
                BaudRate = baudRate,
                DataBits = 8,
                Parity = Parity.None,
                StopBits = StopBits.One,
                Handshake = Handshake.None,
                ReadTimeout = 1000,
                WriteTimeout = 1000
            };

            try
            {
                _serialPort.Open();
                Console.WriteLine($"串口 {portName} 已打开");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"打开串口失败: {ex.Message}");
                _serialPort = null;
                throw;
            }
        }
    }

    /// <summary>
    /// 线程安全的发送命令方法
    /// </summary>
    public bool SendCommand(string command)
    {
        lock (_serialLock)
        {
            if (_serialPort == null || !_serialPort.IsOpen)
            {
                Console.WriteLine("串口未打开");
                return false;
            }

            try
            {
                _serialPort.Write(command);
                Console.WriteLine($"发送命令: {command}");
                return true;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"发送命令失败: {ex.Message}");
                return false;
            }
        }
    }

    /// <summary>
    /// 线程安全的读取响应方法
    /// </summary>
    public string ReadResponse(int timeoutMs = 1000)
    {
        lock (_serialLock)
        {
            if (_serialPort == null || !_serialPort.IsOpen)
            {
                return "ERROR: 串口未打开";
            }

            try
            {
                // 设置读取超时
                _serialPort.ReadTimeout = timeoutMs;
                
                StringBuilder response = new StringBuilder();
                DateTime startTime = DateTime.Now;
                
                while ((DateTime.Now - startTime).TotalMilliseconds < timeoutMs)
                {
                    if (_serialPort.BytesToRead > 0)
                    {
                        string data = _serialPort.ReadExisting();
                        response.Append(data);
                        
                        // 根据实际情况判断是否读取完成
                        // 例如,如果仪器响应以特定字符结尾
                        if (data.Contains("\n") || data.Contains("\r"))
                        {
                            break;
                        }
                    }
                    
                    Thread.Sleep(10); // 短暂休眠避免CPU占用过高
                }
                
                return response.ToString().Trim();
            }
            catch (TimeoutException)
            {
                return "TIMEOUT: 读取超时";
            }
            catch (Exception ex)
            {
                return $"ERROR: {ex.Message}";
            }
        }
    }

    /// <summary>
    /// 原子操作:发送命令并接收响应
    /// </summary>
    public string SendAndReceive(string command, int timeoutMs = 1000)
    {
        lock (_serialLock)
        {
            if (!SendCommand(command))
            {
                return "ERROR: 发送命令失败";
            }
            
            return ReadResponse(timeoutMs);
        }
    }

    /// <summary>
    /// 关闭串口
    /// </summary>
    private void ClosePort()
    {
        if (_serialPort != null)
        {
            if (_serialPort.IsOpen)
            {
                _serialPort.Close();
            }
            _serialPort.Dispose();
            _serialPort = null;
        }
    }

    /// <summary>
    /// 实现IDisposable接口
    /// </summary>
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                ClosePort();
            }
            _disposed = true;
        }
    }

    ~SerialInstrumentController()
    {
        Dispose(false);
    }
}

2. 使用示例和测试代码

csharp

using System;
using System.Threading.Tasks;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("工控仪器控制演示");
        
        // 使用单例实例
        var controller = SerialInstrumentController.Instance;
        
        try
        {
            // 初始化串口
            controller.InitializeSerialPort("COM3", 9600);
            
            // 单线程测试
            Console.WriteLine("单线程测试:");
            string response = controller.SendAndReceive("*IDN?");
            Console.WriteLine($"响应: {response}");
            
            // 多线程并发测试
            Console.WriteLine("\n多线程并发测试:");
            RunMultiThreadTest(controller);
            
            // 异步任务测试
            Console.WriteLine("\n异步任务测试:");
            RunAsyncTest(controller).Wait();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"错误: {ex.Message}");
        }
        finally
        {
            // 清理资源
            controller.Dispose();
        }
        
        Console.WriteLine("\n按任意键退出...");
        Console.ReadKey();
    }
    
    static void RunMultiThreadTest(SerialInstrumentController controller)
    {
        int threadCount = 5;
        var tasks = new Task[threadCount];
        
        for (int i = 0; i < threadCount; i++)
        {
            int threadId = i;
            tasks[i] = Task.Run(() =>
            {
                string cmd = $"READ{threadId}";
                Console.WriteLine($"线程 {threadId}: 发送命令 {cmd}");
                
                try
                {
                    string response = controller.SendAndReceive(cmd);
                    Console.WriteLine($"线程 {threadId}: 收到响应 {response}");
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"线程 {threadId}: 错误 {ex.Message}");
                }
            });
        }
        
        Task.WaitAll(tasks);
    }
    
    static async Task RunAsyncTest(SerialInstrumentController controller)
    {
        var tasks = new Task<string>[3];
        
        for (int i = 0; i < 3; i++)
        {
            tasks[i] = Task.Run(() =>
            {
                return controller.SendAndReceive("MEASURE?");
            });
        }
        
        var responses = await Task.WhenAll(tasks);
        
        for (int i = 0; i < responses.Length; i++)
        {
            Console.WriteLine($"任务 {i} 响应: {responses[i]}");
        }
    }
}

3. 更高级的异步队列模式实现

csharp

using System;
using System.Collections.Concurrent;
using System.IO.Ports;
using System.Threading;
using System.Threading.Tasks;

public class InstrumentCommandQueue : IDisposable
{
    private class CommandRequest
    {
        public string Command { get; set; }
        public TaskCompletionSource<string> CompletionSource { get; set; }
        public int TimeoutMs { get; set; }
    }
    
    private readonly BlockingCollection<CommandRequest> _commandQueue = 
        new BlockingCollection<CommandRequest>();
    
    private readonly SerialPort _serialPort;
    private readonly Thread _workerThread;
    private readonly CancellationTokenSource _cancellationTokenSource;
    private bool _disposed = false;
    
    public InstrumentCommandQueue(string portName, int baudRate)
    {
        _serialPort = new SerialPort
        {
            PortName = portName,
            BaudRate = baudRate,
            DataBits = 8,
            Parity = Parity.None,
            StopBits = StopBits.One,
            ReadTimeout = 1000,
            WriteTimeout = 1000
        };
        
        _cancellationTokenSource = new CancellationTokenSource();
        
        try
        {
            _serialPort.Open();
            Console.WriteLine($"串口 {portName} 已打开");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"打开串口失败: {ex.Message}");
            throw;
        }
        
        // 启动工作线程
        _workerThread = new Thread(ProcessCommands);
        _workerThread.IsBackground = true;
        _workerThread.Start();
    }
    
    /// <summary>
    /// 提交命令到队列
    /// </summary>
    public Task<string> SubmitCommandAsync(string command, int timeoutMs = 1000)
    {
        var tcs = new TaskCompletionSource<string>();
        
        var request = new CommandRequest
        {
            Command = command,
            CompletionSource = tcs,
            TimeoutMs = timeoutMs
        };
        
        _commandQueue.Add(request);
        return tcs.Task;
    }
    
    private void ProcessCommands()
    {
        try
        {
            foreach (var request in _commandQueue.GetConsumingEnumerable(_cancellationTokenSource.Token))
            {
                try
                {
                    // 串口操作(单线程执行,无需锁)
                    _serialPort.Write(request.Command);
                    Console.WriteLine($"发送: {request.Command}");
                    
                    // 读取响应
                    string response = ReadWithTimeout(request.TimeoutMs);
                    
                    // 完成Task
                    request.CompletionSource.SetResult(response);
                }
                catch (Exception ex)
                {
                    request.CompletionSource.SetException(ex);
                }
            }
        }
        catch (OperationCanceledException)
        {
            // 正常取消
        }
    }
    
    private string ReadWithTimeout(int timeoutMs)
    {
        StringBuilder response = new StringBuilder();
        DateTime startTime = DateTime.Now;
        
        while ((DateTime.Now - startTime).TotalMilliseconds < timeoutMs)
        {
            if (_serialPort.BytesToRead > 0)
            {
                string data = _serialPort.ReadExisting();
                response.Append(data);
                
                if (data.Contains("\n") || response.Length > 1024) // 防止缓冲区溢出
                {
                    break;
                }
            }
            
            Thread.Sleep(10);
        }
        
        return response.ToString().Trim();
    }
    
    public void Dispose()
    {
        if (!_disposed)
        {
            _disposed = true;
            
            // 取消工作线程
            _cancellationTokenSource.Cancel();
            
            // 等待工作线程结束
            if (_workerThread != null && _workerThread.IsAlive)
            {
                _workerThread.Join(2000);
            }
            
            // 清理队列
            _commandQueue.Dispose();
            
            // 关闭串口
            if (_serialPort != null)
            {
                if (_serialPort.IsOpen)
                {
                    _serialPort.Close();
                }
                _serialPort.Dispose();
            }
            
            _cancellationTokenSource.Dispose();
            
            Console.WriteLine("InstrumentCommandQueue 已释放");
        }
    }
}

4. 工厂模式:支持多种仪器类型

csharp

using System;

public interface IInstrumentController
{
    string SendCommand(string command);
    Task<string> SendCommandAsync(string command);
}

public class InstrumentControllerFactory
{
    private static readonly Lazy<InstrumentControllerFactory> _instance = 
        new Lazy<InstrumentControllerFactory>(() => new InstrumentControllerFactory());
    
    private readonly ConcurrentDictionary<string, IInstrumentController> _controllers = 
        new ConcurrentDictionary<string, IInstrumentController>();
    private readonly object _factoryLock = new object();
    
    public static InstrumentControllerFactory Instance => _instance.Value;
    
    private InstrumentControllerFactory() { }
    
    /// <summary>
    /// 获取或创建仪器控制器
    /// </summary>
    public IInstrumentController GetController(string instrumentType, string portName, int baudRate)
    {
        string key = $"{instrumentType}_{portName}_{baudRate}";
        
        return _controllers.GetOrAdd(key, k =>
        {
            switch (instrumentType.ToUpper())
            {
                case "AGILENT_34401A":
                    return new Agilent34401AController(portName, baudRate);
                    
                case "KEITHLEY_2400":
                    return new Keithley2400Controller(portName, baudRate);
                    
                case "FLUKE_5500A":
                    return new Fluke5500AController(portName, baudRate);
                    
                default:
                    return new GenericInstrumentController(portName, baudRate);
            }
        });
    }
    
    /// <summary>
    /// 关闭所有控制器
    /// </summary>
    public void CloseAll()
    {
        foreach (var controller in _controllers.Values)
        {
            if (controller is IDisposable disposable)
            {
                disposable.Dispose();
            }
        }
        _controllers.Clear();
    }
}

// 具体的仪器控制器实现
public class Agilent34401AController : IInstrumentController, IDisposable
{
    private readonly SerialInstrumentController _serialController;
    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
    
    public Agilent34401AController(string portName, int baudRate)
    {
        _serialController = SerialInstrumentController.Instance;
        _serialController.InitializeSerialPort(portName, baudRate);
    }
    
    public string SendCommand(string command)
    {
        // 特定的仪器命令处理逻辑
        if (command.StartsWith("MEAS:"))
        {
            // 添加仪器特定的格式化
            command = command.Replace("MEAS:", "MEASURE:") + ";*OPC?";
        }
        
        return _serialController.SendAndReceive(command);
    }
    
    public async Task<string> SendCommandAsync(string command)
    {
        await _semaphore.WaitAsync();
        try
        {
            return await Task.Run(() => SendCommand(command));
        }
        finally
        {
            _semaphore.Release();
        }
    }
    
    public void Dispose()
    {
        _semaphore.Dispose();
    }
}

5. 使用建议和最佳实践

csharp

public static class InstrumentControlBestPractices
{
    // 1. 使用配置类管理串口参数
    public class SerialPortConfig
    {
        public string PortName { get; set; } = "COM1";
        public int BaudRate { get; set; } = 9600;
        public int DataBits { get; set; } = 8;
        public Parity Parity { get; set; } = Parity.None;
        public StopBits StopBits { get; set; } = StopBits.One;
        public int ReadTimeout { get; set; } = 1000;
        public int WriteTimeout { get; set; } = 1000;
    }
    
    // 2. 使用依赖注入
    public static IServiceCollection AddInstrumentServices(this IServiceCollection services, SerialPortConfig config)
    {
        services.AddSingleton(config);
        
        services.AddSingleton<IInstrumentController>(provider =>
        {
            var cfg = provider.GetRequiredService<SerialPortConfig>();
            return new GenericInstrumentController(cfg.PortName, cfg.BaudRate);
        });
        
        return services;
    }
    
    // 3. 异常处理包装器
    public static T ExecuteWithRetry<T>(Func<T> action, int maxRetries = 3, int delayMs = 100)
    {
        int retryCount = 0;
        
        while (retryCount < maxRetries)
        {
            try
            {
                return action();
            }
            catch (Exception ex) when (retryCount < maxRetries - 1)
            {
                retryCount++;
                Console.WriteLine($"操作失败,第 {retryCount} 次重试: {ex.Message}");
                
                if (ex is UnauthorizedAccessException || ex is IOException)
                {
                    // 串口访问错误,可能需要重新初始化
                    Thread.Sleep(delayMs * retryCount);
                }
                else
                {
                    throw;
                }
            }
        }
        
        throw new InvalidOperationException($"操作在 {maxRetries} 次重试后仍失败");
    }
}

总结

在C#中实现工控串口仪器控制的线程安全,最简单的是使用线程安全的单例模式,但需要根据具体场景选择:

推荐线程安全方案:

  1. 简单应用:使用Lazy<T>实现的单例模式
  2. 高并发场景:使用消息队列模式
  3. 多仪器类型:使用工厂模式配合单例

关键要点:

  1. 使用lock语句保护共享资源访问
  2. 实现IDisposable接口确保资源正确释放
  3. 添加适当的超时机制防止线程阻塞
  4. **使用Taskasync/await**支持异步操作