aardio 封装 Sunny 网络中间件:3个线程安全陷阱与回调函数正确写法
在 aardio 中调用 Sunny 网络中间件时,多线程环境下的回调函数处理是一个需要特别注意的技术难点。本文将深入剖析三个常见的线程安全问题,并提供可落地的解决方案。
1. 理解 Sunny 网络中间件的线程模型
Sunny 网络中间件是一个功能强大的跨平台网络分析组件,与 Fiddler 类似。它支持 HTTP/HTTPS/WS/WSS/TCP 等协议的网络分析,无内存泄漏,专为二次开发量身定制。
关键特性:
- 支持获取/修改请求和返回数据
- 支持设置全局代理和指定连接使用特定代理
- 支持多种协议的重定向
- 支持数据解压缩
- 支持主动发送数据
当我们在 aardio 中使用 Sunny DLL 时,必须清楚其回调函数的执行环境:
// 错误示例:使用 raw.tostdcall() // sunny.callback_http = ..raw.tostdcall(function(winform,context,id,msgid,msgtype,mod,url,err,pid){ // // 这里的代码将在主线程执行 // }) // 正确示例:使用 thread.tostdcall() sunny.callback_http = ..thread.tostdcall(function(winform,context,id,msgid,msgtype,mod,url,err,pid){ // 这里的代码将在工作线程执行 }, winform) // 注意传递 owner 参数2. 三个典型线程安全问题及解决方案
2.1 主线程 UI 组件访问问题
问题现象:在回调函数中直接访问主线程的窗口组件会导致程序崩溃或界面冻结。
错误示例:
sunny.callback_http = ..thread.tostdcall(function(winform,context,id,msgid,msgtype,mod,url,err,pid){ winform.edit.text = url // 直接访问UI组件,危险! })解决方案:使用owner参数安全访问主线程资源:
sunny.callback_http = ..thread.tostdcall(function(owner,context,id,msgid,msgtype,mod,url,err,pid){ owner.form.edit.text = url // 通过owner安全访问 }, {form=winform}) // 传递包含form的对象作为owner原理对比表:
| 访问方式 | 安全性 | 执行线程 | 推荐程度 |
|---|---|---|---|
| 直接访问 | 不安全 | 工作线程 | ❌ 禁止使用 |
| 通过owner访问 | 安全 | 主线程 | ✅ 推荐使用 |
| win.delay调用 | 较安全 | 主线程 | ⚠️ 特殊情况使用 |
2.2 内存管理陷阱
问题现象:Sunny DLL 返回的指针数据如果没有正确释放会导致内存泄漏。
典型场景:
sunny.callback_http = ..thread.tostdcall(function(owner,context,id,msgid,msgtype,mod,url,err,pid){ var pData = sunny.GetResponseBody(id) // 获取指针 var data = ..raw.tostring(pData) // 转换为字符串 // 忘记释放 pData 导致内存泄漏 })正确做法:
sunny.callback_http = ..thread.tostdcall(function(owner,context,id,msgid,msgtype,mod,url,err,pid){ var pData = sunny.GetResponseBody(id) var data = ..raw.tostring(pData) sunny.Free(pData) // 必须释放内存 // 更安全的封装方式 function safeGetBody(id){ var p = sunny.GetResponseBody(id) var s = ..raw.tostring(p) sunny.Free(p) return s } var safeData = safeGetBody(id) }, winform)2.3 回调函数重入问题
问题现象:当网络请求密集时,回调函数可能被多个线程同时调用,导致数据竞争。
防护方案:
// 使用线程安全的数据结构 var threadSafeList = ..thread.list() sunny.callback_http = ..thread.tostdcall(function(owner,context,id,msgid,msgtype,mod,url,err,pid){ // 使用线程安全容器 threadSafeList.add({ id=id, url=url, time=..time.now() }) // 或者使用临界区保护 ..thread.lock("callback_lock", function(){ // 临界区代码 owner.form.log = url }) }, winform)3. 回调函数最佳实践模板
基于上述问题,我们总结出一个线程安全的回调函数模板:
/** * Sunny 网络回调安全模板 * @param owner 拥有者对象,必须包含form属性指向主窗口 * @param context 上下文 * @param id 连接ID * @param msgid 消息ID * @param msgtype 消息类型 * @param mod 传输模式 * @param url 请求URL * @param err 错误信息 * @param pid 进程ID */ var safeCallbackTemplate = function(owner, context, id, msgid, msgtype, mod, url, err, pid){ // 1. 线程安全的数据收集 var logEntry = { id=id, url=url, time=..time.now(), type=msgtype } // 2. 安全访问主线程UI owner.form.invoke( function(){ owner.form.vlist.addRow( "[@rowindex]", "http", godking.sunny.msgType.getName_http(msgtype), mod, url ) owner.form.vlist.ensureVisible(owner.form.vlist.count) } ) // 3. 安全处理请求/响应数据 if(msgtype == godking.sunny.msgType.http_request){ var request = godking.sunny.httpRequest(msgid) var contentType = request.getHeader("Content-Type") // 示例:修改图片请求 if(owner.form.rb1.checked && url.find("@@.(jpg|jpeg|png)$")){ request.setUrl("http://example.com/replacement.jpg") } } // 4. 内存安全示例 if(msgtype == godking.sunny.msgType.http_response){ var response = godking.sunny.httpResponse(msgid) var len = response.getBodyLen() if(len > 0){ var pBody = response.getBody() var body = ..raw.tostring(pBody, 1, len) response.free(pBody) // 必须释放 // 处理body数据... } } } // 使用模板 sunny.callback_http = ..thread.tostdcall(safeCallbackTemplate, {form=winform})4. 高级调试技巧
当遇到难以定位的线程问题时,可以采取以下调试策略:
调试方法对比表:
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 日志记录 | 所有场景 | 不影响性能 | 需要分析日志 |
| 线程堆栈 | 死锁问题 | 直接定位问题 | 需要调试符号 |
| 内存检测 | 内存泄漏 | 准确发现泄漏 | 影响性能 |
| 简化复现 | 复杂问题 | 隔离问题 | 可能无法复现 |
具体实施:
- 增强日志记录:
var debugLog = ..io.open("sunny_debug.log", "a+") sunny.callback_http = ..thread.tostdcall(function(owner, context, id, msgid, msgtype, mod, url, err, pid){ debugLog.writeLine(..string.format( "[%s] Thread:%d ID:%d URL:%s", ..time.now(), ..thread.getId(), id, url )) // ...其余代码... }, {form=winform, log=debugLog})- 使用 aardio 的线程调试工具:
// 在回调开始和结束处添加标记 ..thread.set("callback_running", true) // ...回调处理代码... ..thread.set("callback_running", false) // 在其他线程中可以检查状态 if(..thread.get("callback_running")){ console.log("警告:回调函数执行时间过长") }- 内存检测技巧:
var startMem = ..process.memory() // 执行可疑代码 var endMem = ..process.memory() console.log("内存变化:", endMem-workingSet-startMem)5. 性能优化建议
在多线程环境下,性能优化同样重要:
- 减少回调中的内存分配:
// 不好的做法:每次回调都创建新对象 var temp = {} // 好的做法:复用对象 var threadLocal = ..thread.local({ buffer = ..raw.buffer(1024) }) sunny.callback_http = ..thread.tostdcall(function(owner, context, id, msgid, msgtype, mod, url, err, pid){ var buf = threadLocal.buffer // 使用buf处理数据... }, winform)- 批量处理请求:
var requestBatch = ..thread.list() sunny.callback_http = ..thread.tostdcall(function(owner, context, id, msgid, msgtype, mod, url, err, pid){ requestBatch.add({id=id, url=url}) // 每100个请求或1秒处理一次 if(requestBatch.count >= 100 || ..time.now()-lastProcess > 1000){ processBatch(requestBatch) requestBatch.clear() lastProcess = ..time.now() } }, winform)- 连接池管理:
var connPool = { maxSize = 100, pool = ..table.list(), get = function(){ if(this.pool.count > 0){ return this.pool.pop() } return createNewConnection() }, release = function(conn){ if(this.pool.count < this.maxSize){ this.pool.push(conn) }else{ conn.close() } } }通过以上方案,我们可以在 aardio 中安全高效地使用 Sunny 网络中间件,充分发挥其强大的网络分析能力,同时避免多线程环境下的常见陷阱。