背景

当你点击EXE文件系统一个应用程序的时候 - 系统会创建一个进程(process)
而在一个进程内可以包含多个线程(thread)。用来显示界面的线程,我们通常称为“界面线程”,
其他不是用来显示界面的线程,我们一般称为“工作线程”或者是“后台线程”。

界面线程会使用 win.loopMessage(); 启动一个消息循环,
win.loopMessage(); 就象一个快递公司不知疲倦的收发消息,直到用户关闭最后一个窗口他才会退出。
当然你也可以使用 win.quitMessage() 退出消息循环。

下面是一个启动界面线程的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import win.ui;
/*DSG{{*/
var winform = win.form(text="aardio form";right=759;bottom=469)
winform.add(
button={cls="button";text="耗时操作"; ...}
)
/*}}*/

//用户点击窗口上的按钮时会触发下面的回调函数
winform.button.oncommand = function(id,event){
//下面用sleep函数休眠5秒(5000毫秒)模拟耗时操作
sleep(5000)
}

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

你可以看到一个窗体显示在屏幕上,如果你去掉代码中的最后一句 win.loopMessage();那么窗体只会显示一下就消失了,你的程序也迅速退出了。

但如果你加上 win.loopMessage(); 窗体就会一直显示在屏幕上(直到你点击关闭按钮)。
并且你可以做其他的操作,例如点击按钮。

我们尝试点击按钮,点击按钮后触发了 winform.button.oncommand() 函数,
一件让我们困惑的事发生了,窗体卡死了任何操作都没有反应,这是因为类似 sleep(5000) 这样的耗时操作阻塞了win.loopMessage()启动的消息循环过程。

一种解决方法是把 sleep(5000)改成 win.delay(5000),虽然他们同样都是延时函数,但是win.delay()会同时继续处理窗口消息。但如果我们不只是延时还要做其他耗时的操作,那就需要启动一个新的线程。

创建线程

一个线程会排队执行一系列的编程指令,但一个线程同时只能做一件事。
例如在界面上有耗时的操作在执行时 - 就不能同时处理其他的界面消息或者响应用户的操作。
这时候我们就要使用多线程来完成我们的任务。

我们假设有一个耗时操作是这样的:

1
2
3
4
5
6
7
8
9
10
11
//下面这个函数执行耗时操作
doSomething = function( str ){

for(i=1;100){
str = str + " " + i;
sleep(100)
}

return 123;

}

一般我们直接调用这个函数会是这样写:

1
doSomething( "也可以有参数什么的" )

如果希望写复杂一点调用这个函数,我们也可以这样写:

1
invoke(doSomething ,,"也可以有参数什么的" )

如果我们希望创建一个新的线程来调用这个函数,那么就需要下面这样写:

1
thread.invoke(doSomething ,"也可以有参数什么的" )

切记不要犯一个低级错误:
如果把创建线程的代码改为 thread.invoke( doSomething("也可以有参数什么的") )
这是在创建线程前就调用函数了,实际执行的代码是 thread.invoke( 123 ) 这肯定会出错的。

线程的规则与限制

线程有独立的运行上下文,独立的变量环境

多线程最让人困惑的是线程间的同步和交互

线程就象多个在并列的轨道上疾驰的火车,你要在A火车上与B火车上的人交互,或者你想让B火车上的人干什么,你不能直接从火车上把手伸出去跟别的火车上的人拉拉扯扯发生种种亲密的互动。

  • 一种方式是先让所有的火车都停下来,互动完了再继续往前开,需要互动的时候再停下来,这通常需要用到线程同步的锁,在aardio中就是thread.lock(),但实际上在aardio中多线程同步很少需要用到同步锁,所以这里我也就不多讲。
  • 另外一种更先进的方式就是TLS(Thread Local Storage),也就是线程局部存储
    一些编程语言的全局变量是多线程共享的,一次修改多个线程中立即生效,这看起来很方便,其实带来的潜在麻烦会很多,会制造大量混乱的BUG。而aardio自带TLS, 所有变量都是线程局部存储。你不能在多线程间直接共享变量。

交换变量的方法

  1. 如果你有一些函数需要被多个线程用到,请他们写到库文件里,然后在任何线程中使用 import 语句导入即可使用。

  2. 可以在创建线程时,通过线程的启动参数把变量从一个线程传入另一个线程,例如:

1
thread.invoke( 线程启动函数,"给你的","这也是给你的","如果还想要上车后打我电话" )
  1. 多线程共享的变量,必须通过 thread.get() 函数获取,并使用 thread.set() 函数修改其值,thread.table对象对这两个函数做了进一步的封装(伪装成一个普通的表对象)

  2. aardio提供了很多线程间相互调用函数的方法,通过这些调用方式的传参也可以交互变量,具体请查看aardio范例中的多线程范例。

交换变量的规则

在aardio中每个线程有独立的运行上下文、线程有独立的变量环境,有独立的堆栈,所以你不能把包含局部变量闭包的对象从一个线程传到另一个线程,常见的就是调用类创建的对象,因为this就是闭包变量。

另外你也不可以在一个线程中引用库文件,并且把引用的库直接传到另一个线程,因为库文件中通常会大量的使用局部变量闭包,而应该在每个线程中自行导入需要用到的库,一个例子:

1
2
3
4
5
6
7
8
9
10
import console;

thread.invoke(
function(){
import console;
console.log("线程要自己调用 import console;")
}
)

console.pause(true);

调用窗口对象

新手可能不太容易理解,aardio中的这种模式给多线程开发带来了巨大的方便,在aardio的多线程代码中基本很少看到同步锁,也很少会因为同步出现各种BUG和麻烦,以前面并列飞奔的多个火车来比喻,在aardio中每辆火车都只要愉快的往前跑就行了,不存在谁停下来等谁同步的问题。

但不可否认,工作线程中如果能直接操作窗口上的控件那会带来巨大的方便(因为工作线程需要访问界面控件的需求还是非常多的),但这违反了aardio的规则,在aardio的旧版本中这是行不通的,在aardio新版本中,我们愉快的解决了这个问题。现在aardio中可以存在一些特权对象,让一些不能在线程中直接传递的对象可以跨线程传递,例如窗口对象,下面我们看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import win.ui;
var winform = win.form(text="aardio form";right=759;bottom=469)
winform.add(
button={cls="button";text="再来个多线程,可以多点几次不会乱的"; ...};
edit={cls="edit";edge=1;multiline=1;z=1; ...}
)

winform.button.oncommand = function(id,event){
thread.invoke(
function(winform){
for(i=1;10;1){
winform.edit.print( time.tick(),"线程ID:",thread.getId() );
sleep(1000)
}
},winform
)
}

winform.show()
win.loopMessage();

注意上面的线程启动函数直接写在了参数里(匿名函数),跟下面的写法作用是一样的:

1
2
3
4
5
6
7
8
9
10
//下面这个函数执行耗时操作
var doSomething = function( winform ){

for(i=1;10;1){
winform.edit.print( time.tick(),"线程ID:",thread.getId() );
sleep(1000)
}
}

thread.invoke( doSomething,winform ) //启动线程

界面与逻辑分离

在工作线程中直接操作界面控件固然令人愉快,
但如果代码量一大,界面与逻辑混杂在一起,会让代码不必要的变的千头万绪复杂臃肿。

如果把多线程比作多条轨道上并列飞奔的火车,那么火车交互的方法不仅仅只有停下来同步,或者把手伸出车窗来个最直接的亲密交互。一种更好的方式是拿起手机给隔壁火车上的人打个电话 - 发个消息,或者等待对方操作完了再把消息发回来。

这种响应式的编程方式在aardio里就是 thead.command,下面我们看一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import win.ui;
var winform = win.form(text="线程命令";right=599;bottom=399)
winform.add(edit={cls="edit"; ...})

import thread.command;
var listener = thread.command();
listener.print = function( ... ){
winform.edit.print( ... ) //我们在界面线程中这样响应工作线程的消息
}

thread.invoke( //创建工作线程
function(){
import thread.command; //必须在线程函数内部导入需要的库
thread.command.print("hello world",1,2,3); //调用界面线程的命令
}
)

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

thread.command可以把多线程间复杂的消息交互伪装成普通的函数调用,非常的方便。

这里新手仍然可能会困惑一点:我在工作线程中不是可以直接操作界面控件么?! 你这个thread.command虽然好用,但是多写了不少代码呀。

这样去理解是不对的,你开个轮船去对象菜市场买菜固然是有点麻烦,但如果你开轮船去环游世界那你就能感受到它的方便在哪里了。thread.command 一个巨大的优势是让界面与逻辑完全解耦,实现界面与逻辑的完全分离,当你的程序写到后面,代码越来越多,就能感受到这种模式的好处了。

例如 aardio自带的自动更新模块的使用示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import fsys.update.dlMgr;
var dlMgr = fsys.update.dlMgr(
"http://update.aardio.com/api/v1/version.txt","/download/update-files")

dlMgr.onError = function(err,filename){
//错误信息 err,错误文件名 filename 这里可以不用做任何处理,因为出错了就是没有升级包了
}

dlMgr.onConfirmDownload = function(isUpdated,appVersion,latestVersion,description){
if( ! isUpdated ){
//已经是最新版本了
}
else {
//检测到最新版本,版本号 latestVersion
};

return false; //暂不下载
}

dlMgr.create();

这个fsys.update.dlMgr里面就用到了多线程,但是他完全不需要直接操作界面控件。
而你在界面上使用这个对象的时候,你甚至都完全不用理会他是不是多线程,不会阻塞和卡死界面,有了结果你会收到通知,你接个电话就行了压根不用管他做了什么或者正在做什么。

这个fsys.update.dlMgr里面就是使用thread.command实现了实现界面与逻辑分离,你可以把检测、下载、更新替换并调整为不同的界面效果,但是fsys.update.dlMgr的代码可以始终复用。

使用线程句柄

一般我们可以使用 thread.invoke() 函数简单快捷的创建线程,
thread.create() 的作用和用法与 thread.invoke() 一样,唯一的区别是 thread.create()会返回线程句柄。

线程句柄可以用来控制线程(暂停或继续运行等等),
如果不再使用线程句柄,应当使用 raw.closehandle() 函数关闭线程句柄(这个操作不会关停线程)

有了线程句柄,我们可以使用 thread.waitOne() 等待线程执行完毕,
而且 thread.waitOne() 还可以一边等待一边处理界面消息(让界面不会卡死)。

下面看一下aardio范例里的多线程入门示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import console;

var thrdHandle = thread.create(
function(){
import console;
thread.lock("写控制台",function(){
console.log("线程在执行",thread.getId() )
})

//这里如果高级点可以用 thread.event来实现信号判断,参考本目录下的定时器范例
while(!thread.get("红灯停")){
console.log("线程在执行",time() )
sleep(1000)
}
console.log("停了!")
}
)

thread.suspend(thrdHandle) //使用线程句柄就可以控制线程,例如下面的函数暂停线程
thread.resume(thrdHandle) //线程恢复运行

var act = thread.stillActive(thrdHandle) //线程是否在运行

sleep(3000)

//每个线程的全局变量环境都是独立的,而 thread.set() 则用于设置所有线程共享的全局变量。
thread.set("红灯停", true)
thread.waitAll(thrdHandle)

//句柄不用的时候一定要用下面的函数关闭(切记:句柄是绝对不会自动释放的),关闭句柄并不会关闭线程
raw.closehandle(thrdHandle)
thread.invoke(
function(){
import console;
console.log("线程在执行",thread.getId() )
}
)
sleep(100)
console.pause(true);

您可以使用 thread.command 在线程间交互通信,请参考《多线程开发入门》
您还可以使用 thread.event 来实现线程间的同步,请参考《多线程中的交通信号灯:thread.event》
或者使用 thread.worksthread.manage 这些线程管理器来批量的管理线程句柄,
请参考此目录中的其他范例。

管理多线程

aardio中提供了 thread.managethread.works 等用于管理多个线程的对象,
例如标准库中用于实现多线程多任务下载文件的 thread.dlManager 就使用了thread.works管理线程。

thread.works 用于创建多线程任务分派,多个线程执行相同的任务,但可以不停的分派新的任务,

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import console;
import thread.works;

var works = thread.works( 20,
function(...) {
import console;

thread.lock("写控制台")
console.log("线程ID" + thread.getId(),",开始工作,接收到任务指令参数",...)
thread.unlock("写控制台")

return "返回值,线程ID" + thread.getId();
}
);

//分派任务
works.push("一个任务")
works.push("两个任务")

//等待任务完成
works.wait(
function(r){
console.log( "检查成果", r )
}
)

works.push("三个任务")
works.push("四个任务")
works.push("五个任务")

//退出程序前,等待任务完成并关闭所有线程
works.waitClose(
function(r){
console.log( "检查成果", r )
}
)

execute("pause")

thread.manage 可以用来创建多个线程执行多个不同的任务,可以添加任意个线程启动函数,
在线程执行完闭以后可以触发onEnd事件,并且把线程函数的返回值取回来,

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import console;
import thread.manage

//创建线程管理器
manage = thread.manage(3)

var thrdFunc = function(name){
import win;
import console;

for(i=1;10;1){
console.log( thread.getId(),name )
if( !win.delay(1000) ){ //主线程可以用 manage.quitMessage()中断这个循环
console.log("收到退出指令")
return;
}
}
return 67;
}

manage.create(thrdFunc,"线程1").onEnd = function(...){
console.log("线程1的回调",...)
}

manage.createLite(thrdFunc,"线程2").onEnd = function(){
console.log("线程2的回调")
}

manage.create(thrdFunc,"线程3")

manage.waitClose()
console.pause();

thread.manage通常是用于界面线程里管理工作线程,上面为了简化代码仅仅用到了控制台。

获取线程返回值

我们有时候在界面中创建一个线程,仅仅是为了让界面不卡顿,我们希望用 thead.waitOne() 阻塞等待线程执行完闭(界面线程同时可以响应消息),然后我们又希望在后面关闭线程句柄,并获取到线程最后返回的值。

可能我们希望一切尽可能的简单,尽可能的少写代码,并且也不想用到thread.manage(因为并不需要管理多个线程)。

这时候我们可以使用 win.invoke
win.invoke 的参数和用法与 thread.invoke完全一样,
区别是 win.invoke 会阻塞并等待线程执行完毕,并关闭线程句柄,同时获取到线程函数的返回值。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import win.ui;
var winform = win.form(text="aardio form";right=759;bottom=469)
winform.add(
button={cls="button";text="读取网页";z=1; ...};
edit={cls="edit";text="edit";edge=1;multiline=1;z=2; ...}
)

winform.button.oncommand = function(id,event){
winform.edit.text = win.invoke(
function(){
import inet.http;
sleep(3000);//暂停模拟一个耗时的操作
return inet.http().get("http://www.aardio.com");
}
)
}

winform.show()
win.loopMessage();

代码运行测试一下,在线程执行完以前,你仍然可以流畅的拖动窗口,操作界面。

写在最后

重视范例,才能开箱即用!

教程中用到的多线程直接调用窗口对象的功能 - 需要更新到新版 aardio才能支持。
更多关于多线程的功能请大家看aardio范例和文档。

一些用户可能不明白 aardio怎样才能真正的“开箱即用”,我接触到的一些用户拿起aardio就可以直接使用,写出非常好的程序而且速度很快,他们高兴的表示aardio简洁轻巧不用特别的学习直接就可以使用,而另外一些却始终在犹豫,在到处找教程、找文档,始终找不到方法,每前进一步都要求你准备一大堆的说明书才敢向前迈一步,实际上我发现他们换其他编程工具也是类似的结果( 可能有极少数学步车式的开发工具他们会适应 )。

我也跟那些上手比较快的用户聊过一些,发现他们都有一个共同的习惯就是非常重视范例,
因为 aardio的范例非常、非常的多,而且aardio范例跟其他语言都有一些不同,很多代码就是几句代码就是一个简单而完整的程序,我经常听到一些人跟我说,仅仅是复制一些范例整合到一起,做一些修改就可以做出软件。

所以请记住:教程写的再多,看的再多,始终是纸上谈兵。

搞培训的人很愉快因为能赚到钱,而参加培训的人也很愉快因为找到了心理安慰剂,但真正能让你学会编程的是多看范例,多跑代码,多动手写代码!