win.involk设置线程超时时间

By popy32 at 2021-12-23 • 0人收藏 • 176人看过

编写多线程界面程序时,win.involk可以说是经常用到的函数,多线程带返回值,不卡界面。

import win.ui;
/*DSG{{*/
var winform = win.form(text="aardio form";right=319;bottom=159)
winform.add(
button={cls="button";text="访问网页";left=200;top=112;right=304;bottom=144;z=1};
edit={cls="edit";left=8;top=8;right=304;bottom=104;edge=1;multiline=1;z=2}
)
/*}}*/

showHttpData = function(){
	var r = win.invoke(
		function(){
			import win;
			import inet.http;
			var url = "https://www.qq.com";
			var ohttp = inet.http();
			var d = ohttp.get(url);
			var t = d;//web.json.tryParse(d);
			return t;
		} 
	)
	if(r == null ) return "";
	return r;
}

winform.button.oncommand = function(id,event){
	winform.button.disabled = true;
	winform.edit.text = showHttpData();
	winform.button.disabled = false;
}

winform.show();
win.loopMessage();


但有个需求是线程超时返回,例如访问网页指定超时退出(当然inet可以配置超时参数)。如果遇到线程卡住的时候

import win.ui;
/*DSG{{*/
var winform = win.form(text="aardio form";right=319;bottom=159)
winform.add(
button={cls="button";text="访问网页";left=200;top=112;right=304;bottom=144;z=1};
edit={cls="edit";left=8;top=8;right=304;bottom=104;edge=1;multiline=1;z=2}
)
/*}}*/

showHttpData = function(){
	var r = win.invoke(
		function(){
			import win;
			win.delay(4500);
			return "4500ms";
		} 
	)
	if(r == null ) return "";
	return r;
}

winform.button.oncommand = function(id,event){
	winform.button.disabled = true;
	winform.edit.text = showHttpData();
	winform.button.disabled = false;
}

winform.show();
win.loopMessage();


关注到thread.wait函数(lib/preload/thread.aardio)是自带超时退出的,于是试着给win.involk加上超时参数。


win.involk的实现

win.involk 实际调用的是 thread.invokeAndWait。相关的调用栈为

invokeAndWait -> waitOne -> threadwait

可以注意到threadwait实际上由超时处理的,只是invokeAndWait并没有传入超时参数,于是尝试重写invokeAndWait,增加参数超时时间timeout

namespace thread {
    var _id_invoke = {};
    invokeAndWait2 = function(timeout, func,... ){
    	var id = ..table.push(_id_invoke,1);
    	var name = ..string.format("$(_winvoke:[tid:%d][%d]",..thread.getId(),id );
	
		var h = create(
			function(func,name,...){ 
				..thread.set(name, null);
				var ret = { func(...) }
				..thread.set(name, ret);
			},func,name,...
		); 
		
		if(timeout == 0) {
		    timeout = null;
		}
		
		waitOne(h, timeout); 
	 	//wait(h, timeout); 
	 	//waitClose(h, timeout); 
	 	
		var ret = get(name);
		_id_invoke[id] = null;
		..raw.closehandle(h);
	 	
		if(ret){
			return ..table.unpackArgs(ret);
		}
	}
}


完整代码

import win.ui;
/*DSG{{*/
var winform = win.form(text="aardio form";right=319;bottom=159)
winform.add(
button={cls="button";text="访问网页";left=200;top=112;right=304;bottom=144;z=1};
edit={cls="edit";left=8;top=8;right=304;bottom=104;edge=1;multiline=1;z=2}
)
/*}}*/

namespace thread {
	var _id_invoke = {};
    invokeAndWait2 = function(timeout, func,... ){
    	var id = ..table.push(_id_invoke,1);
    	var name = ..string.format("$(_winvoke:[tid:%d][%d]",..thread.getId(),id );
	
		var h = create(
			function(func,name,...){ 
				..thread.set(name, null);
				var ret = { func(...) }
				..thread.set(name, ret);
			},func,name,...
		); 
		
		if(timeout == 0) {
		    timeout = null;
		}
		
		waitOne(h, timeout); 
	 	//wait(h, timeout); 
	 	//waitClose(h, timeout); 
	 	
		var ret = get(name);
		_id_invoke[id] = null;
		..raw.closehandle(h);
	 	
		if(ret){
			return ..table.unpackArgs(ret);
		}
	}
}

import time.performance;
showHttpData = function(){
	var tk1 = time.performance.tick();
	var r = thread.invokeAndWait2( 500, 
		function(){
			import win;
			win.delay(4500);
			return "";
		} 
	)
	var tk2 = time.performance.tick();
	winform.edit.print( tostring(tk2 - tk1) ,"ms");
	
	if(r == null ) return "";
	return r;
}

winform.button.oncommand = function(id,event){
	winform.button.disabled = true;
	//winform.edit.text = "";
	showHttpData()
	//winform.edit.print(  );
	winform.button.disabled = false;
}

winform.show();
win.loopMessage();


改进后的invokeAndWait2确实可以超时退出了,同时窗口也不会卡死,说明能响应窗口消息,但不停的拖放窗口,移动鼠标这些操作(相当于不停的发送)后发现,invokeAndWait2不能按照指定的超时时间timeout返回,需要等鼠标稳定后很长时间才能返回。


invokeAndWait2设置的是500ms,但实际和500ms有较大偏差


invokeAndWait2.gif


改进threadwait


继续研究threadwait的处理逻辑,threadwait主要过程是参数解析(获得需要wait的线程,手动自动handle),涉及消息处理的是API是 msgWaitForMultipleObjects,如果在threadwait过程中一种有窗口消息,流程将在do while中不断循环。


这时如果msgWaitForMultipleObjects以经过timeout超时后返回,又将在循环里继续运行,导致重复的timeout时间。处理方式也很简单,用last记录剩余的超时时间,time.performance计算实际运行的时间。


// 已修改过的threadwait 
var threadwait = function( bClose,bAll, ...){
    var threads,timeout = ...;
    if(type(threads)!="table") {
    	timeout = 0xFFFFFFFF
    	threads ={...}
    	if( type(threads[#threads]) == "number" ){
    		timeout = ..table.pop(threads,1)
    	}
    }
    elseif(timeout === null ) timeout = 0xFFFFFFFF  /* Infinite timeout*/
    	
    var len = #threads
    if(!len) error("参数未指定线程句柄",3);
    var threads_c = ..raw.toarray( threads ,"pointer" ,"array");
    var msg,peek,parse,hasMsg;  
    
    var last = timeout; //剩余
    
    var ret; 
    if( (!bAll) && ..win[["_form"]] ){
    	msg = ::MSG();
		parse = ..win._parseMessage;
		delay = ..win.delay;
		peek = ..__messagePeek; 
		
    	do{
    	                var tk1 = ..time.performance.tick();
			ret = msgWaitForMultipleObjects(len,threads_c,bAll,last , 0x4FF/*_QS_ALLINPUT*/); 
			//
			
/*
			last = last - ..math.floor(tk2 - tk1);
			
			if(last <= 0) {
				break;
				//..win.quitMessage(msg.wParam);
				//return null;
			}
*/
			
			if( ret!=len ) break;
			hasMsg =  peek(msg); 
			if(hasMsg) {
				parse(msg);
				//delay(100);
			}
			elseif(hasMsg===null){
				..win.quitMessage(msg.wParam);
			}
			var tk2 = ..time.performance.tick();
			last = last - ..math.floor(tk2 - tk1);
			
			if(last <= 0) {
				break;
			}
		}while( hasMsg!==null) 
    }  
    else {
    	ret = waitForMultipleObjects(len,threads_c,bAll,timeout, 0x4FF/*_QS_ALLINPUT*/);  
    }
    // 后面的代码省略


改进后的效果


invokeAndWait21.gif


效果比之前好了一些,和期望的timeout更接近,但窗口有太多消息时仍然会一直等待到消息结束,大家如果有更好的方案欢迎讨论。



6 个回复 | 最后更新于 2021-12-24
2021-12-23   #1

win.invoke带个win就是因为工作在窗口线程, 会被消息阻塞, 你这个时候就需要直接用thread.create, thread.wait就好了吧, 也不卡界面, 干嘛一定要用win.invoke? 在界面线程里工作?

2021-12-23   #2

回复#1 @admin :


文章里写了,win.invoke实际调用的时 thread.invokeAndWait,并不是窗口线程处理事情。


本文重点其实是想分享,线程超时退出的实现。标题不限于win.invoke设置超时,换成thread.involk,thread.create都可以。


另外thread.wait 测试中卡了界面 ,只能用thread.waitone,原理就在thread.threadwait这个函数。窗口事件,超时的实现都在thread.threadwait。

2021-12-23   #3

回复#2 @popy32 :

你的分享棒棒的,非常感谢。

admin 的意思不是指窗口线程处理事情 —— 这个他肯定懂的,他是说用 win.invoke , thread.waitOne 函数的目的是为了执行工作线程的同时 —— 可以响应界面线程,而你在试图消除这个处理消息的耗时,但如果是这样 ——  不用这个函数创建的线程就不会受到界面线程的干扰。 

thread.invokeAndWait 就是为了获取到线程的返回值,没有获得返回值就提前返回 —— 可能也不需要用这个函数。当然 thread.invokeAndWait2 就没问题,让大家自由扩展 —— 本来就是我们开源标准库的初衷。

至于你说的计时器误差,在一般的软件界面里多等几毫秒少等几毫秒 —— 用户应当不会有感觉,也不可能真有用户会拿鼠标在屏幕上不停地晃来晃去 ------ 晃了半天然后去计算晃与不晃的时差是否存在微小的误差。

当然,如果你的软件确实有这种需求,那么你扩展出自己需要的函数也是极好的,但我要提醒你一下,大量调用 time.performance.tick() —— 这本身就是一种不必要的消耗,如果你很介意代码的耗时,应当避免不必要的函数调用,而不是增加更多的函数调用。

2021-12-23   #4

注意是 win.invoke

不是  win.involk



2021-12-24   #5

这个 thread.waitOne 不是第一次有人纠结要去改这个 timeout 参数 了,

真想改的话,建议新增一个函数,例如增加一个 thread.waitOne2 会更好。


那个被你大手一挥删掉的 delay 函数,说实话这写法至少反复研究了半年。

用户操作以后通常会紧随密集的非输入消息要处理,我们暂停 wait 循环,把优先权使用 delay 这是更好的选择。你可以多写几个更复杂的程序再试试。


thread.waitOne 在界面线程没有校正 timeout 是因为需求小 —— 付出的成本与回报不对称。一些几乎不会被用到的代码,被加进核心函数千万遍地频繁执行,这不是一个好的选择。


追求完美也要具体情况分别对待,例如 plus 控件播放 GIF 的会存在延时误差,

但是 plus 控件却添加了很多代码来校正这个延时,以尽可能减少误差。 

因为这时候回报大于付出成本嘛,而且这也不是核心函数。


标准库嘛 —— 就是大家要和谐与合作,

你要往东,我要往西,这个事就很尴尬了,每次更新你都得去替换那多累呀是不?!


2021-12-24   #6

写标准库不是最累的,最累的是要解释清楚每一句代码,开源不容易啊。

登录后方可回帖

登 录
信息栏
本站永久域名:HtmLayout.Cn
aardio可以快速开发上位机,本站主要记录了学习过程中遇到的问题和解决办法及aardio代码分享

这里主要专注于aardio学习交流和经验分享.
纯私人站,当笔记本用的,学到哪写到哪.

AARDIO语言QQ群:70517368
Aardio 官方站:Aardio官方
Aardio最新功能:Aardio官方更新日志
C大Aardio论坛:Aar爱好者论坛
本 站 主 站:Stm32cube中文网
Sciter中文在线文档Sciter在线学习文档
赞助商:才仁机械
Loading...