
如果你正在开发一个需要同时处理多个窗口、多个线程的桌面应用比如自动化测试工具、数据采集系统、或者复杂的交易软件你可能会遇到这样的困境窗口和线程的管理代码像意大利面一样纠缠在一起加一个新功能就可能导致整个程序崩溃。这不是你的代码写得不好而是这类应用本身就存在一个核心矛盾——界面响应与后台任务执行之间的资源竞争与状态同步问题。传统的做法往往是开一个线程绑定一个窗口然后在线程里写死业务逻辑。当需求变成要同时监控10个窗口、执行20种不同的任务时代码立刻变得难以维护。线程泄露、窗口假死、消息队列堵塞、数据竞争……这些问题会接踵而至。“多线程多窗口商用框架模块设计”这个主题听起来很学术但它要解决的正是这个最实际的工程难题。它不是一个具体的开源库而是一套架构设计思想与最佳实践的集合。本文将为你彻底拆解这套设计提供一个清晰、可落地的模块化框架蓝图。读完本文你将能理解核心矛盾厘清多线程与多窗口交互中的关键挑战数据竞争、死锁、消息传递。掌握模块化设计学会如何将系统解耦为独立的“管理模块”、“通信模块”和“业务模块”。获得可复用的代码骨架基于C# WinForms原理通用提供一套可直接用于项目的框架核心代码。规避常见陷阱明确线程安全、资源释放、异常处理等方面的最佳实践与排查清单。我们不止步于理论而是直接从一个真实的“电商数据监控面板”场景出发带你从零搭建一个稳健的商用级框架。1. 核心问题为什么多窗口多线程容易失控在深入设计之前我们必须先诊断“病因”。多窗口多线程程序的问题通常不是偶然的而是源于几个固有的设计缺陷紧耦合架构业务逻辑、线程管理和窗口控制代码写在一起。修改业务可能影响线程生命周期调整UI又可能破坏数据流。原始线程通信直接使用Control.Invoke或Dispatcher.Invoke虽然简单但容易造成回调地狱并且难以跨模块、跨窗口传递复杂消息。缺乏统一的生命周期管理窗口关闭时其关联的线程、定时器、网络连接等资源可能没有正确释放导致内存泄漏和僵尸线程。状态同步困难一个后台线程计算出的数据需要安全地更新到多个窗口的控件上同时还要避免UI线程阻塞。错误处理黑洞子线程的异常如果不加捕获会默默导致线程退出问题难以追踪。一个健壮的框架目标就是通过模块化和约定优于配置的原则系统性地解决这些问题。2. 框架核心模块设计我们将框架划分为四个核心层每层职责单一通过清晰的接口进行通信。[ 表现层 (UI Windows) ] ↑ [ 窗口管理模块 (Window Manager) ] ↑ [ 线程与任务管理模块 (Task Engine) ] ↑ [ 业务逻辑模块 (Business Services) ]2.1 业务逻辑模块 (Business Services)这是框架的基石完全独立于UI和线程。它包含纯业务逻辑例如数据获取、计算、验证等。职责提供原子性的业务操作。特点无状态或管理自身状态不引用任何UI控件。示例DataFetcherService,TradeCalculatorService。2.2 线程与任务管理模块 (Task Engine)这是框架的“发动机”负责创建、调度、监控和销毁后台线程或任务。职责提供线程池或自定义任务队列。封装Thread、Task或BackgroundWorker的创建。实现任务的取消、超时和重试机制。收集任务执行状态和结果。关键设计使用CancellationTokenSource来支持协作式取消避免强制Abort线程。2.3 窗口管理模块 (Window Manager)这是框架的“调度中心”管理所有窗口实例并作为UI层与后台任务层的桥梁。职责窗口的创建、显示、隐藏和销毁。维护窗口与后台任务的映射关系例如哪个窗口发起了哪个任务。提供安全、统一的UI更新接口。关键设计它持有Task Engine的引用并接收来自业务模块的结果然后将其派发给正确的窗口。2.4 表现层 (UI Windows)这是具体的窗口形式WinForms的Form、WPF的Window。它们应该尽可能“笨”只负责职责接收用户输入。调用Window Manager提供的方法来发起业务请求。实现由Window Manager调用的更新接口以刷新界面。黄金法则UI控件属性如TextBox.Text只在UI线程上设置。3. 通信机制消息总线的引入模块解耦后它们如何通信直接互相引用会重新引入耦合。我们引入一个轻量级的消息总线Event Aggregator模式。原理模块之间不直接调用而是向总线“发布”消息关心某类消息的模块向总线“订阅”。总线负责将消息传递给所有订阅者。优势彻底解耦发布者不知道订阅者是谁反之亦然。简化跨线程调用后台线程可以发布“数据已更新”消息由总线确保在UI线程上执行UI更新订阅者的逻辑。易于扩展新增一个需要响应的窗口只需订阅相应消息无需修改发送方代码。4. 环境准备与项目结构我们以C# WinForms (.NET 6)为例进行实现。原理同样适用于WPF、WinUI或其他语言/框架。开发环境IDE: Visual Studio 2022 或 RiderSDK: .NET 6 或更高版本目标框架net6.0-windows(确保支持Windows桌面应用)项目结构规划MultiThreadWindowFramework/ ├── FrameworkCore/ # 框架核心库类库 │ ├── Interfaces/ # 接口定义 │ ├── Models/ # 消息、事件等模型 │ ├── Services/ # 核心服务实现TaskEngine, WindowManager等 │ └── Messaging/ # 消息总线实现 ├── BusinessLogic/ # 业务逻辑库类库 │ └── Services/ # 具体的业务服务 └── MainApp/ # 主应用程序WinForms应用 ├── Forms/ # 具体的窗口 └── Program.cs # 启动和依赖注入配置5. 核心代码实现5.1 定义消息模型与总线接口首先在FrameworkCore/Models/中定义消息基类和具体消息。// FrameworkCore/Models/BaseMessage.cs namespace FrameworkCore.Models { /// summary /// 消息基类可包含发送源、时间戳等信息 /// /summary public abstract class BaseMessage { public object? Sender { get; set; } public DateTime Timestamp { get; } DateTime.Now; } // 示例一个数据更新消息 public class DataUpdatedMessage : BaseMessage { public string DataType { get; } public object Data { get; } public DataUpdatedMessage(string dataType, object data) { DataType dataType; Data data; } } // 示例一个任务状态变更消息 public class TaskStatusChangedMessage : BaseMessage { public Guid TaskId { get; } public string Status { get; } // Running, Completed, Failed, Cancelled public string? Message { get; } public TaskStatusChangedMessage(Guid taskId, string status, string? message null) { TaskId taskId; Status status; Message message; } } }接着在FrameworkCore/Messaging/中实现一个简单的事件聚合器。// FrameworkCore/Messaging/EventAggregator.cs using System; using System.Collections.Generic; using System.Linq; using FrameworkCore.Models; namespace FrameworkCore.Messaging { public interface IEventAggregator { void SubscribeTMessage(ActionTMessage handler) where TMessage : BaseMessage; void UnsubscribeTMessage(ActionTMessage handler) where TMessage : BaseMessage; void PublishTMessage(TMessage message) where TMessage : BaseMessage; } public class EventAggregator : IEventAggregator { private readonly DictionaryType, Listobject _handlers new(); public void SubscribeTMessage(ActionTMessage handler) where TMessage : BaseMessage { var messageType typeof(TMessage); if (!_handlers.ContainsKey(messageType)) { _handlers[messageType] new Listobject(); } _handlers[messageType].Add(handler); } public void UnsubscribeTMessage(ActionTMessage handler) where TMessage : BaseMessage { var messageType typeof(TMessage); if (_handlers.ContainsKey(messageType)) { _handlers[messageType].Remove(handler); } } public void PublishTMessage(TMessage message) where TMessage : BaseMessage { var messageType typeof(TMessage); if (_handlers.ContainsKey(messageType)) { // 复制列表以避免在迭代过程中修改导致的异常 foreach (var handler in _handlers[messageType].ToList().CastActionTMessage()) { handler?.Invoke(message); } } } } }5.2 实现线程与任务管理模块在FrameworkCore/Services/中实现一个基本的任务引擎。// FrameworkCore/Services/ITaskEngine.cs TaskEngine.cs using System; using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; using FrameworkCore.Messaging; using FrameworkCore.Models; namespace FrameworkCore.Services { public interface ITaskEngine { Guid QueueTask(FuncCancellationToken, Task taskFunc, string description); void CancelTask(Guid taskId); TaskStatus GetTaskStatus(Guid taskId); } public class TaskEngine : ITaskEngine, IDisposable { private readonly IEventAggregator _eventAggregator; private readonly ConcurrentDictionaryGuid, TaskInfo _runningTasks new(); private readonly CancellationTokenSource _globalCts new(); public TaskEngine(IEventAggregator eventAggregator) { _eventAggregator eventAggregator; } public Guid QueueTask(FuncCancellationToken, Task taskFunc, string description) { var taskId Guid.NewGuid(); var linkedCts CancellationTokenSource.CreateLinkedTokenSource(_globalCts.Token); var taskInfo new TaskInfo { Id taskId, Description description, CancellationTokenSource linkedCts, Status TaskStatus.WaitingToRun }; _runningTasks[taskId] taskInfo; // 使用Task.Run在后台线程池执行避免阻塞调用者 _ Task.Run(async () { taskInfo.Status TaskStatus.Running; PublishStatusChange(taskId, Running); try { await taskFunc(linkedCts.Token); taskInfo.Status TaskStatus.RanToCompletion; PublishStatusChange(taskId, Completed); } catch (OperationCanceledException) { taskInfo.Status TaskStatus.Canceled; PublishStatusChange(taskId, Cancelled, Task was cancelled.); } catch (Exception ex) { taskInfo.Status TaskStatus.Faulted; PublishStatusChange(taskId, Failed, ex.Message); } finally { _runningTasks.TryRemove(taskId, out _); linkedCts.Dispose(); } }, linkedCts.Token); return taskId; } public void CancelTask(Guid taskId) { if (_runningTasks.TryGetValue(taskId, out var taskInfo)) { taskInfo.CancellationTokenSource?.Cancel(); } } public TaskStatus GetTaskStatus(Guid taskId) { return _runningTasks.TryGetValue(taskId, out var taskInfo) ? taskInfo.Status : TaskStatus.Unknown; } private void PublishStatusChange(Guid taskId, string status, string? message null) { _eventAggregator.Publish(new TaskStatusChangedMessage(taskId, status, message)); } public void Dispose() { _globalCts.Cancel(); _globalCts.Dispose(); } private class TaskInfo { public Guid Id { get; set; } public string Description { get; set; } string.Empty; public CancellationTokenSource CancellationTokenSource { get; set; } null!; public TaskStatus Status { get; set; } } } }5.3 实现窗口管理模块与UI线程调度窗口管理器需要协调UI更新。我们创建一个IUiThreadDispatcher接口来抽象UI线程调用并在WinForms中实现它。// FrameworkCore/Services/IUiThreadDispatcher.cs namespace FrameworkCore.Services { public interface IUiThreadDispatcher { void Invoke(Action action); Task InvokeAsync(Action action); } } // MainApp/Services/WinFormsUiDispatcher.cs (在主应用程序项目中) using System; using System.Threading.Tasks; using System.Windows.Forms; using FrameworkCore.Services; namespace MainApp.Services { public class WinFormsUiDispatcher : IUiThreadDispatcher { private readonly Control _controlForInvoke; public WinFormsUiDispatcher(Control control) { _controlForInvoke control ?? throw new ArgumentNullException(nameof(control)); } public void Invoke(Action action) { if (_controlForInvoke.InvokeRequired) { _controlForInvoke.Invoke(action); } else { action(); } } public Task InvokeAsync(Action action) { var tcs new TaskCompletionSourcebool(); _controlForInvoke.BeginInvoke(new Action(() { try { action(); tcs.SetResult(true); } catch (Exception ex) { tcs.SetException(ex); } })); return tcs.Task; } } }现在实现WindowManager它订阅消息并利用IUiThreadDispatcher安全更新UI。// FrameworkCore/Services/IWindowManager.cs WindowManager.cs using System; using System.Collections.Generic; using FrameworkCore.Messaging; using FrameworkCore.Models; using FrameworkCore.Services; namespace FrameworkCore.Services { public interface IWindowManager { void RegisterWindow(string windowId, IUpdatableWindow window); void UnregisterWindow(string windowId); void UpdateWindowData(string windowId, string dataType, object data); } // 窗口需要实现的更新接口 public interface IUpdatableWindow { void OnDataUpdated(string dataType, object data); void OnTaskStatusChanged(Guid taskId, string status, string? message); } public class WindowManager : IWindowManager, IDisposable { private readonly IEventAggregator _eventAggregator; private readonly IUiThreadDispatcher _uiDispatcher; private readonly Dictionarystring, IUpdatableWindow _windows new(); public WindowManager(IEventAggregator eventAggregator, IUiThreadDispatcher uiDispatcher) { _eventAggregator eventAggregator; _uiDispatcher uiDispatcher; SubscribeToMessages(); } private void SubscribeToMessages() { // 订阅数据更新消息并在UI线程上转发给对应窗口 _eventAggregator.SubscribeDataUpdatedMessage(msg { // 这里简化处理广播给所有窗口。实际可根据msg中的信息路由到特定窗口。 foreach (var window in _windows.Values) { _uiDispatcher.Invoke(() window.OnDataUpdated(msg.DataType, msg.Data)); } }); // 订阅任务状态消息 _eventAggregator.SubscribeTaskStatusChangedMessage(msg { foreach (var window in _windows.Values) { _uiDispatcher.Invoke(() window.OnTaskStatusChanged(msg.TaskId, msg.Status, msg.Message)); } }); } public void RegisterWindow(string windowId, IUpdatableWindow window) { _windows[windowId] window; } public void UnregisterWindow(string windowId) { _windows.Remove(windowId); } // 提供一个直接更新窗口的API可选 public void UpdateWindowData(string windowId, string dataType, object data) { if (_windows.TryGetValue(windowId, out var window)) { _uiDispatcher.Invoke(() window.OnDataUpdated(dataType, data)); } } public void Dispose() { // 清理订阅等资源 _windows.Clear(); } } }5.4 实现业务逻辑与主窗口示例业务逻辑服务// BusinessLogic/Services/IMonitorService.cs using System.Threading; using System.Threading.Tasks; namespace BusinessLogic.Services { public interface IMonitorService { Taskstring FetchDataFromSourceAsync(string sourceId, CancellationToken ct); Task PerformCalculationAsync(string data, CancellationToken ct); } public class SimulatedMonitorService : IMonitorService { public async Taskstring FetchDataFromSourceAsync(string sourceId, CancellationToken ct) { // 模拟网络延迟 await Task.Delay(2000, ct); ct.ThrowIfCancellationRequested(); return $模拟数据来自 {sourceId} - {DateTime.Now:HH:mm:ss}; } public async Task PerformCalculationAsync(string data, CancellationToken ct) { await Task.Delay(1000, ct); ct.ThrowIfCancellationRequested(); // 模拟计算 Console.WriteLine($计算完成: {data}); } } }主窗口实现// MainApp/Forms/MainForm.cs using System; using System.Windows.Forms; using BusinessLogic.Services; using FrameworkCore.Services; using FrameworkCore.Messaging; using FrameworkCore.Models; namespace MainApp.Forms { public partial class MainForm : Form, IUpdatableWindow { private readonly ITaskEngine _taskEngine; private readonly IMonitorService _monitorService; private readonly IEventAggregator _eventAggregator; private readonly IWindowManager _windowManager; private Guid _currentTaskId; public MainForm(ITaskEngine taskEngine, IMonitorService monitorService, IEventAggregator eventAggregator, IWindowManager windowManager) { InitializeComponent(); // WinForms Designer生成的方法 _taskEngine taskEngine; _monitorService monitorService; _eventAggregator eventAggregator; _windowManager windowManager; // 向窗口管理器注册自己 _windowManager.RegisterWindow(MainMonitorWindow, this); SetupEventHandlers(); } private void SetupEventHandlers() { btnStart.Click BtnStart_Click; btnCancel.Click BtnCancel_Click; btnOpenNew.Click BtnOpenNew_Click; this.FormClosing MainForm_FormClosing; } private async void BtnStart_Click(object? sender, EventArgs e) { btnStart.Enabled false; btnCancel.Enabled true; listBoxLog.Items.Add($[{DateTime.Now:HH:mm:ss}] 开始获取数据...); // 通过任务引擎排队任务 _currentTaskId _taskEngine.QueueTask(async (ct) { var data await _monitorService.FetchDataFromSourceAsync(Source_A, ct); // 业务逻辑完成发布消息而不是直接操作UI _eventAggregator.Publish(new DataUpdatedMessage(StockData, data)); await _monitorService.PerformCalculationAsync(data, ct); // 可以发布另一个计算完成的消息 }, 获取并处理监控数据); } private void BtnCancel_Click(object? sender, EventArgs e) { if (_currentTaskId ! Guid.Empty) { _taskEngine.CancelTask(_currentTaskId); listBoxLog.Items.Add($[{DateTime.Now:HH:mm:ss}] 已请求取消任务。); } } private void BtnOpenNew_Click(object? sender, EventArgs e) { // 使用依赖注入容器解析并打开新窗口这里简化为new // var detailForm _serviceProvider.GetRequiredServiceDetailForm(); // detailForm.Show(); MessageBox.Show(此处演示打开新窗口新窗口也应实现IUpdatableWindow并注册。); } // 实现IUpdatableWindow接口由WindowManager在UI线程调用 public void OnDataUpdated(string dataType, object data) { if (dataType StockData data is string textData) { // 现在是在UI线程上可以安全更新控件 txtDataDisplay.Text textData; listBoxLog.Items.Add($[{DateTime.Now:HH:mm:ss}] 数据已更新。); } } public void OnTaskStatusChanged(Guid taskId, string status, string? message) { if (taskId _currentTaskId) { listBoxLog.Items.Add($[{DateTime.Now:HH:mm:ss}] 任务状态: {status} - {message}); if (status Completed || status Failed || status Cancelled) { // 任务结束恢复按钮状态 this.Invoke(new Action(() { btnStart.Enabled true; btnCancel.Enabled false; })); } } } private void MainForm_FormClosing(object? sender, FormClosingEventArgs e) { // 清理资源 _windowManager.UnregisterWindow(MainMonitorWindow); if (_currentTaskId ! Guid.Empty) { _taskEngine.CancelTask(_currentTaskId); } } } }5.5 程序启动与依赖注入配置在Program.cs或Startup.cs中配置所有服务。// MainApp/Program.cs using System; using System.Windows.Forms; using Microsoft.Extensions.DependencyInjection; using FrameworkCore.Messaging; using FrameworkCore.Services; using BusinessLogic.Services; using MainApp.Forms; using MainApp.Services; namespace MainApp { internal static class Program { [STAThread] static void Main() { ApplicationConfiguration.Initialize(); var services new ServiceCollection(); // 注册框架核心服务 services.AddSingletonIEventAggregator, EventAggregator(); services.AddSingletonITaskEngine, TaskEngine(); // WindowManager和UiDispatcher需要特殊处理因为依赖Control // 我们先注册它们为Scoped或Transient在创建主窗体时提供Control services.AddScopedIWindowManager, WindowManager(); services.AddScopedIUiThreadDispatcher(provider { // 注意这个委托会在主窗体创建后被调用。这里是个简化示例。 // 更健壮的做法是使用工厂模式或在主窗体构造函数中手动设置。 // 此处仅为演示假设有一个静态方式获取主Control。 return new WinFormsUiDispatcher(Application.OpenForms[0]); }); // 注册业务逻辑服务 services.AddSingletonIMonitorService, SimulatedMonitorService(); // 注册主窗体 services.AddScopedMainForm(); var serviceProvider services.BuildServiceProvider(); // 创建主窗体依赖注入容器会自动解析其依赖 using (var scope serviceProvider.CreateScope()) { var mainForm scope.ServiceProvider.GetRequiredServiceMainForm(); Application.Run(mainForm); } } } }6. 运行与效果验证编译并运行上述项目。你会看到一个主窗体包含“开始”、“取消”、“打开新窗口”按钮和一个列表框。点击“开始”列表框会显示“开始获取数据...”按钮状态改变。大约2秒后文本框会显示获取的模拟数据列表框更新状态。这演示了后台任务通过消息总线安全更新UI。在任务执行中点击“取消”列表框会显示取消请求任务被协作式取消。这演示了统一的任务生命周期管理。点击“打开新窗口”可以扩展为打开另一个实现了IUpdatableWindow的窗体它将自动接收相同的数据更新消息展示了多窗口协同。成功的关键验证点UI不会卡死响应性。后台任务异常不会导致程序崩溃稳定性。可以安全地取消任务可控性。数据更新自动同步到所有已注册窗口解耦与通信。7. 常见问题与排查思路问题现象可能原因排查方式解决方案UI更新无效控件属性未改变1. 未在UI线程上更新控件。2. 消息未正确发布或订阅。3. 窗口未向WindowManager注册。1. 检查OnDataUpdated方法是否被调用加断点或日志。2. 检查EventAggregator的订阅列表。3. 确认窗口的RegisterWindow被调用。1. 确保所有UI更新都通过IUiThreadDispatcher.Invoke。2. 检查消息类型和订阅代码。3. 在窗口构造函数或Load事件中完成注册。任务无法取消1. 业务逻辑未检查CancellationToken。2.CancellationTokenSource未被正确链接或传递。1. 在业务服务的异步方法中检查ct.ThrowIfCancellationRequested()。2. 检查TaskEngine.QueueTask中linkedCts的创建。1. 在业务循环或长时间操作中定期检查ct.IsCancellationRequested并退出。2. 确保将CancellationToken传递给所有可取消的异步API如Task.Delay。内存泄漏窗口关闭后任务仍在运行1. 窗口关闭时未取消关联任务。2. 未从WindowManager注销窗口导致事件持有引用。1. 在窗口的FormClosing或Dispose中调用TaskEngine.CancelTask。2. 使用内存分析工具查看对象引用。1. 在IUpdatableWindow实现中妥善处理Dispose模式。2. 确保WindowManager.UnregisterWindow被调用。多个窗口收到不相关的消息消息总线是广播模式所有订阅者都会收到消息。检查消息内容看是否需要包含目标窗口ID。在消息模型如DataUpdatedMessage中增加TargetWindowId属性在WindowManager中根据此属性进行路由只调用目标窗口的更新方法。程序退出时抛出对象已释放异常后台任务仍在运行但依赖的服务如EventAggregator已被释放。查看退出时的堆栈跟踪。实现更优雅的关闭流程在程序退出主循环前调用TaskEngine的Dispose方法取消所有任务并等待一段时间让任务结束。8. 最佳实践与工程建议依赖注入如示例所示使用DI容器如Microsoft.Extensions.DependencyInjection管理服务生命周期这是实现松耦合的关键。日志记录在TaskEngine、WindowManager和业务服务中添加详细的日志如使用ILogger这是线上问题排查的生命线。配置化将线程池大小、任务超时时间、重试策略等参数提取到配置文件中。错误处理与重试在TaskEngine中增强错误处理逻辑对于可重试的异常如网络超时可以实现简单的重试机制。使用更成熟的消息库对于大型项目可以考虑使用MediatR等成熟的进程内消息库它们提供了更丰富的功能如请求/响应、管道行为等。考虑使用Channel进行生产消费对于高吞吐量的数据流可以用System.Threading.Channels替代简单的回调实现更高效的生产者-消费者模式。为每个窗口任务建立上下文在WindowManager中维护一个Dictionarystring, ListGuid记录每个窗口发起的任务ID便于窗口关闭时批量取消。进行单元测试业务逻辑模块和TaskEngine等核心服务应易于进行单元测试。通过模拟IEventAggregator和IUiThreadDispatcher可以测试其逻辑。9. 总结与扩展方向本文构建的不仅仅是一个代码示例而是一个可扩展的多线程多窗口应用框架模型。它通过清晰的模块划分业务、任务、窗口、消息解决了紧耦合问题通过消息总线实现了彻底解耦并通过统一的线程调度器保障了UI线程安全。你可以在此基础上进行深度扩展任务依赖与调度让TaskEngine支持有依赖关系的任务链。任务进度报告定义ProgressMessage让后台任务能报告精确进度。窗口间直接通信除了通过总线广播也可以实现基于窗口ID的点对点消息。插件化架构将业务模块设计为插件实现动态加载。跨进程通信如果需要与外部进程交互可以将消息总线升级为使用IPC如命名管道、gRPC。这套设计的价值在于它将“多线程多窗口”这个复杂问题分解为一系列职责单一、通过接口通信的模块。当你的应用从监控1个数据源扩展到100个从1个主窗口扩展到包含图表、日志、仪表盘等多个子窗口的复杂系统时这个框架能帮助你保持代码的清晰、可维护和可测试。