自己实现多线程

在开发BREW程序的时候,SDK中包含了线程(我记得是3.1以上)模型,但不推荐使用。而移植的很多内容,都使用了线程。由于BREW也不允许使用静态变量,死循环或者嵌套很深的占用大量时间的运算,如果要随时暂停的话,只能把变量临时存储起来,然后一层一层的往外退。首先,修改的话比较困难,出错的几率很大,其次,每次恢复和暂停都需要占用时间,进行额外的操作,影响效率。这个时候,线程就起作用了,不是不推荐使用么,我们就自己实现一个。

先贴代码,ARM部分的,一个日本人写的,我很早之前就明白其中的做法,由于不能拿手机随便整(整坏了赔不起)。幸好别人已经做了,安全性瞬间提高100%。同时,我稍加修改

       	CODE32
	AREA ||i.StartThread||, CODE, READONLY
	EXPORT StartThread
StartThread PROC
	STMFD sp!, {r0-r12, lr}
	ADD r4, r0, #4
	SWP sp, sp, [r4]
	MOV r5, #1
	STR r5, [r0, #8]
	STR r0, [sp, #-4]!
	MOV lr, pc
	BX r1

	LDR r0, [sp], #4
	MOV r5, #0
	STR r5, [r0, #8]
	ADD r4, r0, #4
	SWP sp, sp, [r4]
	LDMFD sp!, { r0-r12, lr }
	MOV pc, lr
	ENDP
	
	CODE32
	AREA ||i.SwitchThread||, CODE, READONLY
	EXPORT SwitchThread
SwitchThread PROC
	STMFD sp!, {r0-r12, lr}
	ADD r4, r0, #4
	SWP sp, sp, [r4]
	LDMFD sp!, {r0-r12, lr }
	MOV pc, lr
	ENDP
       

说明:
实现两个接口函数,StartThread和SwitchThread,编译时使用armasm编译,调用按照C函数的方式调用(原因可能在以后的文章中会提到的)
先看函数接口的声明:

       extern "C" void StartThread( void*, void* /*(*callback)( void* )*/ );
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分开,只是我想或许我会试试其他的做法也不一定。
调用的例子:

  class Graphics;
class test_Thread_Class: public Thread< 10*1024 >
{
public:
        static const int THREAD_INIT = 0;
        static const int THREAD_RUN = 1;
        static const int THREAD_PAUSE = 2;
        static const int THREAD_QUIT = 3;
        virtual void run();
         void quit();
         int getState();
        int getValue();
        void setState(int state);
        void draw(Graphics* g, int x, int y, int width, int height, int color);
        int state;
        int value;
};
void test_Thread_Class::run()
{
        state = THREAD_RUN;
        while(true) {

                for (int i = 0; i < 100; i++) {
                        value = i;
                        DBGPRINTF("state=%d", state);
                        if (state == THREAD_QUIT)
                                return;
                        suspend();
                }
        }
}

int test_Thread_Class::getValue()
{
        return value;
}
int test_Thread_Class::getState()
{
        return state;
}
void test_Thread_Class::setState(int state)
{
        this->state = state;
}
void test_Thread_Class::quit()
{
        state = THREAD_QUIT;
}

void test_Thread_Class::draw(Graphics* g, int x, int y, int width, int height, int color)
{
        g->setColor(Graphics::getColorOfRGB((color&0xFF0000)>>16, (color&0xFF00)>>8, color&0xFF));
        g->fillRect(x, y, width, height);
        g->setColor(Graphics::getColorOfRGB(0xFF, 0x0, 0x0));
        g->drawString((String("")+value).c_str(), x + 4, y+4, Graphics::LEFT|Graphics::TOP);
}
  

test_Thread_Class类使用地方法:
初始化:

  Runable* thread = new test_Thread_Class();
  

如果你想将Start和以后的执行分开的话,可以先调用thread->start(),然后在其他的地方只用调用thread->resume()就可以了。不用调用suspend()是因为在run()函数的内部调用,而不是外部调用,suspend()和resume()调用要一一对应(除非删除线程)。如果你不关心这些,和我一样,每帧都让它执行,可以进行简单的这样调用:

  if (!(thread->isExecute()))
  thread->start();
  else
  thread->resume();
  

如果线程执行完毕,让它重新执行。
上面的线程核心都是手机上可以执行的代码,理论上来说,你可以移植到任何ARM平台,因为和系统完全没有关系。我们大部分的开发,刚开始的时候都是在x86的模拟器上的。我知道你想要的是什么,下面的就是StartThread和SwitchThread的x86的代码,这下完美了。

  .386P
.MODEL FLAT, STDCALL        
        StartThread PROTO C :DWORD, :DWORD
_TEXT	SEGMENT
StartThread PROC C pthis:DWORD, pfun:DWORD
        mov eax, pthis
        mov DWORD PTR [eax+8], 1
        pushad
        xchg esp, [eax + 4]
        push pthis
        call pfun
        pop eax
        mov DWORD PTR[eax+8],0
        xchg DWORD PTR [eax+4], esp
        popad
        mov esp, ebp
        pop ebp 
        db 0c3h
StartThread ENDP
_TEXT	ENDS 
        SwitchThread PROTO C :DWORD
_TEXT	SEGMENT
SwitchThread PROC C pthis:DWORD
        mov eax, pthis
        pushad
        xchg DWORD PTR [eax + 4], esp
        popad
        mov esp, ebp
        pop ebp
        db 0c3h
SwitchThread ENDP
_TEXT	ENDS
END
  

比ARM稍多一些,db 0c3h是ret的意思,我没有记错的话,这样写是因为栈要自己处理,VC的编译器在编译的时候,会将ret语句之前添加栈恢复的代码,完全不合我的意思。
线程很好,很强大,原来我们的噩梦都不复存在了。世界是美好的,有线程实在太好了。但是,这是一个相对的世界。十全十美的事情总是很难发生。下面开始了黑暗之旅。
上面的代码看上去完美的,执行上应该是完美的,但是,就真机的测试来说,大部分机型上是完美的,个别机型上会出问题的,我曾经试图解决这个问题,就像我之前提到的,我没办法乱整真机,我也没办法进行真机调试,如果谁知道在任何BREW手机上如何真机调试的话,务必告诉我。出问题的表现就是手机重启,简单的来说,主要集中的放置一段时间之后,屏保出现的时候,其他时候都是正常的。就我的经验来说,机型是固定的,这是庆幸的。新的BREW 4.0的手机则暂时没有发现问题。
我前几天没有更新博客,因为发生了些无法预料的灾祸,刚开始是我的老笔记本出现问题,开不了了。开始修理,最后不了了之。接下来是我的老台式机,放置了半年之后,准备当服务器。结果,也开不了了。真是祸不单行,福无双降啊。想想我笔记本硬盘和移动硬盘同时挂掉,道理相同啊。

发布日期:
分类:程序 标签:

5条评论

  1. “SDK中包含了线程(我记得是3.1以上)模型,但不推荐使用”
    能说说为什么不推荐使用么?
    我用BREW SDK的IThread接口写了一个很简单的例子,在模拟器和大部分手机上跑都没有问题,不知道为什么一到三星的新手机(w319)上就重启

    1. 系统自带的我没有用过。如果使用该接口的话,可能需要向运营商做特殊申请。很早之前是这样子的,现在不清楚是否还有这样的规定了。

      1. 我准备试试您介绍的这个汇编版线程了。
        感觉您水平很高,不知道有没有兴趣一起共事。我们这是北京的AONE公司,在电信BREW游戏这边可以算是排名第一,今后也想往手机网游方面发展。

        1. 由于某些原因,目前暂时不行。
          我主要是研究基础编程。主要都是手机方面是因为比较好测试。而自己在这个平台上也专心的做了研究。就像很多时候使用j2me,并不是因为自己非常喜欢j2me,只是因为简便。

回复 wtyqm 取消回复

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据