C# WinForms串口事件调用HttpClient接口异常缓慢问题
串口事件中调用Web服务异常缓慢的原因及解决办法
问题场景
WinForms窗体的SerialPort_DataReceived事件(运行在独立线程)中调用Web服务接口耗时45-60秒,但同样的接口在Node.js中调用仅需约10秒,在窗体按钮点击事件中调用同一方法仅需11秒。
串口事件代码
private async void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e) { SerialPort sp = (SerialPort)sender; string indata = sp.ReadLine().Replace("?","").Replace(" ",""); if(indata != null) { var weightMatch = Regex.Match(indata, @"(\d+\.\d+)kg$"); var weight = weightMatch.Groups[1].Value; var message = barcode + " Weight:" + weight; this.Invoke(new Action(() => { scanData.Add(new ScanDataItem(scanData.Count + 1, message, "")); })); this.Invoke(new Action(() => { mainList.SelectedIndex = mainList.Items.Count - 1; })); string jsonData = ""; // 以下代码耗时比预期多30秒 - 内部调用Web服务 var jsonString = await TrayWeighScan.doOrder(barcode, Decimal.Parse(weight)); try { var jsonDoc = JsonDocument.Parse(jsonString); jsonData = JsonSerializer.Serialize(jsonDoc, new JsonSerializerOptions { WriteIndented = true }); } catch (Exception) { jsonData = jsonString; } this.Invoke(new Action(() => { scanData.Last().responseData = jsonData; })); this.Invoke(new Action(() => { responseTextBox.Text = jsonData; })); this.Invoke(new Action(() => { statusTextBox.Text = "Ready to Scan"; })); this.Invoke(new Action(() => { busy = false; })); } } public class TrayWeighScan { //private static readonly HttpClient httpClient = new(); public static async Task<string> doOrder(string orderNumber, decimal weight ) { HttpClient httpClient = new(); var url = "http://r2hserver/logistics/weighscan?orderNumber="+orderNumber+"&weight="+weight.ToString("F2"); HttpResponseMessage response = await httpClient.GetAsync(url); string responseText = await response.Content.ReadAsStringAsync(); return responseText; } }
按钮点击事件代码
private async void btnUrlTest_Click(object sender, EventArgs e) { string orderNumber = "ORD100302338"; decimal weight = 1.606m; //以下代码耗时11秒 var a = await TrayWeighScan.doOrder(orderNumber,weight); var b = ""; }
核心原因分析
HttpClient频繁创建导致连接无法复用
TrayWeighScan.doOrder方法每次调用都新建HttpClient实例,而HttpClient设计为可复用的单例对象。频繁创建实例会导致TCP连接无法复用,每次请求都要重新完成三次握手、TLS协商(如果是HTTPS)等流程,大幅增加请求耗时。按钮事件执行频率低,系统连接池资源充足,所以耗时不明显;但串口事件可能触发更频繁,连接池资源被快速耗尽,导致请求等待时间剧增。多次UI线程切换的额外开销
串口事件线程是IO线程池线程,代码中多次调用this.Invoke切换到UI线程执行更新操作,频繁的上下文切换会累积额外的性能开销,进一步拉长整体耗时。
解决办法
1. 复用HttpClient单例
修改TrayWeighScan类,使用静态单例的HttpClient,并添加ConfigureAwait(false)减少线程上下文切换:
public class TrayWeighScan { // 静态单例HttpClient,复用TCP连接 private static readonly HttpClient httpClient = new(); public static async Task<string> doOrder(string orderNumber, decimal weight ) { // 对URL参数进行转义,避免特殊字符导致请求异常 var url = $"http://r2hserver/logistics/weighscan?orderNumber={Uri.EscapeDataString(orderNumber)}&weight={weight.ToString("F2")}"; // ConfigureAwait(false) 避免await后回到原线程上下文,减少切换开销 HttpResponseMessage response = await httpClient.GetAsync(url).ConfigureAwait(false); // 可选:确保响应成功,否则抛出异常便于排查问题 response.EnsureSuccessStatusCode(); string responseText = await response.Content.ReadAsStringAsync().ConfigureAwait(false); return responseText; } }
2. 合并UI线程更新操作
将多次Invoke调用合并为一次,减少上下文切换次数:
// 合并前的多次Invoke // this.Invoke(new Action(() => { scanData.Add(...); })); // this.Invoke(new Action(() => { mainList.SelectedIndex = ...; })); // 合并后的单次Invoke this.Invoke(new Action(() => { scanData.Add(new ScanDataItem(scanData.Count + 1, message, "")); mainList.SelectedIndex = mainList.Items.Count - 1; })); // ... // 调用Web服务后的UI更新也合并 this.Invoke(new Action(() => { scanData.Last().responseData = jsonData; responseTextBox.Text = jsonData; statusTextBox.Text = "Ready to Scan"; busy = false; }));
验证效果
修改后,串口事件中调用Web服务的耗时会大幅降低,接近按钮事件和Node.js调用的耗时水平,主要原因是TCP连接复用消除了重复建立连接的开销,同时减少了UI线程切换的额外消耗。
内容的提问来源于stack exchange,提问作者Martin Thompson
相关产品推荐
相关产品推荐

