C#异步线程改造市场API机器人:解决阻塞与并发买卖需求
如何将商品买卖机器人的阻塞同步方法改为异步并发执行?
我开发了一款类似商品买卖市场机器人的程序,通过在线API发送/接收POST请求与响应,功能正常但运行时会阻塞当前线程,导致无法操作程序其他功能。我希望将
AttachAndBuy、buyOrder函数改为异步执行,未来还需添加可并发执行的卖出功能,相关代码如下:
private void RunBot() { numToBuy = (int)nudNumToBuy.Value; for (int i = 0; i < numToBuy; i++) { while (true) { if (AttachAndBuy()) { break; } } } } private bool AttachAndBuy() { string data = "<DATA HERE>"; string URL = "<URL HERE>"; Cookies.SetCookies(cookie, "PHPSESSID=" + SessionIDTextBox.Text.Replace("PHPSESSID=", "")); HttpWebRequest Request = (HttpWebRequest)WebRequest.Create(URL); Request.ContentType = "application/json"; Request.Accept = "*/*"; Request.CookieContainer = Cookies; Request.Host = "<HOST HERE>"; Request.Method = "POST"; Request.UserAgent = "<USER AGENT HERE>"; Request.Headers.Add("<HEADER>", "<VALUE>"); Request.KeepAlive = true; byte[] CompressedRequest = SimpleZlib.CompressToBytes(Encoding.UTF8.GetBytes(data), 9); Stream RequestStream = Request.GetRequestStream(); RequestStream.Write(CompressedRequest, 0, CompressedRequest.Length); RequestStream.Flush(); Stream CompressedResponseStream = Request.GetResponse().GetResponseStream(); byte[] CompressedResponseData = ReadToEnd(CompressedResponseStream); string DecompressedResponseData = SimpleZlib.Decompress(CompressedResponseData, null); OffersResponse Return = Json.Deserialize<OffersResponse>(DecompressedResponseData); int LowestCost = 1000000000; Offer BestOffer = new Offer(); foreach (Offer CurrentOffer in Return.data.offers) { bool moneyOffer = false; int Costs = CurrentOffer.requirementsCost; string id = CurrentOffer._id; foreach (Requirement CurrentRequirement in CurrentOffer.requirements) { if (CurrentRequirement._tpl == "<TEMPLATE ID HERE>") { moneyOffer = true; } } if (moneyOffer == false) { continue; } if (Costs < LowestCost) { LowestCost = Costs; BestOffer = CurrentOffer; } } BestOfferID = BestOffer._id; BestOfferCost = LowestCost; string MoneyID = getStack(BestOfferCost); while (true) { BuyRequestAttemptCounter++; if (LowestCost > 140000) { AddLog("No Suitable Item! Skipping! Lowest Item Cost: " + LowestCost.ToString()); return false; } else AddLog("Best Item Cost: " + LowestCost.ToString() + " | ID: " + BestOfferID); int Result = buyOrder(MoneyID); if (Result == 0) { //log info for averaging numberPurchased++; TotalCost += BestOfferCost; averageCost = TotalCost / numberPurchased; lblNumPurchased.Text = numberPurchased.ToString(); lblAverageCost.Text = averageCost.ToString(); lstPricesPurchased.Items.Add(LowestCost.ToString()); AddLog("===================================="); AddLog("Number Purchased: " + numberPurchased); AddLog("Average Cost: " + averageCost); AddLog("===================================="); System.Media.SystemSounds.Exclamation.Play(); return true; } else if (Result == 1) return false; else if (Result == 2) continue; else return false; } } private int buyOrder(string MoneyID) { string data = "<DATA HERE>"; string URL = "<URL HERE>"; Cookies.SetCookies(cookie, "PHPSESSID=" + SessionIDTextBox.Text.Replace("PHPSESSID=", "")); HttpWebRequest Request = (HttpWebRequest)WebRequest.Create(URL); Request.ContentType = "application/json"; Request.Accept = "*/*"; Request.CookieContainer = Cookies; Request.Host = "<HOST HERE>"; Request.Method = "POST"; Request.UserAgent = "<USER AGENT HERE>"; Request.Headers.Add("<HEADER>", "<VALUE>"); Request.KeepAlive = true; byte[] CompressedRequest = SimpleZlib.CompressToBytes(Encoding.UTF8.GetBytes(data), 9); Stream RequestStream = Request.GetRequestStream(); RequestStream.Write(CompressedRequest, 0, CompressedRequest.Length); RequestStream.Flush(); Stream CompressedResponseStream = Request.GetResponse().GetResponseStream(); byte[] CompressedResponseData = ReadToEnd(CompressedResponseStream); string DecompressedResponseData = SimpleZlib.Decompress(CompressedResponseData, null); ResponseRoot Return = Json.Deserialize<ResponseRoot>(DecompressedResponseData); string returnErrorCode = DecompressedResponseData.ToString(); //AddLog(DecompressedResponseData); if (Return.err == 0 && returnErrorCode.Contains("id")) { System.Windows.Forms.Clipboard.SetText(DecompressedResponseData); //AddLog("Successful Purchase!"); return 0; } else if (returnErrorCode.Contains("1503")) { //AddLog("Failed with 1503!"); return 1; } else if (returnErrorCode.Contains("1512")) { // AddLog("Failed with 1512!"); return 2; } return 3; }
当然可以实现!你遇到的阻塞问题本质是同步IO操作占满了UI线程,改用C#的async/await异步模型就能轻松解决,还能为后续的并发卖出功能打好基础。下面是完整的改造方案:
核心思路:用异步HTTP客户端替代同步请求,释放UI线程
首先推荐把老旧的HttpWebRequest换成HttpClient——它原生支持异步,代码更简洁,还能复用连接池,适合频繁API调用的场景。
1. 初始化全局HttpClient实例
在窗体类里定义一个静态的HttpClient,避免每次请求都创建新实例(会导致端口耗尽):
private static readonly HttpClient _httpClient = new HttpClient(); public YourFormName() { InitializeComponent(); // 预先设置通用请求头和配置 _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*")); _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("<USER AGENT HERE>"); _httpClient.DefaultRequestHeaders.Add("<HEADER>", "<VALUE>"); _httpClient.DefaultRequestHeaders.Host = "<HOST HERE>"; }
2. 把buyOrder改成异步方法
将所有同步IO操作替换为异步版本,返回Task<int>:
private async Task<int> buyOrderAsync(string moneyID) { string data = "<DATA HERE>"; string url = "<URL HERE>"; // 更新Cookie(如果SessionID可能动态变化,这里要每次都设置) _httpClient.DefaultRequestHeaders.Remove("Cookie"); _httpClient.DefaultRequestHeaders.Add("Cookie", $"PHPSESSID={SessionIDTextBox.Text.Replace("PHPSESSID=", "")}"); // 压缩请求数据 byte[] compressedRequest = SimpleZlib.CompressToBytes(Encoding.UTF8.GetBytes(data), 9); using var content = new ByteArrayContent(compressedRequest); content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); try { using var response = await _httpClient.PostAsync(url, content); response.EnsureSuccessStatusCode(); // 自动抛出HTTP错误状态码异常 // 读取并解压响应 byte[] compressedResponse = await response.Content.ReadAsByteArrayAsync(); string decompressedResponse = SimpleZlib.Decompress(compressedResponse, null); ResponseRoot returnObj = Json.Deserialize<ResponseRoot>(decompressedResponse); // 判断返回结果 if (returnObj.err == 0 && decompressedResponse.Contains("id")) { Clipboard.SetText(decompressedResponse); return 0; } else if (decompressedResponse.Contains("1503")) { return 1; } else if (decompressedResponse.Contains("1512")) { return 2; } return 3; } catch (HttpRequestException ex) { AddLog($"购买请求出错: {ex.Message}"); return 3; } }
3. 改造AttachAndBuy为异步方法
同样改成Task<bool>,内部调用异步的buyOrderAsync,还要注意UI线程安全——异步线程不能直接操作UI控件,必须用Invoke切换到UI线程:
private async Task<bool> AttachAndBuyAsync() { string data = "<DATA HERE>"; string url = "<URL HERE>"; // 更新Cookie _httpClient.DefaultRequestHeaders.Remove("Cookie"); _httpClient.DefaultRequestHeaders.Add("Cookie", $"PHPSESSID={SessionIDTextBox.Text.Replace("PHPSESSID=", "")}"); byte[] compressedRequest = SimpleZlib.CompressToBytes(Encoding.UTF8.GetBytes(data), 9); using var content = new ByteArrayContent(compressedRequest); content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); try { using var response = await _httpClient.PostAsync(url, content); response.EnsureSuccessStatusCode(); byte[] compressedResponse = await response.Content.ReadAsByteArrayAsync(); string decompressedResponse = SimpleZlib.Decompress(compressedResponse, null); OffersResponse returnObj = Json.Deserialize<OffersResponse>(decompressedResponse); // 筛选最优报价 int lowestCost = int.MaxValue; Offer bestOffer = null; foreach (var currentOffer in returnObj.data.offers) { // 简化判断逻辑,用LINQ替代嵌套循环 bool isMoneyOffer = currentOffer.requirements.Any(req => req._tpl == "<TEMPLATE ID HERE>"); if (!isMoneyOffer) continue; if (currentOffer.requirementsCost < lowestCost) { lowestCost = currentOffer.requirementsCost; bestOffer = currentOffer; } } if (bestOffer == null) { AddLog("未找到符合条件的商品"); return false; } BestOfferID = bestOffer._id; BestOfferCost = lowestCost; string moneyID = getStack(lowestCost); if (lowestCost > 140000) { AddLog($"无合适商品!跳过!最低价格: {lowestCost}"); return false; } AddLog($"最优商品价格: {lowestCost} | ID: {BestOfferID}"); // 循环尝试购买 while (true) { BuyRequestAttemptCounter++; int result = await buyOrderAsync(moneyID); switch (result) { case 0: // 必须用Invoke切换到UI线程更新控件 Invoke((Action)(() => { numberPurchased++; TotalCost += lowestCost; averageCost = TotalCost / numberPurchased; lblNumPurchased.Text = numberPurchased.ToString(); lblAverageCost.Text = averageCost.ToString(); lstPricesPurchased.Items.Add(lowestCost.ToString()); })); AddLog("===================================="); AddLog($"已购买数量: {numberPurchased}"); AddLog($"平均成本: {averageCost}"); AddLog("===================================="); SystemSounds.Exclamation.Play(); return true; case 1: return false; case 2: continue; default: return false; } } } catch (HttpRequestException ex) { AddLog($"获取报价请求出错: {ex.Message}"); return false; } }
4. 改造RunBot为异步启动方法
原来的同步RunBot会阻塞UI线程,改成异步后可以选择串行或并发执行购买任务:
// 假设这是按钮点击事件(替换原来的RunBot调用) private async void btnStartBot_Click(object sender, EventArgs e) { // 禁用按钮防止重复触发 btnStartBot.Enabled = false; try { numToBuy = (int)nudNumToBuy.Value; // 方式1:串行购买(一个接一个,保持原有逻辑但不阻塞UI) for (int i = 0; i < numToBuy; i++) { bool success = false; while (!success) { success = await AttachAndBuyAsync(); } } // 方式2:并发购买(同时执行多个任务,适合快速批量操作) // List<Task> purchaseTasks = new List<Task>(); // for (int i = 0; i < numToBuy; i++) // { // purchaseTasks.Add(Task.Run(async () => // { // bool success = false; // while (!success) // { // success = await AttachAndBuyAsync(); // } // })); // } // await Task.WhenAll(purchaseTasks); } finally { // 无论成功失败都恢复按钮状态 btnStartBot.Enabled = true; } }
5. 未来添加并发卖出功能的注意事项
- 卖出方法的改造逻辑和购买完全一致,改成
async Task即可 - 用
Task.WhenAll或.NET 6+的Parallel.ForEachAsync实现并发卖出 - 线程安全:如果多个任务同时修改共享变量(比如卖出数量、收益),要用
lock或者Interlocked类保证原子操作 - UI更新:所有UI操作必须通过
Invoke切换到UI线程,避免跨线程异常
内容的提问来源于stack exchange,提问作者Ignition K6
相关产品推荐
相关产品推荐

