Symbian中的进程和线程

《Symbian OS:线程编程》
Symbian操作系统中的线程和进程

在Symbian操作系统中,每个进程都有一个或多个线程。线程是执行的基本单位。一个进程的主线程是在进程启动时生成的。
Symbian属于抢占式多任务操作系统,这意味着每个线程都有自己的执行时间,直到系统将CPU使用权给予其他线程。当系统调度时,具有最高优先权的线程将首先获得执行。
进程边界是受内存保护的。所有的用户进程都有自己的内存地址空间,同一进程中的所有线程共享这一空间,用户进程不能直接访问其他进程的地址空间。
每个线程都有它自己的stack和heap,这里heap可以是私有的,也可以被其他线程共享。应用程序框架生成并安装了一个active scheduler,并且为主线程准备了清除栈。如果没有使用框架(如编写exe程序)那就要手动生成这些了:)
Symbian操作系统专为单线程应用优化,因此强烈推荐使用“活动对象”代替多线程。

一、使用单线程的优点
在每个线程都有自己的stack空间时,使用单线程可以减少内存耗费。
在线程间切换上下文要比切换活动对象(通过active scheduler)慢得多。
不需要处理互斥现象,这减少了程序的负担,简化了代码,减少了错误发生的几率。
一些资源只能为主线程所用,因此它们并不是线程安全的,如动态数组。

二、使用多线程的优点
有时为了保证所执行的任务的持续性,如播放声音时,我们可以将其归在一个单独的线程中处理。
将复杂的多线程或长时间运行程序移植到Symbian上,如果不使用多线程处理,可能会比较难也更耗时间。
(题外话:我曾綺将一个棋类程序移植到symbian上,里面复杂的递归运算使我不得不使用多线程,这样的情况下,你是很难将时间有序的分化开来,使用活动对象的)


三、线程的基本使用方法
RThread提供了线程的各项功能。线程是为内核所拥有的对象,RThread对象封装了这些对象的句柄。

1、生成一个新线程
新的线程可以通过构造一个RThread对象,并调用它的Create()函数生成。如:

Code:
1: TInt threadFunction(TAny *aPtr)
2: {
3: // points to iParameter
4: TInt *i = (TInt *)aPtr;
5: ?_
6: }
7:
8: RThreadthread;
9: thread.Create(KThreadName, threadFunction, 4096,
10: KMinHeapSize, 256*KMinHeapSize, &iParameter);
11: thread.Resume();2、线程状态
一个线程的最重要的状态为运行、准备、等待和暂停。在生成后,线程首先处于暂停状态,你可以调用Resume()函数来启动它的运行。一个线程也可以通过调用Suspend()来进入中断状态。

线程一般通过Kill(TInt aReason)来结束,Terminate()与其相似。如果一个进程的主线程结束,则该进程与所属所有线程都将结束。
一种非正常关闭线程的方式就是调用Panic(const TDesC& aCategory, TInt aReason)来中断执行。
如何获得中断线程的信息呢,我们可通过ExitType(),ExitReason()以及ExitCategory()方法来获得。

线程可以在中断时发出请求,我们通过调用异步方法Logon()来完成此任务。返回值在aStatus中。LogonCancel()可以取消前次请求。
void Logon(TRequestStatus& aStatus) const;
TInt LogonCancel(TRequestStatus& aStatus) const;

我们可以通过SetProtected(ETrue)方法将线程保护起来,也可以通过SetProtected(EFalse)来取消保护。在保护时,另一个线程是无法中断、结束、异常中断或设置该线程的优先级的。Protected()方法可以返回该线程的保护状态。

3、访问线程及进程
我们可以通过构造一个RThread对象来访问当前线程。Id()方法可以返回改线程的ID。
拥有此线程的进程可以通过访问RThread的Process(RProcess&aProcess)方法来获得。这里传入的参数应该是一个RProcess对象。
其他线程可以通过Open()方法来访问。我们通过传递TThreadId、线程名称或TFindThread对象来打开线程。
TInt Open(const TDesC& aFullName, TOwnerType aType=EOwnerProcess);
TInt Open(TThreadId aID, TOwnerType aType=EOwnerProcess);
TInt Open(const TFindThread& aFind, TOwnerType aType=EOwnerProcess);


Code:
1: // * as a wildcard for the name search
2: _LIT(KFindAll, “*”);
3: // default RThread object, has a handle of the current thread
4: RThread thread;
5: TFullName fullName;
6: TFindThread finder(KFindAll);
7:
8: while (finder.Next(fullName) == KErrNone)
9: {
10: // on success, variable ‘thread’ will contain a handle of
11: // a thread found by finder
12: thread.Open(finder);
13:
14: // get thread’s memory information
15: TInt heapSize, stackSize;
16: thread.GetRamSizes(heapSize, stackSize);
17:
18: // show fullName, heapSize and stackSize
19: ...
20: }4、线程优先级
线程可以被赋予一个绝对或相对的优先级。绝对优先级定义了这个线程的总体优先级,不需要考虑其拥有者进程的优先级了。而赋予相对优先级时则将此线称定义为拥有者进程的优先级加上该相对优先级后的结果。

下面粗体标示的优先级值可以由用户代码设置:

Code:
enum TProcessPriority
{
   EPriorityLow=150,
   EPriorityBackground=250,
   EPriorityForeground=350,
   EPriorityHigh=450,
   EPriorityWindowServer=650,
   EPriorityFileServer=750,
   EPriorityRealTimeServer=850,
   EPrioritySupervisor=950
};

enum TThreadPriority
{
EPriorityNull=(-30),
EPriorityMuchLess=(-20),
EPriorityLess=(-10),
EPriorityNormal=0,
EPriorityMore=10,
EPriorityMuchMore=20,
EPriorityRealTime=30,
EPriorityAbsoluteVeryLow=100,
EPriorityAbsoluteLow=200,
EPriorityAbsoluteBackground=300,
EPriorityAbsoluteForeground=400,
EPriorityAbsoluteHigh=500
};上面枚举出来的值中绝对优先级值为:
EPriorityAbsoluteVeryLow, EPriorityAbsoluteLow, EPriorityAbsoluteBackground,EPriorityAbsoluteForeground, EPriorityAbsoluteHigh.
相对优先级值为:
EPriorityMuchLess, EPriorityLess, EPriorityNormal, EPriorityMore,EPriorityMuchMore.
EPriorityNull是一个特殊值,它定义了最低的级别,Kernel idel thread使用的就是它*_*

EPriorityRealTime定义了除核心服务线程优先级外最高的总体优先级。
RThread中的Priority()方法返回了一个线程的优先级(按以上描述值)。我们也可以通过SetPriority(TThreadPrioriy aPriority)方法来修改优先级。
ProcessPriority()方法返回了拥有该线程之进程的优先级(按TProcessPriority描述值)。我们也可以通过SetProcessPriority(TProcessPriority)方法来修改该进程的优先级。

5、异常处理
每个线程都有自己的异常处理模块。当线程发生异常时会调用异常处理模块。异常处理模块的訽型为:
typedef void TExceptionHandler(TExcType);

RThread包含了下列异常处理相关的方法:
TExceptionHandler* ExceptionHandler()
返回该线程当前异常处理模块的地址。

TInt SetExceptionHandler(TExceptionHandler* aHandler, TUint32 aMask);
定义了该线程新的异常处理模块的地址,以及它所处理异常的类别。

void ModifyExceptionMask(TUint32 aClearMask, TUint32 aSetMask)
修改异常处理模块所定之异常类别,aClearMask参数定义了不再为异常处理模块所处理的类别,而aSetMask则定义了新的处理类别。

TInt RaiseException(TExcType aType);
引发线程上指定类型的异常,这时异常处理模块将被启动执行(发生在调用之后)。

TBool IsExceptionHandled(TExcType aType);
检查线程的异常处理模块是否捕捉到aType类型的异常。


(1)异常类别及类型
异常类型是一组针对单个异常的类型识别,主要用在异常发生时。
异常类别则代表一组异常形式。

异常类别的一个集是由一个或多个异常类别通过OR形式组合成的,如KExceptionInteger|KExceptionDebug,这些值用来设置及修改异常处理模块所处理的类别。

下面列示了所有的类型及类别。
异常类别异常类型
KExceptionInterrupt ->EExcGeneral, EExcUserInterrupt
KExceptionInteger ->EExcIntegerDivideByZero, EExcIntegerOverflow
KExceptionDebug->EExcSingleStep, EExcBreakPoint
KExceptionFault ->EExcBoundsCheck, EExcInvalidOpCode, EExcDoubleFault,EExcStackFault, EExcAccessViolation, EExcPrivInstruction, EExcAlignment,EExcPageFault
KExceptionFpe ->EExcFloatDenormal, EExcFloatDivideByZero,EExcFloatIndexactResult, EExcFloatInvalidOperation, EExcFloatOverflow,EExcFloatStackCheck, EExcFloatUnderflow
KExceptionAbort ->EExcAbort
KExceptionKill->EExcKill
6、其他线程函数
TInt Rename(const TDesC& aName)
为线程定义个新名字。

void RequestComplete(TRequestStatus*& aStatus, TInt aReason)
通知线程与一个异步请求绑定的请求状态对象aStatus已綺完成。sStatus完成代码将负责设置aReason及发出线程请求信号的通知。

TInt RequestCount()
返回线程请求信号的数目。如果是负值则表示该线程正在等待至少一个异常请求的完成。

void HandleCount(TInt& aProcessHandleCount, TInt& aThreadHandleCount)
得到线程中及拥有该线程的进程中处理模块的数目。

RHeap* Heap()
返回一个指向改线程堆的指针。

TInt GetRamSizes(TInt& aHeapSize, TInt& aStackSize)
得到该线程中堆和栈的大小。

TInt GetCpuTime(TTimeIntervalMicroSeconds& aCpuTime)
得到改线程所分配到的CPU时间

void Context(TDes8& aDes)
得到该线程( sleeping状态)所注册的上下文环境。

四、线程内部的通信
1)共享内存
在线程间交换信息最直接的方法就是使用共享内存。线程入口函数中有一个参数TAny* aPtr,这个指针可以用于任何目的。通常可以用它来传递一个负责线程间共享信息的数据结构或类实例。因为同一进程中的线程是共享内存地址空间的,因此这里指针所指向的数据可以被两个线程所共享,注意访问该数据时必须是同步形式。
另外这里的指针参数可以使用SetInitialParameter(TAny* aPtr)方法来改变,但这时线程应处于suspend状态。

2)Client/Server API
Symbian操作系统提供了一组基于server/session的API,允许一个线程扮演server的角色,向其他线程或进程提供服务。这里API也提供处理一组方法处理信息的传递,异步以数据传输。

3)进程内数据传输
如果两个线程分属不同的进程,则他们无法直接管理需要通信的数据,因为他们没有共享的数据区。这里可以使用RThread提供的ReadL()方 法及WriteL()方法,我们可以用来在当前线程和由RThread提供的另一个线程间的地址空间拷贝8/16位的数据。这里当前线程和另一个线程可以 归属同一个进程也可分属不同进程。

数据的传输是通过拷贝数据来完成的,RThread提供了方法返回在它地址空间内一个descriptor的长度及最大允许长度。

a>读取另个线程所提供的descriptor
void ReadL(const TAny* aPtr,TDes8& aDes,TIntanOffset) const;
void ReadL(const TAny* aPtr,TDes16 &aDes,TInt anOffset) const;

这里ReadL()方法从另一个线程的descriptor(由aPtr所指)中拷贝一组数据,传递到当前线程的descriptor(由aDes所指)。
aPtr指针必须指向一个在RThread句柄所指线程的地址空间中有效的descriptor。

从源descriptor中的内容是从anOffset位置那里开始拷贝到目的descriptor(aDes)的。

b>向另个线程写入descriptor
void WriteL(const TAny* aPtr, const TDesC8& aDes, TInt anOffset) const;
void WriteL(const TAny* aPtr, const TDesC16& aDes, TInt anOffset) const;

用这个方法将当前线程descritor(aDes)所提供的数据都拷贝在另一个线程(aPtr所指)的descriptor中。这里anOffset参数设定了目标descriptor的初始化拷贝位置。

aPtr为线程地址空间内有效的可修改descriptor。

如果拷贝进去的数据长度超过目标descriptor的最大长度,则函数会发生异常。

c>Descriptor帮助函数
TInt GetDesLength(const TAny* aPtr) const;
TInt GetDesMaxLength(const TAny* aPtr) const;
这里RThread的GetDesLength()方法可以返回aPtr所指向的descriptor长度。这里descriptor必须为RThread句柄所指定的线程的地址空间中。
RThread的GetMaxDesLength()方法返回aPtr所指向descriptor的最大长度。descriptor也应在RThread句柄所指的线程地址空间中。

建议在ReadL()和WriteL()等方法前使用这些函数。

 

最近看了一些进程间通讯的资料,基本上原理是一样的,在一个线程开一个全局的服务,另外一个可以通过全名字,或者部分名字查找这个服务,然后取得这个全局服务的内存地址,得到内存中的数据。

 在symbian上有RChunk,RTread等类,但是在3rd fp1上取消了RTread的Read和Write方法,所以只能用RChunk类了,调用方法如下:

   一个线程启动服务:

   _LIT(KChunkName,"xn Globla Chunk");
   TInt rc=iChk.CreateGlobal(KChunkName,0x1000,0x5000);  

   //取得共享内存地址

   TUint8* uid=iChk.Base();
   HBufC* buf=HBufC8::NewL(255);
   buf->Des().Copy(_L8("123456"));
  
   //uid=&(iHbuf->Des()[0]);
   //将数据放入到共享内存中

   TPtrC8 ptr;
   Mem::Copy(uid,(TAny *)buf->Des().Ptr(),buf->Length());
   //服务线程ok

   另外线程:

   _LIT(KChunkName,"xn Globla Chunk");
   RChunk chk;
   chk.OpenGlobal(KChunkName,0);//第一个参数指定了全局内存块的名称,第二个参数用于说明块是为只读(1)还是可写的(0)
   TUint8* uid=chk.Base();
   TPtrC8 ptr;
    //Mem::Copy(uid,(TAny*)iHbuf->Des().Ptr(),iHbuf->Length());
    ptr.Set( (const TUint8*)uid , chk.Szie());

    //ok 共享内存中的数据取到了,当然也可以修改。

 

RMsgQueue对应DMsgQueue,property是在kernel空间中,所以访问他们需要swi把cpu切换到supervisormode。
shared chunk是同一块内存映射到不同进程中,直接读写即可,这个理论上效率更高点。而且symbian中fbs server就是用shared chunk和client共享大数据(bitmap)。

 

s/p对应的kernel对象是TProperty,msg queue对应的kernel对象是DMsgQueue。
s/p和msg queue都有数据从一个process copy到kernel的过程,不然别的进程是没有办法访问的,另外访问(无论读写)都有切换到kernel过程,所以效率相对比较低。

shared chunk就是同一段物理内存在不同进程中都进行了映射(虚拟地址可能不同),process1写了以后,process2直接读就可以了,效率上应该有明显的提升。

 

需要指出的是kernel是有办法把数据从一个processcopy到另一个process的,一个时刻只有一个process在运行,它的pde, pte是有效的,所以访问这个进程的地址空间是没有问题的,而kernel同样也可以查到inactive process(未运行的进程)的pde和pte信息,所以能够找到另一个进程的物理地址,进行直接读写。

在eka2之前,是支持RThread::ReadL/WriteL的,这样一个进程就可以读写进程的用户数据,这样虽然方便共享,但是安全性没有保障,所以eka2的时候去掉了。


4)线程局部存储(TLS)
Symbian操作系统是不允许在DLL中出现可写静态变量的。然而每个DLL中每个线程都会分配一个32位字符空间。这个字符用来存放一个指向数据结构或类示例的指针。分配和释放这些资源可在例如DLL的入口函数E32Dll中处理。

另一个使用线程局部存储的示例为保存指向类示例的指针,这样静态回调函数可以访问与线程相联系的该对象。当我们处理自定义异常处理模块时是很有用的。

Dll::SetTls(TAny *aPtr)函数负责设置线程局部存储的指针。
Dll::Tls()函数负责返回一个指向线程局部存储的指针。取得后该指针所指定数据可以正常使用。

5) User-Interrupt Exception
如3.5“Exception Handling”所述,线程可以引发其他线程的异常。有一种异常类型是专为用户所保留的,那就是EExcUserInterrupt,可以通过指定异常 类型KExceptionUserInterrupt来处理。其他要传递的信息应该通过共享内存来处理。这是在最段时间内向其他线程传递信息的方式,当异常发生时调用RaiseException()函数可切换到另个线程的异常处理模块。

 
6) Publish & Subsribe
Publish & Subscrible是一个进程间的通信机制(在SymbianOS v8.0a和Series 60 Platform 2nd Editon, Feature Pack2中有所介绍),可以查看相关的文挡。

这个机制包括了三个基本方面:properties, publishers, 和subscribers.Properties是由一个标准SymbianOS UID所定义的全局唯一变量,它定义了属性类别,而另一个整数定义了property sub-key。
Publishers是负责更新属性的线程。Subscribers是负责监听属性变化的线程。

7) 消息队列
消息队列是另一个进程间通信的机制(在SymbianOS v8.0a和Series 60 Platform 2ndEditon, Feature Pack2中有所介绍)。
消息队列用来向队列发送消息,而无需获得接收者的状态标识信息。任何进程(都在同一队列中的)或任何同一进程中的线程(在局部队列中)都可以读取这些信息。

五、同步
1)目的
如果多个线程在没有保护机制的情况下使用同一资源,就会出现一些问题。如,线程A更新了部分descriptor,而线程B接手后又重写了内容。回到线程A后,又开始更新内容。这样descriptor的内容就在A与B中来回修改了。

为了防止这类情况的发生,你需要使用非抢占式client/server机制或同步对象来处理。同步对象(mutex,semaphore, critical section)都是核心对象,可以通过句柄来访问。他们会限制或直接锁住对多线程们所要访问的资源,这种资源形式被称为共享资源。
在任何时刻只能有一个线程对共享资源进行写操作,每个要访问资源的线程都应使用同步机制来管理资源。
同步操作一般有如下步骤:
1. Call Wait() of the synchronization object reserved for this resource.
2. Access the shared resource.
3. Call Signal() of the synchronization object reserved for this resource.

注意,当kill线程时要小心点。因为如果线程使用已綺注销的对象,不同的同步对象其处理方式是不同的。因此,忽略使用同步类型而kill一个已綺更新过部分资源的线程是会引发问题的。

2)使用Semaphores(信号)
Semaphores可以管理共享资源的同步化访问。这里semaphore的句柄可通过RSemaphore类获得。
Semaphore限制了同一时刻访问共享资源的数目。semaphore计数的初始化工作可以放在构造函数中进行。
Semaphore可以是全局的也可以是局部的,全局的semaphore有自己的名称,可以被其他进程搜索并使用。而局部的semaphore没有名称,只能在同一进程间的线程中使用。
调用semaphore的Wait()方法将减少semaphore计数,而如果计数为负的话,调用线程就会进入等待状态。
调用semaphore的Signal()方法将增加semaphore计数,如果增长之前为负数,则等待信号的第一个线程将设定为准备运行状态。

调用semaphore的Signal(TInt aCount)和调用n次Signal()效果是一样的。
当线程死亡时,只有该线程正等待该信号时,信号才能被通知。因为信号在下面这样的情况也是可以执行的:在一个线程中调用Wait(),在另一个线程中调用Signal(),这样的信号无法在使用它的线程死亡时被通知。这样只会导致信号计数减低。

3)使用互斥(Mutex)
互斥主要使用在同步下独占访问共享资源。它的句柄可以通过RMutex类来获得。
和信号一样,互斥可以是全局也可以是局部的。唯一的不同在于其计数初始化时总为1。Mutex因此只允许最多一个访问共享资源。
如果一个线程已綺为mutex调用Wait(),但没有Signal(),则线程死亡时该互斥将被通知。

4)使用临界区(Critical Sections)
Critical Sections可用来在一单独进程中独占访问共享资源。Critical Sections句柄可以通过RCriticalSection类来获得。
Critical Sections只能用在同一进程的线程间,通常它用来管理某段代码的访问,每次只能有一个线程来访问。
同一线程中,在调用Wait()前调用Signale()将会引发线程的异常。但不会出现在其他类型的同步对象中。
线程的中断是不会影响critical sections的状态的,因此使用critical sections的线程将不会被其他线程杀死,除非不在critical sections中。当不在需要时,线程的死亡是不会有癬,很安全的。

5)同步实例

class CMessageBuffer

    {

   public:

   CMessageBuffer();

   void AddMessage(const TDes &aMsg);

   void GetMessages(TDes &aMsgs);

 

   public:

   RMutex iMutex;

   TDes iMsgs;

   };

 

CMessageBuffer::CMessageBuffer()

    {

   iMutex.CreateLocal();

    }

 

void CMessageBuffer::AddMessage(const TDes&aMsg)

    {

   iMutex.Wait();

   iMsgs.Append(aMsg);

   iMutex.Signal();

    }

 

void CMessageBuffer::GetMessages(TDes&aMsgs)

    {

   iMutex.Wait();

   aMsg.Copy(iMsgs);

   iMsgs.Zero();

   iMutex.Signal();

    }

 

static void CMyClass::threadFunction(TAny*aPtr)

    {

   CMessageBuffer *msgBuffer = (CMessageBuffer *)TAny;

   TInt count = 0;

   TBuf<40> msg;

 

   while (TRUE)

       {

       msg.Format(“ID: %d, count: %d\n”, RThread().Id(), count++);

        msgBuffer->AddMessage(msg);

       User::After(1000 * 1000);

       }

    }

在上面所述中,CMessageBuffer是一个半成品类,它允许用户增加消息到buffer中,也允许获得所有消息。
线程函数CMyClass::threadFunction负责向CMessageBuffer共享对象添加信息,这里内存分配和错误检查并没有列出,需要读者自己完成。
假设有多个线程要共享CMessageBuffer对象实例,则在实际访问buffer时必须要同步来处理。我们也可在线程函数中完成,但在CMessageBuffer中完成同步将使得该类成为线程安全级,同样它也可以被用在单个线程中了。

六、总结
  很多情况下都需要多线程的,当使用多线程时,同步及互斥排他也要考虑在内,以便保证线程通信的安全性。如果线程使用共享资源,我们应该使用某种同步机制来避免异常的发生,Semaphores, critical sections,和mutexes都提供了基本的解决方案。此外,如果使用活动对象或清除机制,我们还需要手工设置active scheduler和清除栈。总的来说,线程编程不是这么容易的,因为这类编程需要全面理解框架、多任务和线程安全机制。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值