转自 http://zeze0556.tk/2009/12/25/自己实现多线程
在开发BREW程序的时候,SDK中包含了线程(我记得是3.1以上)模型,但不推荐使用。而移植的很多内容,都使用了线程。由于BREW也不允许使用静 态变量,死循环或者嵌套很深的占用大量时间的运算,如果要随时暂停的话,只能把变量临时存储起来,然后一层一层的往外退。首先,修改的话比较困难,出错的 几率很大,其次,每次恢复和暂停都需要占用时间,进行额外的操作,影响效率。这个时候,线程就起作用了,不是不推荐使用么,我们就自己实现一个。
先贴代码,ARM部分的,一个日本人写的,我很早之前就明白其中的做法,由于不能拿手机随便整(整坏了赔不起)。幸好别人已经做了,安全性瞬间提高100%。同时,我稍加修改
02 | AREA ||i.StartThread||, CODE, READONLY |
05 | STMFD sp!, {r0-r12, lr} |
19 | LDMFD sp!, { r0-r12, lr } |
24 | AREA ||i.SwitchThread||, CODE, READONLY |
27 | STMFD sp!, {r0-r12, lr} |
30 | LDMFD sp!, {r0-r12, lr } |
说明:
实现两个接口函数,StartThread和SwitchThread,编译时使用armasm编译,调用按照C函数的方式调用(原因可能在以后的文章中会提到的)
先看函数接口的声明:
1 | extern "C" void StartThread( void *, void * ); |
2 | extern "C" void SwitchThread( void * ); |
对于StartThread函数,传递两个指针,第一个传递的是线程结构的内存指针,第二个是调用函数的内存地址,为什么不声明的更明了一些。最后就会明白的。
StartThread的那十几行做了什么事?
其实很简单,它从第一个指针(线程结构体中)得到堆栈的栈顶,然后将栈指针指向栈顶,接着,调用回调函数,回调函数调用结束之后,返回到调用 StartThread函数的上一级函数,接着执行。看上去仅仅是普通的调用,仅仅进行了栈指针的交换而已。是的,就是这么回事。
对于SwitchThread函数,就更简单了,保存几乎所有关键的寄存器,然后交换栈指针,然后读取被保存的寄存器(不是刚刚被保存的,因为栈已经被切换了),然后返回。
线程?就这样?是的,这就是最简单的线程,最核心的代码。总共32行,包含注释和无用的内容。一个模拟的环境进行一个模拟的调用:
1、外部函数调用StartThread函数
2、StartThread首先保存栈(stack1),设置栈指针到自定义堆栈(stack2),然后开始调用回调。
3、如果回调函数中没有需要暂停的内容,直接返回,执行StartThread中BX之后的语句,StartThread正常返回。否则,继续。
4、回调函数暂停时,触发SwitchThread函数,SwitchThread函数先保存寄存器内容到栈中(stack2),然后切换栈。
5、恢复保存的栈,对于第一次执行,使用的栈是StartThread中保存的栈(stack1)。
6、使用新的栈继续执行,对于第一次执行,由于已经恢复了栈,那么,继续的调用,将会返回到调用StartThread函数的上一级函数。
7、如果要继续之前被暂停的线程,重新调用SwitchThread函数,然后会切换栈,跳转到5。
8、如果函数正常返回,则返回到调用回调函数的上一级函数。
很混乱?是的,我在写的时候也是很混乱,我的建议是,看完下面的代码,然后用一个简单的图例画出来,就会发现很简单的。我不画是因为在emacs下,我的 这一技能还未学会。下面是线程模型的Thread模板,我不但要使用自己的栈,还要能定义它的栈大小,简单的来说,我不想让每个线程的栈都是一样的大小。
class Runnable
{
public:
virtual ~Runnable() {}
virtual void run() = 0;
virtual void resume() = 0;
virtual void suspend() = 0;
};
extern "C" void StartThread( void*, void* /*(*callback)( void* )*/ );
extern "C" void SwitchThread( void* );
template< int LENGTH >
class Thread : public Runnable
{
public:
Thread() :_pStack( NULL ), _bSuspend( false ), _bExecute( 0 )
{
DBGPRINTF("LENGTH=%d", LENGTH);
}
virtual ~Thread(){};
virtual void start(){
if( isExecute() ) return;
DBGPRINTF("LENGTH=%d", LENGTH);
_pStack = &_stack[LENGTH];
StartThread( this, (void*)(&StartCallback) );
}
virtual void suspend()
{
if( isSuspend() || !isExecute() ) return;
_bSuspend = true;
SwitchThread( this );
}
virtual void resume()
{
if( !isSuspend() ) return;
_bSuspend = false;
SwitchThread( this );
}
virtual bool isExecute()
{
return ( _bExecute != 0 );
}
virtual bool isSuspend()
{
return _bSuspend;
}
private:
byte* _pStack;
int _bExecute;
byte _stack[LENGTH];
bool _bSuspend;
static void StartCallback( Thread< length >* pThread )
{
pThread->run();
}
};
上面是线程的全部结构,简单的来说,为什么要返回上上级函数(我嘴巴没有打结)。主要是基于栈平衡和实现简单性的考虑,虽然有些时候,我较容易的写汇编代 码,但我还是很想尽可能少的写,这样烦人的事情就会少很多,同时少了很多为什么和进行修改也会很少了,而不用每次都要我亲自动手。
使用模板来确定Stack的大小,为什么不动态分配?因为它不能和其他参数保存在同一个相对地址,对于确定更加麻烦,更加容易出错,我试过,我不能用简单 的32行代码来做相同的事情。模板有什么好处,可以确定栈的大小的同时,相对于Thread的指针地址来说,它的地址是固定的,而且,不用考虑内存释放, 只用把Thread释放之后,Stack数组也自然释放,即使是基于寻址来说,也比动态分配要高效。简单,高效,不易出错。何乐而不为?
上面是模板的好处,下面就是不要的地方了,也不是很重要,声明的StartThread和SwitchThread函数的参数,变成了两个void指针,难看。
将Runnable和Thread分开,只是我想或许我会试试其他的做法也不一定。
调用的例子:
02 | class test_Thread_Class: public Thread< 10*1024 > |
05 | static const int THREAD_INIT = 0; |
06 | static const int THREAD_RUN = 1; |
07 | static const int THREAD_PAUSE = 2; |
08 | static const int THREAD_QUIT = 3; |
13 | void setState( int state); |
14 | void draw(Graphics* g, int x, int y, int width, int height, int color); |
18 | void test_Thread_Class::run() |
23 | for ( int i = 0; i < 100; i++) { |
25 | DBGPRINTF( "state=%d" , state); |
26 | if (state == THREAD_QUIT) |
33 | int test_Thread_Class::getValue() |
37 | int test_Thread_Class::getState() |
41 | void test_Thread_Class::setState( int state) |
45 | void test_Thread_Class::quit() |
50 | void test_Thread_Class::draw(Graphics* g, int x, int y, int width, int height, int color) |
52 | g->setColor(Graphics::getColorOfRGB((color&0xFF0000)>>16, (color&0xFF00)>>8, color&0xFF)); |
53 | g->fillRect(x, y, width, height); |
54 | g->setColor(Graphics::getColorOfRGB(0xFF, 0x0, 0x0)); |
55 | g->drawString((String( "" )+value).c_str(), x + 4, y+4, Graphics::LEFT|Graphics::TOP); |
test_Thread_Class类使用地方法:
初始化:
1 | Runable* thread = new test_Thread_Class(); |
如果你想将Start和以后的执行分开的话,可以先调用thread->start(),然后在其他的地方只用调用 thread->resume()就可以了。不用调用suspend()是因为在run()函数的内部调用,而不是外部调用,suspend()和 resume()调用要一一对应(除非删除线程)。如果你不关心这些,和我一样,每帧都让它执行,可以进行简单的这样调用:
1 | if (!( thread ->isExecute())) |
如果线程执行完毕,让它重新执行。
上面的线程核心都是手机上可以执行的代码,理论上来说,你可以移植到任何ARM平台,因为和系统完全没有关系。我们大部分的开发,刚开始的时候都是在 x86的模拟器上的。我知道你想要的是什么,下面的就是StartThread和SwitchThread的x86的代码,这下完美了。
03 | StartThread PROTO C :D WORD , :D WORD |
05 | StartThread PROC C pthis: DWORD , pfun: DWORD |
07 | mov DWORD PTR [eax+8], 1 |
13 | mov DWORD PTR[eax+8],0 |
14 | xchg DWORD PTR [eax+4], esp |
21 | SwitchThread PROTO C :D WORD |
23 | SwitchThread PROC C pthis: DWORD |
26 | xchg DWORD PTR [eax + 4], esp |
比ARM稍多一些,db 0c3h是ret的意思,我没有记错的话,这样写是因为栈要自己处理,VC的编译器在编译的时候,会将ret语句之前添加栈恢复的代码,完全不合我的意思。
线程很好,很强大,原来我们的噩梦都不复存在了。世界是美好的,有线程实在太好了。但是,这是一个相对的世界。十全十美的事情总是很难发生。下面开始了黑暗之旅。
上面的代码看上去完美的,执行上应该是完美的,但是,就真机的测试来说,大部分机型上是完美的,个别机型上会出问题的,我曾经试图解决这个问题,就像我之 前提到的,我没办法乱整真机,我也没办法进行真机调试,如果谁知道在任何BREW手机上如何真机调试的话,务必告诉我。出问题的表现就是手机重启,简单的 来说,主要集中的放置一段时间之后,屏保出现的时候,其他时候都是正常的。就我的经验来说,机型是固定的,这是庆幸的。新的BREW 4.0的手机则暂时没有发现问题。
我前几天没有更新博客,因为发生了些无法预料的灾祸,刚开始是我的老笔记本出现问题,开不了了。开始修理,最后不了了之。接下来是我的老台式机,放置了半 年之后,准备当服务器。结果,也开不了了。真是祸不单行,福无双降啊。想想我笔记本硬盘和移动硬盘同时挂掉,道理相同啊。