螺旋滚筒输送机:线程学习之笔记
来源:百度文库 编辑:九乡新闻网 时间:2024/10/02 17:49:04
线程学习(1) 线程的基础知识
对于了解任何一种事物,最容易也最常见的方法自然是先学习他的基础知识,先知其然。循序渐进,这很重要。对于线程当然也不例外。那么线程会有哪些基础的知识要了解呢?我在这里不会大篇的介绍那些API如何使用,也不会介绍一些库的使用,比如MFC啥的。要了解线程的基础知识,我想只需要回答四个问题即可:
1.什么是线程?
2. 什么时候应该用多线程?
3. 什么时候又不能用多线程?
4. 如何创建线程?
5. 如何终止线程?
回答了这五个问题,对于线程自然会有了基本的了解,那么你也就进入了线程的世界。首先我们来看第一个问题:
什么是线程?
每个进程至少包含一个线程,一个进程包括一个进程对象,一个地址空间。同样线程由两个部分组成:线程内核对象和线程栈。操作系统通过线程内核对象管理线程,同时内核对象也是系统用来存放线程统计信息的地方。线程栈用于维护线程在执行代码时需要的所有函数参数和局部变量。进程是从来不执行任何东西的,他只是线程依存的地方。线程总是在某个进程中创建,并且整个生命周期都在进程中。也就是说,线程在他的进程地址空间中执行代码,对相同的数据进行操作。当然还能共享内核对象句柄。因为内核对象句柄表是依赖于进程而不是线程。
很多人都知道线程比进程“重”,为什么呢?因为:
1. 进程需要更多的地址空间所以进程使用的系统资源比线程多很多。为进程创建一个虚拟地址空间需要很多系统资源。需要占用大量的内存来保留大量记录。
2. 由于.exe或.dll文件要加载到一个弟子空间,所以需要文件资源。而线程使用的系统资源少很多。
什么时候应该用线程?
用一句话来回答就是:当你想一心二用的时候你就应该使用多线程!比如我现在想一边敲这些字一边和可乐就得用多线程。在你炒菜的同时又要烧水也得用多线程(并发)。。当你很用心在写一段很长的代码的时候,如果别人在这个时候叫你,你不希望听不见那么也得用多线程(防止阻塞,UI假死)。再比如分房的年代,如果是按照人头分房,你希望分的房子大一点那么你也得用多线程(可能会获得更多的CPU时间片,特别是在多核上)。如果你是一个创业者,当你的公司渐渐的长大,人越来越多,事情也越来越复杂的时候,你希望不同的人去做不同的事情,你希望把更多的资源给重要的人,而不希望(至少希望不是很多)那些不太重要的事情占用你某些宝贵的资源的时候,你也得用多线程(优先级)。因此我认为有四种情况,我们是需要使用多线程的。即:
1. 有多件事情,顺序执行无法满足的时候;
2. 在处理长时间的事情(算法)时为了防止应用界面(UI)不响应用户输入,造成UI假死的时候;比如大图像渲染、大数据处理/排序、搜索等
3. 为了通过获得更多的CPU时间片来提高程序效率的时候;
4. 需要同时处理的事情有优先级别的时候;应该使用高优先级线程管理对时间要求很急的任务,而使用低优先级线程执行被动任务或者对时间不敏感的任务。高
什么时候不应该使用多线程?
非常重要也是初学多线程编程很容易犯错误的一点就是不要将你的程序的界面(窗口)放到不同的线程中去。除非你能做到像explorer那么好。还有编辑/打印问题。当然都不是绝对的。关键是要用好,不能随便用。
如何创建线程?
线程都必须有一个入口函数,线程从这个入口开始运行。主线程的入口函数可以为:main,wmain,WinMain,wWinMain.创建的辅助线程也必须有入口函数,函数形式:
DWORD WINAPI ThreadFunc(PVOID pvParam)
{
DWORD dwResult = 0;
……
return dwResult ;
}
在线程函数中必须返回一个值,他将成为该进程的代码。而且应该尽可能使用参数和局部变量。当使用静态变量和全局变量时,多个线程可以同时访问这些变量,就可能会破坏变量的内存。但是参数和局部变量是在线程栈中创建的,所以不太可能别其他线程破坏。
创建线程使用CreateThread函数。这是系统提供的唯一一个创建线程的函数。这个函数首先创建一个线程内核对象,如何从进程的地址空间分配内存供线程使用。线程可以访问进程的内核对象的所有句柄、进程中的所有内存以及同进程的其他线程的栈。因此同进程中多个线程能够非常容易相互通信。虽然CreateThread是系统提供的唯一一个创建线程的函数,但是如果你是使用C/C++编写多线程,却不能使用这个函数,而是应该使用编译器提供的替代函数。比如VC提供的是_beginthreadex函数。可能你会问为什么。要回答这个问题,得从c运行库说起。C运行库是1970年问世的。那时候还没有任何线程的应用,因此,C运行库自然是不支持多线程的。存在问题的函数包括errno,_doserrno,strtok,_wcstok,strerror,_strerror,tmpnam,tmpfile,asctime,_wasctime,gmtime,_ecvt和_fcvt等。存在问题是因为这些内容都是活类似全局变量,线程直接是相互覆盖的,也就是不是线程安全的。要解决也很容易。只要创建一个数据结构保存这些内容,并将他和线程关联就可以了。但是问题也来了,系统又不知道你不安全,甚至连你是不是C/C++程序都不知道。_beginthreadex函数是vc的crt函数。幸好MS提供了源代码。看看源代码我们就知道一切了。下面是从VC8的crt中的threadex.c文件中拷贝而来。
_MCRTIMP uintptr_t __cdecl _beginthreadex (
void *security,
unsigned stacksize,
unsigned (__CLR_OR_STD_CALL * initialcode) (void *),
void * argument,
unsigned createflag,
unsigned *thrdaddr
)
{
_ptiddata ptd; /* pointer to per-thread data */
uintptr_t thdl; /* thread handle */
unsigned long err = 0L; /* Return from GetLastError() */
unsigned dummyid; /* dummy returned thread ID */
/* validation section */
_VALIDATE_RETURN(initialcode != NULL, EINVAL, 0);
/* Initialize FlsGetValue function pointer */
__set_flsgetvalue();
/*
* Allocate and initialize a per-thread data structure for the to-
* be-created thread.
*/
if ( (ptd = (_ptiddata)_calloc_crt(1, sizeof(struct _tiddata))) == NULL )
goto error_return;
/*
* Initialize the per-thread data
*/
_initptd(ptd, _getptd()->ptlocinfo);
ptd->_initaddr = (void *) initialcode;
ptd->_initarg = argument;
ptd->_thandle = (uintptr_t)(-1);
#if defined (_M_CEE) || defined (MRTDLL)
if(!_getdomain(&(ptd->__initDomain)))
{
goto error_return;
}
#endif /* defined (_M_CEE) || defined (MRTDLL) */
/*
* Make sure non-NULL thrdaddr is passed to CreateThread
*/
if ( thrdaddr == NULL )
thrdaddr = &dummyid;
/*
* Create the new thread using the parameters supplied by the caller.
*/
if ( (thdl = (uintptr_t)
CreateThread( (LPSECURITY_ATTRIBUTES)security,
stacksize,
_threadstartex,
(LPVOID)ptd,
createflag,
(LPDWORD)thrdaddr))
== (uintptr_t)0 )
{
err = GetLastError();
goto error_return;
}
/*
* Good return
*/
return(thdl);
/*
* Error return
*/
error_return:
/*
* Either ptd is NULL, or it points to the no-longer-necessary block
* calloc-ed for the _tiddata struct which should now be freed up.
*/
_free_crt(ptd);
/*
* Map the error, if necessary.
*
* Note: this routine returns 0 for failure, just like the Win32
* API CreateThread, but _beginthread() returns -1 for failure.
*/
if ( err != 0L )
_dosmaperr(err);
return( (uintptr_t)0 );
}
从代码中清楚的看到每个线程都有一个属于自己的tiddata数据结构。传进来的参数和现场函数地址都保存在这里。当然他正如我们预料的一样最终调用了CreateThread函数。只是没有把传进来的线程函数和参数传给CreateThread。而是_threadstartex和tiddata。下面我们接下来看一下这两个东西。tiddata位于mtdll.h,_threadstartex和_beginthread在同一个文件。
/* Structure for each thread’s data */
struct _tiddata {
unsigned long _tid; /* thread ID */
uintptr_t _thandle; /* thread handle */
int _terrno; /* errno value */
unsigned long _tdoserrno; /* _doserrno value */
unsigned int _fpds; /* Floating Point data segment */
unsigned long _holdrand; /* rand() seed value */
char * _token; /* ptr to strtok() token */
wchar_t * _wtoken; /* ptr to wcstok() token */
unsigned char * _mtoken; /* ptr to _mbstok() token */
/* following pointers get malloc’d at runtime */
char * _errmsg; /* ptr to strerror()/_strerror() buff */
wchar_t * _werrmsg; /* ptr to _wcserror()/__wcserror() buff */
char * _namebuf0; /* ptr to tmpnam() buffer */
wchar_t * _wnamebuf0; /* ptr to _wtmpnam() buffer */
char * _namebuf1; /* ptr to tmpfile() buffer */
wchar_t * _wnamebuf1; /* ptr to _wtmpfile() buffer */
char * _asctimebuf; /* ptr to asctime() buffer */
wchar_t * _wasctimebuf; /* ptr to _wasctime() buffer */
void * _gmtimebuf; /* ptr to gmtime() structure */
char * _cvtbuf; /* ptr to ecvt()/fcvt buffer */
unsigned char _con_ch_buf[MB_LEN_MAX];
/* ptr to putch() buffer */
unsigned short _ch_buf_used; /* if the _con_ch_buf is used */
/* following fields are needed by _beginthread code */
void * _initaddr; /* initial user thread address */
void * _initarg; /* initial user thread argument */
/* following three fields are needed to support signal handling and
* runtime errors */
void * _pxcptacttab; /* ptr to exception-action table */
void * _tpxcptinfoptrs; /* ptr to exception info pointers */
int _tfpecode; /* float point exception code */
/* pointer to the copy of the multibyte character information used by
* the thread */
pthreadmbcinfo ptmbcinfo;
/* pointer to the copy of the locale informaton used by the thead */
pthreadlocinfo ptlocinfo;
int _ownlocale; /* if 1, this thread owns its own locale */
/* following field is needed by NLG routines */
unsigned long _NLG_dwCode;
/*
* Per-Thread data needed by C++ Exception Handling
*/
void * _terminate; /* terminate() routine */
void * _unexpected; /* unexpected() routine */
void * _translator; /* S.E. translator */
void * _purecall; /* called when pure virtual happens */
void * _curexception; /* current exception */
void * _curcontext; /* current exception context */
int _ProcessingThrow; /* for uncaught_exception */
void * _curexcspec; /* for handling exceptions thrown from std::unexpected */
#if defined (_M_IA64) || defined (_M_AMD64)
void * _pExitContext;
void * _pUnwindContext;
void * _pFrameInfoChain;
unsigned __int64 _ImageBase;
#if defined (_M_IA64)
unsigned __int64 _TargetGp;
#endif /* defined (_M_IA64) */
unsigned __int64 _ThrowImageBase;
void * _pForeignException;
#elif defined (_M_IX86)
void * _pFrameInfoChain;
#endif /* defined (_M_IX86) */
_setloc_struct _setloc_data;
void * _encode_ptr; /* EncodePointer() routine */
void * _decode_ptr; /* DecodePointer() routine */
void * _reserved1; /* nothing */
void * _reserved2; /* nothing */
void * _reserved3; /* nothing */
int _cxxReThrow; /* Set to True if it’s a rethrown C++ Exception */
unsigned long __initDomain; /* initial domain used by _beginthread[ex] for managed function */
};
typedef struct _tiddata * _ptiddata;
从上面的代码可以看出很多内容就是前面我们提到的C运行库不安全的内容。/****_threadstartex() - New thread begins here**Purpose:* The new thread begins execution here. This routine, in turn,* passes control to the user’s code.**Entry:* void *ptd = pointer to _tiddata structure for this thread**Exit:* Never returns - terminates thread!**Exceptions:********************************************************************************/static unsigned long WINAPI _threadstartex ( void * ptd ){ _ptiddata _ptd; /* pointer to per-thread data */ /* Initialize FlsGetValue function pointer */ __set_flsgetvalue(); /* * Check if ptd is initialised during THREAD_ATTACH call to dll mains */ if ( ( _ptd = (_ptiddata)__fls_getvalue(__get_flsindex())) == NULL) { /* * Stash the pointer to the per-thread data stucture in TLS */ if ( !__fls_setvalue(__get_flsindex(), ptd) ) ExitThread(GetLastError()); /* * Set the thread ID field — parent thread cannot set it after * CreateThread() returns since the child thread might have run * to completion and already freed its per-thread data block! */ ((_ptiddata) ptd)->_tid = GetCurrentThreadId(); } else { _ptd->_initaddr = ((_ptiddata) ptd)->_initaddr; _ptd->_initarg = ((_ptiddata) ptd)->_initarg; _ptd->_thandle = ((_ptiddata) ptd)->_thandle;#if defined (_M_CEE) || defined (MRTDLL) _ptd->__initDomain=((_ptiddata) ptd)->__initDomain;#endif /* defined (_M_CEE) || defined (MRTDLL) */ _freefls(ptd); ptd = _ptd; } /* * Call fp initialization, if necessary */#ifndef MRTDLL#ifdef CRTDLL _fpclear();#else /* CRTDLL */ if (_FPmtinit != NULL && _IsNonwritableInCurrentImage((PBYTE)&_FPmtinit)) { (*_FPmtinit)(); }#endif /* CRTDLL */#endif /* MRTDLL */#if defined (_M_CEE) || defined (MRTDLL) DWORD domain=0; if(!_getdomain(&domain)) { ExitThread(0); } if(domain!=_ptd->__initDomain) { /* need to transition to caller’s domain and startup there*/ ::msclr::call_in_appdomain(_ptd->__initDomain, _callthreadstartex); return 0L; }#endif /* defined (_M_CEE) || defined (MRTDLL) */ _callthreadstartex(); /* * Never executed! */ return(0L);}这个函数的主要功能就是使用线程本地存储将数据结构tiddata和线程关联起来。以及一个SEH。至此,我们知道了为什么C/C++运行库的函数需要为为他创建的每个函数设置单独的内存块,同时也了解了如何通过调用_beginthreadex函数来分配内存块以及初始化,并且还和线程关联起来。 下面我们来看一下如果应用程序直接调用CreateThread会如何。首先他肯定没有创建与线程关联的内存块,但是在运行的过程中很有可能有其他的C++运行库的函数企图去获得线程内存块(TlsGetValue)。结果当然是返回NULL。这是C++运行库会自动为线程创建一个内存块。看起来很完美,系统想得真是周到。但是仍然有两个问题:1. 没有SEH。程序可能直接挂掉;2.系统自动创建的内存块谁负责释放呢? C++运行库还提供了另外一个函数创建线程:_beginthread。这个函数能用吗?能用!但是不提倡。因为:1.无法创建带有安全属性的线程;2.无法创建暂停的线程;3.无法返回线程ID。 如何销毁终止线程 终止线程运行有几种方法:1. 线程函数返回结束; 这是线程结束的唯一的正确方法。线程函数必须设计成需要线程结束时就能返回的形式。通过这种方式结束线程可以保证:a。所有C++对象正确的被销毁;b。线程正确的释放线程栈使用的内存;c。系统将线程函数的返回值设置成线程的推出码;d。系统递减线程的计数器;2.调用ExitThread; 用这种方式终止线程能让系统自动清除所有线程使用的所有系统资源。但是无法清除C++资源。3.其他线程调用TerminateThread; 这种方法无法实现1中的a和b两项。而且这个函数是异步函数。4.创建线程的进程终止; 这种方法最野蛮,问题也最多。1中的四项都无法保证。而且很有可能是线程要访问的资源不存在了,但是线程还在,这样必然导致弹出出错对话框。而且内存数据也不会存入硬盘。线程终止过程:1. 线程所有的用户对象将被释放,但是窗口和钩子比较特殊,当线程终止时,所有窗口将被销毁,同时所有的钩子将被卸载。其他资源在进程终止时才被销毁。2. 线程的退出代码从STILL_ACTIVE改为ExitThread或TerminateThread的代码。3. 线程内核对象的状态变成已经通知。4. 如果线程是进程的最后一个线程,那么该进程也被视为已经终止运行。5.线程内核对象的使用计数递减1.一个线程终止运行时,在线程的内核对象所有相关联的引用都关闭之前,该内核对象不会被自动释放。前面线程的创建中我们介绍了不用直接使用CreateThread而应该使用C++运行库提供的函数,VC提供的是_beginthreadex,也不要使用_beginthread.那么线程结束也应该使用C++运行库提供的函数。VC提供了两个函数_endthreadex和_endthread.void __cdecl _endthreadex(unsigned retcode){_ptiddata ptd;pid = _getptd();__freeptd(ptd);ExitThread(retcode)}从该函数的实现上看,我们知道他做了两个事情,一个是释放内存块。然后调用系统的ExitThread,真正推出线程。现在大家知道为什么前面我们说直接调用ExitThread会引起内存块的泄漏。因此如果想要强制终止线程可以调用_endthreadex而不是调用ExitThread。但是一般不要调用。 我们来看一下_endthread函数。他和_endthreadex有点不同。最大的区别在于它在调用ExitThread之前调用了CloseHandle,如果这时候线程的计数器已经为0,那么内核对象将被释放。此后所有其他线程调用此内核对象都将失败。 最后我们来看一下,线程的标识。标识有两个,一个是句柄,一个是ID。句柄用得比较多。可以通过GetCurrentThread获得当前线程句柄。但是要注意的是该句柄是一个伪句柄。线程伪句柄是一个特别的数(0xfffffffe),只是代表当前的线程句柄(假如将某个线程的伪句柄拿到另一个线程去使用,那么这个伪句柄实际操作另一个线程),因此它不会影响线程的引用计数。所以不需要调用CloseHandle。当然调用了不会有问题,CloseHandle会忽略。线程的真实句柄:每个进程有一张内核对象表,这个表里放置进程内打开的所有内核对象,并给每个对象分配一个序号,线程句柄实际上就是内核对象表中对应线程对象的序号。因此句柄与进程相关。在这个进程中的句柄在不能随意拿到另一个进程中使用,可以通过DuplicateHandle进行句柄拷贝。打开一个句柄,会使线程对象的引用技术加一,CloseHandle会使线程对象引用计数减一,所以使用完句柄后需要进行关闭。
线程学习(2)线程的实现细节
关于线程的实现细节,我们这里只讨论两个问题。即线程的内核对象和线程栈以及线程的创建过程。上一章我们提到,线程是由线程内核对象以及线程栈两个部分组成的。系统通过线程的内核对象管理线程。线程栈则维护参数和局部变量。
1. 线程的内核对象和线程栈
下表很清楚的表明了内核对象和线程栈的内容以及直接的管理。
调用了CreateThread之后,系统会创建线程的内核对象。内核对象包括一个上下文(用于恢复现场)。从上图可以看出。SP指向了线程栈的首地址。IP指向了系统提供的一个函数。引用计数是表示当前内核对象的引用次数,当递减到0,内核对象将被删除。挂起计数表示此内核对象被挂起的计数,当减到0时,线程成为可调度状态。刚刚创建的线程退出码都是STILL_ACTIVE。都是没有信号状态。也许你问一个问题:为什么初始引用计数要等于2呢?因为线程创建的时候要返回一个句柄,他拥有了一个计数,另一个计数是被创建的新线程自己拥有的。在线程函数返回时递减。弄清楚了这个我们也就清楚了在线程函数最后返回前是否需要调用_endthread或_endthreadex或调用CloseHandle关闭自己。线程内核对象只有在计数器减到0时才会被系统销毁。销毁之后再调用与线程句柄相关的函数都可能出错。比如CloseHandle,当然如果传入的是伪句柄不一定,但是可能递减了其他线程的计数器。
线程的内核对象一旦创建完毕,系统就分配用于线程栈的内存。内存是用进程的地址空间分配来的。然后,系统将两个值写入栈的上端。第一个值是传给CreateThread的参数(最后传入线程函数),另一个是线程函数的地址。从上图可以看到,内核对象初始化后,IP指向了一个系统的函数BaseThreadStart而不是用户的线程函数。否则系统就无法在线程函数返回时调用ExitThread函数了。
1. 线程的创建过程
a.建立线程内核对象
b.设置内核对象初始值:比如计数器=2等
c.分配内存给线程栈
d.初始化线程栈:将线程函数和参数push
e.初始化上下文内容:比如IP和SP以及其他CPU寄存器
f. 调用BaseThreadStart函数
g.调用用户线程函数
线程学习(3)线程优先级和调度
终于开始写到3了,最近一直很忙,似乎还有点累。上班时间是不可能有空写的,只有回到家等女儿睡着了才能坐下来。每每这个时候也不早了,1:23,现在。后面还有好多,还得加油。
说到线程的调度,我觉得还是得从线程的优先级讲起。在Windows中,每个线程都被赋予了优先级的概念,线程总共有32个优先级数从0(最低)到31(最高)。从前面的章节我们知道,线程是从属于某个进程的,那么对于某个具体进程的某个线程的优先级是怎么确定的呢?这得从几个优先级概念说起。一个线程的优先级是由进程的基本优先级(或者叫优先级类)和线程的相对优先级决定的。因为我们无法直接设置一个线程的优先级数,比如2或29。
进程的基本优先级:他是通过调用系统API—SetPriorityClass来设置,运行中的进程的优先级可以通过任务管理器查看。基本优先级是针对进程来讲的,基本优先级有8个类别,分别是:ABOVE_NORMAL_PRIORITY_CLASS,
BELOW_NORMAL_PRIORITY_CLASS,
HIGH_PRIORITY_CLASS,IDLE_PRIORITY_CLASS,
NORMAL_PRIORITY_CLASS,
PROCESS_MODE_BACKGROUND_BEGIN,
PROCESS_MODE_BACKGROUND_END,
REALTIME_PRIORITY_CLASS.
具体的含义可以查看MSDN。如果程序没有进行特别设置,那么进程的基本优先级将会设置为NORMAL_PRIORITY_CLASS这个级别。先对来说,使用高于正常的优先级需要特别小心,特别是REALTIME_PRIORITY_CLASS.他表示进程中线程必须立即对事件作出响应,以便执行关键时间的任务.该进程中的线程还会抢先于操作系统组件之前运行.使用本优先级类时必须极端小心.
线程相对优先级:相对优先级是针对线程的,他是通过调用SetThreadPriority函数设置的。具体参数可以参考MSDN.那么线程的优先级到底是怎么确定的呢?基本优先级和相对优先级如何结合的呢?下表就是他们的对应关系,是从MSDN上copy过来的。不过M$也说过他们不承诺任何关于优先级级数的保证。因为这个未来可能会变的。所以写程序千万不要直接依赖于有限级数。
具体的含义可以查看MSDN。如果程序没有进行特别设置,那么进程的基本优先级将会设置为NORMAL_PRIORITY_CLASS这个级别。先对来说,使用高于正常的优先级需要特别小心,特别是REALTIME_PRIORITY_CLASS.他表示进程中线程必须立即对事件作出响应,以便执行关键时间的任务.该进程中的线程还会抢先于操作系统组件之前运行.使用本优先级类时必须极端小心.
线程相对优先级:相对优先级是针对线程的,他是通过调用SetThreadPriority函数设置的。具体参数可以参考MSDN.那么线程的优先级到底是怎么确定的呢?基本优先级和相对优先级如何结合的呢?下表就是他们的对应关系,是从MSDN上copy过来的。不过M$也说过他们不承诺任何关于优先级级数的保证。因为这个未来可能会变的。所以写程序千万不要直接依赖于有限级数。
比如两个优先级都是正常的级别,那么线程的优先级就是8。再如基本优先级是REALTIME_PRIORITY_CLASS ,|
相对优先级是THREAD_PRIORITY_TIME_CRITICAL,那么线程的优先级将是31。
如果你够细心的话,你也许发现了一个问题,就是前面说到线程的优先级是从0到31的,但是从表中来看并没有那种组合的结果优先级是0.没错,真的没有0,确实是组合不错来的。因为当系统引导的时候,它会创建一个特殊的线程,称为0页线程.该线程被赋予优先级0,它是整个系统中唯一的一个在优先级0上运行的线程 。当系统中没有任何线程需要执行的时候,0页线程负责将系统中的所有空闲的RAM页面置0.除了0还有其他一些数字在上面表里也找不到,比如17,18,19,20,21,27,28,29,30都没有。17,18,19,20,21,27,28,29,30。如果编写一个以内核方式运行的设备驱动程序,可以获得这些优先级的等级,而用户方式的应用程序则不能.
说到了线程优先级,不得不说一下硬件中断。我们知道不同的处理器的中断机制是不一样的,虽然中断控制器做了中断级别的优化工作,Windows还是强制使用自己的中断优先级方案,即IRQLs。内核在内部以数字表示IRQLs,x86上是从0到31,x64和IA64上是从0到15,数字越高则中断的优先级别越高。内核为软件中断定义了一系列标准的IRQLs,HAL也将硬件诊断号映射到IRQLs。
中断是根据优先级被响应的,一个高优先级的中断会比一个低优先级的中断先被响应。当有一个高优先级的中断产生,处理器保存被中断线程的状态并呼叫与之相关的陷阱调度器(trapdispatchers)。陷阱调度器提升IRQL并调用该中断的服务例程。在中断例程结束之后,陷阱调度器降低处理器的IRQL到中断发生前的级别,接着载入之前存储的机器状态。被中断的线程从被中断的点重新开始执行。当内核降低IRQL后,之前被屏蔽的优先级较低的中断就可能被接收。如果顺利的话,内核会重复这一过程来处理新的中断。
每个处理器的IRQL设置决定了该处理器可接收的中断。IRQLs也被用来同步访问内核模式的数据结构。当一个线程在内核模式下运行,它既可以直接调用KeRaiseIrql和KeLowerIrql来提升或降低处理器的IRQL,也可以间接地通过调用函数获得内核同步物件以提升或降低处理器的IRQL。从拥有高于当前IRQL的中断源发出的中断可以中断处理器,而从有着小于或等于当前IRQL的中断源发出的中断会被屏蔽,直到有执行线程降低该IRQL。
一个内核模式的线程提升或降低运行它的处理器的IRQL取决于它的任务。例如,如果一个中断发生,陷阱处理程序(或者是处理器)会提升处理器的IRQL到为该中断源指定的IRQL。这个高度屏蔽了所有小于或等于该IRQL的中断(只作用于该处理器),保证了在处理器响应该中断的过程中不会被同级或级别较低的中断打断。被屏蔽的中断要么被交由其它的处理器处理,要么就要等到IRQL降下来。因此,系统中的所有组件,包括内核和设备驱动都希望IRQL能够待在无源级别(也叫作低级别)。原因是如果IRQL不被长时间的置于较高的状态,设备驱动就可以及时地响应硬件中断。
IRQL的优先级与线程调度的优先级完全不同。后者是线程的一个属性,而IRQL是中断源,比如键盘、鼠标的一个属性。此外,每个处理器有一个IRQL设置,该设置在执行系统代码时发生变化。所有的线程都运行在中断优先级0和1上。内核态的异步调用运行在1上,用户线程运行在0上,内核线程可以中断用户线程。而且只有内核线程才能提高自己的终端优先级。用户线程虽然提高优先级可以阻塞系统线程,但是用户线程优先级的提高并不会阻塞硬件中断。而线程调度代码运行在DPC/线程调度中断优先级(2级)。这样可防止调度器代码与线程在访问调度器数据结构时发生冲突。
在了解了什么是优先级以及如何设置优先级和中断之后,我们来了解另外一个概念—–时间配额。
时间配额是一个线程从进入运行状态到Windows检查是否有其他优先级相同的线程需要开始运行之间的时间总和。每个线程都有一个代表本次运行最大时间长度的时间配额。时间配额不是一个时间长度值,而一个称为配额单位(quantum unit)的整数。 缺省时,在Windows2000专业版中线程时间配额为6;而在Windows2000服务器中线程时间配额为36。注册表项:HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\PriorityControl\Win32PrioritySeparationWin32PrioritySeparation 可以修改时间配额。
线程状态
线程在不同的时间有不同的线程状态,在NT内核的windows中线程总共有7种状态:
线程状态
说明
Ready(就绪)
此状态下的线程正在等待执行,当调度程序需要找一个线程来执行时,它仅考虑就绪状态下的线程池。
Standby(备用)
已经被选中(当前活动线程的后继),当条件合适时,调度程序对这个线程执行一个上下文转换,备用线程将被切换到某个特定的处理器上运行。对于系统中的每一个处理器,只能有一个线程处于备用状态。
Running(运行)
一旦调度程序将环境切换到某个(备用)线程,这个线程就进入运行状态并开始执行。线程一直执行,直到内核将其抢占去运行一个更高优先级的线程,或者它的时间片到结束运行或自动进入等待状态。
Waiting(等待)
一个线程可能因为以下几个原因而进入等待状态:(1)自动等待一个对象以便同步它的执行。(2)操作系统可以代替该进程进入等待(如为了解决换页I/O)。(3)环境子系统引导线程挂起。
线程等待状态结束后,根据其优先级,开始执行,或者进入就绪状态。
Transition (转变)
当一个线程已经准备好执行,但它的内核栈被换出了内存,这时线程就进入转变状态。一旦它的内核栈被换入内存,线程就进入就绪状态。
Terminated (终止)
当一个线程完成执行,它就进入终止状态。终止后,线程对象可能被删除,也可能不被删除,这将取决于对象管理器什么时候删除对象的策略。如果执行体中有一个指针指向线程对象,执行体可以对线程对象重新初始化并再次使用它。
Initialized (初始)
当一个线程被创建时的状态。(内部使用)
下面这个图是各个状态之间的转换关系:
处理器的亲合性
按照系统默认设置,当系统将CPU分配给线程时,如果其他因素都相同的话,那么系统将设法在线程上次运行的那个处理器上再次运行线程。这样是为了重复使用CPU的高速缓存。这就是系统的软亲合性。但是为了适应某些特殊情况,系统将允许用户设置线程或进程的亲合性。也就是允许哪个CPU能允许哪些线程。这就是处理器的硬亲合性。API为:SetProcessAffinityMask/SetThreadAffinityMask。
线程调度
Windows是一个基于优先级的抢占式多处理器调度系统。调度系统总是运行优先级最高的就绪线程。Windows的没有单独的调度模块或程序,调度的代码是在内核中实现的,广泛分布在内核中那些与调度相关的事件发生的地方。这些负责调度的程序被总称为“内核的调度器”。线程调度发生在DPC/Dispatch级别。 当一个线程进入运行状态时,每次时钟中断都会从时间配额中减少一个固定值(一般是3)。当配额用完后,系统中断线程,看是否需要降低线程优先级,并查找是否有其他高优先级或相同优先级的线程在等待运行。因为windows是一个抢占式的操作系统,,因为一个线程可能会在自己时间配额还没用完的时候就被其他已经就绪的高优先级的线程给抢先了。而且用户线程可以抢先内核线程。抢先时只与优先级有关而不关心是用户线程还是内核线程。处于实时优先级的线程被抢先时,时间配额被重置为一个完整的时间片,而处于动态优先级的线程被抢先时,时间配额不变。当一个线程出现等待事件时,时间配额会被减1,当线程优先级大于14时,优先级被重置。有四种情况会引起线程的调度:
- 变成就绪状态的线程。例如:一个新创建的线程,或者从等待状态释放出来的线程。
- 因其时间配额用完而离开运行状态的线程,它或者结束了,或者进入等待状态。
- 线程的优先级改变了,是因为系统调用,或者是Windows自己改变了优先级。
- 正在运行的线程的处理器亲合性改变了。
在每一个上述情况的衔接点,Windows必须决定下一个运行的线程是哪一个。一旦选择了一个新的线程运行,Windows将对其执行一个上下文转换的操作,即保存正在运行的线程的相关的机器状态,装载另一个线程的状态,开始新线程的执行。
动态提高优先级
windows(2k/xp)在下面5种情况会自动提高线程的优先级:
2. 为了避免I/O操作导致对某些线程的不公平偏好,在I/O操作完成后唤醒等待线程时将把该线程的时间配额减1
3.线程优先级的实际提升值是由设备驱动程序决定的。与I/O操作相关的线程优先级提升建议值在文件“Wdm.h”或“Ntddk.h”中。设备驱动程序在完成I/O请求时通过内核函数IoCompleteRequest来指定优先级提升的幅度。
4. 线程优先级的提升幅度与I/O请求的响应时间要求是一致的,响应时间要求越高,优先级提升幅度越大
2. 阻塞于事件或信号量的线程得到的处理机时间比处理机繁忙型线程要少,这种提升可减少这种不平衡带来的影响。
3. SetEvent、PulseEvent或ReleaseSemaphore函数调用可导致事件对象或信号量对象等待的结束。
4.提升是以线程的基本优先级为基点的,而不是线程的当前优先级。提升后的优先级永远不会超过15。在等待结束时,线程的时间配额被减1,并在提升后的优先级上执行完剩余的时间配额;随后降低1个优先级,运行一个新的时间配额,直到优先级降低到初始的基本优先级。
1. 对于前台进程中的线程,一个内核对象上的等待操作完成时,内核函数KiUnwaitThread会提升线程的当前优先级(不是线程的基本优先级),提升幅度为变量PsPrioritySeparation的值。
2. 在前台应用完成它的等待操作时小幅提升它的优先级,以使它更有可能马上进入运行状态,有效改进前台应用的响应时间特征。
3. 用户不能禁止这种优先级提升,甚至是在用户已利用Win32的函数SetThreadPriorityBoost禁止了其他的优先级提升策略时,也是如此。
1. 拥有窗口的线程在被窗口活动唤醒(如收到窗口消息)时将得到一个幅度为2的额外优先级提升。
2. 窗口系统(Win32k.sys)在调用函数KeSetEvent时实施这种优先级提升,KeSetEvent函数调用设置一个事件,用于唤醒一个图形用户接口线程。
3. 这种优先级提升的原因是改进交互应用的响应时间。
1. 系统线程“平衡集管理器(balance set manager)” 会每秒钟检查一次就绪队列,是否存在一直在就绪队列中排队超过300个时钟中断间隔的线程。
2. 如果找到这样的线程,平衡集管理器将把该线程的优先级提升到15,并分配给它一个长度为正常值两倍的时间配额;
3. 当被提升线程用完它的时间配额后,该线程的优先级立即衰减到它原来的基本优先级。
Sleep 函数
系统将在大约的指定毫秒数内使线程不可调度。Windows不是个实时操作系统。虽然线程可能在规定的时间被唤醒,但是它能否做到,取决于系统中还有什么操作正在进行。可以调用Sleep,并且为dwMilliseconds参数传递INFINITE。这将告诉系统永远不要调度该线程。这不是一件值得去做的事情。最好是让线程退出,并还原它的堆栈和内核对象。可以将0传递给Sleep。这将告诉系统,调用线程将释放剩余的时间片,并迫使系统调度另一个线程。但是,系统可以对刚刚调用Sleep的线程重新调度。如果不存在多个拥有相同优先级的可调度线程,就会出现这种情况。那么我们如何做到真正切换到另外一个线程呢?答案是可以调用SwitchtoThread函数。
SwitchtoThread函数
系统提供了SwitchToThread函数。当调用这个函数的时候,系统要查看是否存在一个迫切需要CPU时间的线程。如果没有线程迫切需要CPU时间,SwitchToThread就会立即返回。如果存在一个迫切需要CPU时间的线程,SwitchToThread就对该线程进行调度(该线程的优先级可能低于调用SwitchToThread的线程)。这个迫切需要CPU时间的线程可以运行一个时间段,然后系统调度程序照常运行。该函数允许一个需要资源的线程强制另一个优先级较低、而目前却拥有该资源的线程放弃该资源。如果调用SwitchToThread函数时没有其他线程能够运行,那么该函数返回FALSE,否则返回一个非0值。调用SwitchToThread与调用Sleep是相似的。差别是SwitchToThread允许优先级较低的线程运行;而即使有低优先级线程迫切需要CPU时间,Sleep也能够立即对调用线程重新进行调度。
对称多处理机系统上Windows 2000的线程调度
每个线程在对应的内核线程控制块中都保存着两个处理器标识:
首选处理器:线程运行时的偏好处理器
第二处理器:线程运行的第二选择处理器
首选处理器是基于进程控制块的索引值来随机选择的。索引值在创建每个线程时递增。线程一旦创建后,系统就不会修改线程的首选处理器设置,但是用户可以通过SetThreadIdleProcessor来修改.
当线程进入运行状态时,Windows首先试图调度该线程到一个空闲处理机上运行。如果有多个空闲处理机,线程调度器的调度顺序为:
–线程的首选处理机
–线程的第二处理机
–当前执行处理机(即正在执行调度器代码的处理机)。
–如果这些处理机都不是空闲的,Windows将依据处理机标识从高到低扫描系统中的空闲处理机状态,选择找到的第一个空闲处理机。
如果线程进入就绪状态时,所有处理机都处于繁忙状态,Windows将检查一个处于运行状态或备用状态的线程,判断它是否可抢先。检查的顺序如下:
–线程的首选处理机
–线程的第二处理机
–如果这两个处理机都不在线程的亲合掩码中,Windows将依据活动处理机掩码选择线程可运行的编号最大的处理机。
Windows并不检查所有处理机上的运行线程和备用线程的优先级,而仅仅检查一个被选中处理机上的运行线程和备用线程的优先级。
如果在被选中的处理机上没有线程可被抢先,则新线程放入相应优先级的就绪队列,并等待调度执行。为特定的处理机调度线程
在多处理机系统,Windows不能简单地从就绪队列中取第一个线程,它要在亲合掩码限制下寻找一个满足下列条件之一的线程。
–线程的上一次运行是在该处理机上;
–线程的首选处理机是该处理机;
–处于就绪状态的时间超过2个时间配额;
–优先级大于等于24;
如果Windows不能找到满足要求的线程,它将从就绪队列的队首取第一个线程进入运行状态。最高优先级就绪线程可能不处于运行状态.有可能出现这种情况,一个比当前正在运行线程优先级更高的线程处于就绪状态,但不能立即抢先当前线程,进入运行状态。
空闲线程
如果在一个处理机上没有可运行的线程,Windows会调度相应处理机对应的空闲线程。由于在多处理机系统中可能两个处理机同时运行空闲线程,所以系统中的每个处理机都有一个对应的空闲线程。Windows给空闲线程指定的线程优先级为0,该空闲线程只在没有其他线程要运行时才运行。
线程学习 (4) 线程的同步–1
对于多线程编程来说,最难得有两点:1. 线程的生命周期的管理;2.线程同步。在前面几节中我们讨论了第一个问题,现在我们来讨论第二个问题。到目前为止,我们创建线程都是相互独立的。线程之间没有任何瓜葛。因此他们都运行得很好。没有任何问题。性能也很高。这一切看起来都非常的美好。比起单线程来说简直好得太多了。然而不幸的是在多线程编程中,多个线程都是相互独立的事情太少了。更多的是虽然一个线程处理一些事情,另一个线程处理另外一些事情,但是其中一些线程需要了解其他线程的处理结果或者需要在其处理完成之后才能进行。最简单的情况比如:一个线程通过计算,将计算的结果写入一个全局变量,另外一个线程则读取全局变量进行显示。如果不采取任何同步措施,那么就无法保证读数据的线程读到的数据时写数据线程写入之后的结果。为了让线程了解其他线程结束之后的结果或者需要访问相同的资源并改变资源从而使得同一进程中的多个线程协调的工作就叫线程的同步。因此也就有两种基本情况是需要使用线程同步的:1. 当多个线程会访问同一个资源并且会改变资源;2.一个线程需要了解其他一个或多个线程何时结束(或者说是需要通知其他线程);打个比方说也就是“兄弟,这东西我且用着那,您得等会。”“大哥,我活干完啦,你上吧。”也就这么个意味。
要使用好多线程,线程同步是一定要过关的。线程同步是一个不小的话题,从大方面来说可以分为两大类,一类是用户模式同步对象,一类是内核模式同步对象。前面章节我们说过,使用内核对象是需要从用户态转入内核态的,而这个切换是非常花费时间的,大约是1000个时钟周期。所以用户模式的同步对象最大的特点是效率高。但是也有缺点:不能跨进程。而内核模式同步对象大概正好相反。内核对象可是系统级的东东,所谓钱多好办事,自然功能也就强多了。
用户模式同步对象包括原子访问和临界区;内核模式同步对象包括事件,等待定时器,信号量,互斥量。下面我们来学习每个对象的具体用法。
1. 原子访问
一直都不是很理解为啥叫原子访问。ATOM(阿童木),也许和那个认为原子是最小物质的年代的理论有关吧。
既然是最小的物质了,自然是不可以再分了。所以可以理解为原子访问就是不可以再分割的操作。也就是不会被其他更高优先级中断抢先得操作。说得通俗点就是系统能保证这个操作是一次性搞定的,中途绝不能休息。互锁函数的家族十分的庞大,可以查看msdn(http://msdn.microsoft.com/en-us/library/ms683597(VS.85).aspx)以InterLocked开始的函数都是户数函数。使用互锁函数的优点是:他的速度要比其他的CriticalSection,Mutex,Event,Semaphore快很多。通常少于50个时钟周期
一般的互锁函数:
链表的互锁函数:
具体用法可以查看MSDN。在众多的原子操作函数中,最常见的有以下几个:
InterlockedExhangeAdd,InterlockedExchange,InterlockedCompareExchange,InterlockedIncrement和InterlockedDecrement。最后两个在COM中应该很熟悉。
2. 临界区
临界区是指一小段代码,在执行前必须独占对某些资源的访问权。也就是以原子操作的方式来访问资源。换句话说就是你用了别人就没法用了。要用怎么办?等呗。等多久?先占得人说了算!中国有句古话叫“占着茅坑不拉屎”。在这里用太合适了。茅坑就是我们说的资源,想用茅坑的人就是线程啦。一个家伙进去了,同时把牌牌给翻成红色,这时候再有人要进去就得等了。只有里面那个家伙出来并且把牌牌翻成绿色的才能进去。所以用临界区得非常小心才行。如果出来那家伙人走了,可是忘记把牌牌翻成绿色的话,虽然人走了,不过坑就算是废啦。可是可是你还在傻傻的等。如果有个家伙进去了,可是忘记把牌牌翻成红色的,这时你到了门口一看是绿牌牌自然就进去了。。。oh,My God!所以各位千万记得使用临界区时一定记得进去出来都得“翻牌牌”。切记切记!
临界区是用户态的同步对象,因此只能在进程内使用,他无法跨进程。在使用的过程中也无法移动或拷贝或修改。对于线程的获取顺序没有定义,因此不能假定哪个线程将会首先获得临界区。使用完临界区必须执行删除操作。而且一个临界区是不允许进行重复初始化的。否则将产生不可预料的结果。
临界区的使用也很简单,主要有四个函数:
void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);初始化临界区。这个函数虽然没有返回值,但是在低内存时会抛出STATUS_NO_MEMORY 的异常.