线程基础
进程有2个部分组成:进程内核对象和地址空间
线程也有2个部分组成:线程内核对象(用于管理线程)和线程栈(用于存储函数参数和局部变量)
一个进程内的所有线程共享一个地址空间,共享内核对象句柄(句柄表是针对每个进程的)
6.1 何时创建线程
各种在进程中进行的其他检测都可以以线程的方式进行
6.2 何时不应创建线程
不应该并行化的时候,比如打印操作,创建界面。。(提示不应盲目的使用多线程)
6.3 编写第一个线程函数
每个线程都需要一个入口点函数。这是线程执行的起点。主线程的入口点函数是_tmain或_tWinmain。如果在进程中创建新线程必须提供自己的入口点函数。
形如:
DWORD WINAPI ThreadFunc(PVOID pvParam){
DWORD dwResult = 0;
...
return(dwResult);
}
线程函数可以是任何我们希望它执行的任务,最终线程函数会终止并返回。类似于进程内核对象,如果线程内核对象使用计数变为0,则会被销毁。
1) 默认情况下主线程的入口点函数必须命名为main,wmain,WinMain或wWinMain。我们可以通过设置/ENTRY:链接器选项来指定另一个函数作为入口点函数。
2)主线程入口点函数有字符串参数,所以它提供了ANSI/Unicode版本。相反,线程函数只有一个参数,其意义可由我们定义。可以为其传递一个值,也可以将其作为某个数据结构的指针。这需要在线程函数内部做类型转换。
3)线程函数必须返回一个值,它的值传递给ExitThread,作为线程的退出代码。
4)线程函数尽可能地使用局部变量或函数参数,它们是在线程栈上创建的。不太可能被其他线程破坏。使用静态变量或全局变量时其他线程可以访问这些变量,这会导致同步和互斥问题。
6.4 CreateThread函数
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,// SD
SIZE_T dwStackSize,// initial stack size
LPTHREAD_START_ROUTINE lpStartAddress,// thread function
LPVOID lpParameter,// thread argument
DWORD dwCreationFlags,// creation option
LPDWORD lpThreadId // thread identifier
);
6.4.2 dwStackSize
设定线程栈大小,可以使用链接器的/STACK:[reserve] [,commit]开关来控制这个值,默认是一个页面的大小。取dwStackSize和开关的较大值。
设置栈上限1.可以防止耗尽物理内存区域 2.可以尽早察觉程序的bug
6.4.3 lpStartAddress和lpParameter
lpStartAddress线程函数地址(回调函数)如下:
DWORD WINAPI ThreadProc(
LPVOID lpParameter // thread data
);
一个线程终止时,系统会一次执行以下操作:
1 线程拥有的所有用户对象句柄会被释放;
2 线程的退出代码从STILE_ACTIVE变为传给ExitThread或TerminateThread函数的代码;
3 线程内核对象的状态变为已触发状态;
4 如果线程是进程中的最后一个活动线程,系统认为进程也终止了;
5 线程内核对象的使用计数递减1;
6.6 线程内幕
CreateThread 函数的一个调用导致系统创建一个线程内核对象,该对象最初的使用计数为2。( 创建线程内核对象加1,进程拥有的线程内核对象句柄加1 ),所以除非线程终止,而且 CreateThread 返回的句柄关闭,否则线程内核对象不会被销毁。该线程对象的其它属性也被初始化:暂停计数被设为1,退出代码被设备STILE_ACTIVE(0x103),而且对象被设为未触发状态。
创建了内核对象,系统就分配内存,供线程的堆栈使用。此内存是从进程的地址空间分配的,因为线程没有自己的地址空间。系统将来个值写入新线程堆栈的最上端,如图1所示,即调用的线程函数及其参数。
每个线程都有自己的一组CPU寄存器,称为线程的上下文(context)。上下文反映了当线程上一次执行时,线程CPU寄存器的状态。CONTEXT结构保存在线程的内核对象中。
当线程内核对象被初始化的时候,CONTEXT结构的堆栈指针寄存器被设为pfnStartAddr在线程堆栈中的地址。而指令指针寄存器被设为RtlUserThreadStart函数的地址。
VOID RtlUserThreadStart(PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam){
__try {
ExitThread((pfnStartAddr)(pvParam));
}
__except(UnhandledExceptionFilter(GetExceptionInformation())){
ExitProcess(GetExceptionCode());
}
// NOTE: We never get here.
}
线程完全初始化之后,系统检查CREATE_SUSPENDED标志是否已被传给CreateThread函数。如果此标记没有传递,系统将线程的挂起计数递减至0;随后,线程就可以调度给一个处理器去执行。然后,系统在实际的 CPU寄存器中加载上一次在线程上下文中保存的值。现在,线程可以在其进程的地址空间中执行代码并处理数据了。
新线程执行RtlUserThreadStart函数的时候,将发生以下事情:
围绕线程函数,会设置一个结构化异常处理(SEH)帧。这样一来,线程执行期间所产生的任何异常都能得到系统的默认处理。
系统调用线程函数,把传给CreateThread函数的pvParam参数传给它。
线程函数返回时,RtlUserThreadStart调用ExitThread,将你的线程函数的返回值传给它。线程内核对象的使用计数递减,而后线程停止执行。
如果线程产生了一个未被处理的异常,RtlUserThreadStart函数所设置的SEH帧会处理这个异常。通常,这意味着系统会向用户显示一个消息框,而且当用户关闭此消息框时,RtlUserThreadStart会调用ExitProcess来终止这个进程,而不是终止有问题的线程。
当一个进程的主线程初始化时,其指令指针指向RtlUserThreadStart,当RtlUserThreadStart开始执行时,它会调用C/C++运行库的启动代码,后者初始化继而调用你的_tmain或_tWinMain函数。
为了保证C和C++多线程应用程序正常运行,必须创建一个数据结构,并使之与使用了C/C++运行库函数的每个线程关联。然后,在调用C/C++运行库函数时,那些函数必须知道去查找主调线程的数据块,从而避免影响到其它线程
编写C/C++应用程序,一定不要调用操作系统的CreateThread函数,相反,应该调用C/C++运行库函数_beginthreadex:
unsignedlong _beginthreadex(void*security,unsigned stack_size,unsigned( __stdcall *start_address )(void*),void*arglist,unsigned initflag,unsigned*thrdaddr );
_CRTIMP uintptr_t __cdecl _beginthreadex (
void*security,
unsigned stacksize,
unsigned(__stdcall * initialcode)(void*),
void* argument,
unsigned createflag,
unsigned*thrdaddr
)
{
_ptiddata ptd;/* pointer to per-thread data */
uintptr_t thdl;/* thread handle */
unsignedlong err =0L;/* Return from GetLastError() */
unsigned dummyid;/* dummy returned thread ID */
/* validation section */
_VALIDATE_RETURN(initialcode != NULL, EINVAL,0);
/*
* 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,
(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);
}
HANDLE GetCurrentProcess();
HANDLE GetCurrentThread();
这两个函数返回主调函数的进程内核对象或线程内核对象的一个伪句柄。它们不会在主调进程的句柄表中新建句柄。即返回的句柄并不在进程句柄表中有实际的表项,也不会影响进程内核对象或线程内核对象的使用计数。如果使用用CloseHandle函数,并传入一个“伪句柄”,CloseHandle只是简单的忽略此调用。伪句柄指向的是当前线程的句柄,传递给子线程时会改变
将伪句柄转换为真正的句柄,DuplicateHandle函数可以执行这个转换,如在进程句柄表中创建线程内核句柄:
DuplicateHandle(
GetCurrentProcess(),// Handle of process that thread
// pseudohandle is relative to
GetCurrentThread(),// Parent thread's pseudohandle
GetCurrentProcess(),// Handle of process that the new, real,
// thread handle is relative to
&hThreadParent,// Will receive the new, real, handle
// identifying the parent thread
0,// Ignored due to DUPLICATE_SAME_ACCESS
FALSE,// New thread handle is not inheritable
DUPLICATE_SAME_ACCESS);// New thread handle has same </span>
)
BOOL GetThreadTimes(
HANDLE hThread,// handle to thread
LPFILETIME lpCreationTime,// thread creation time
LPFILETIME lpExitTime,// thread exit time
LPFILETIME lpKernelTime,// thread kernel-mode time
LPFILETIME lpUserTime // thread user-mode time
);
_int64 FileTimeToQuadWord(PFILETIME pft)
{
return(Int64shllmod32(pft->dwHighDateTime,32)| pft->dwLowDateTime);
}
voidPerformLongOperation(){
FILETIME ftKernelTimeStart, ftKernelTimeEnd;
FILETIME ftUserTiimeStart, ftUserTimeEnd;
FILETIME ftDummy;
__int64 qwKernelTimeElapsed, qwUserTimeElapsed, qwTotalTimeElapsed;
// Get starting times
GetThreadTimes(GetCurrentThread(),&ftDummy,&ftDummy,&ftKernelTimeStart,&ftUserTimeStart);
// Perform complex algorithm here
...
// Get the ending times
GetThreadTimes(GetCurrentThread(),&ftDummy,&ftDummy,&ftKernelTimeEnd,&ftUserTimeEnd);
// Get the elapsed kernel and user times by converting the start
// and the times from FILETIMEs to quad words, and then subtract
// the start times from the end times.
qwKernelTimeElapsed =FileTimeToQuadWord(&ftKernelTimeEnd)-
FileTimeToQuadWord(&ftKernelTimeStart);
qwUserTimeElapsed =FileTimeToQuadWord(&ftUserTimeEnd)-
FileTimeToQuadWord(&ftUserTimeStart);
// Get total time duration by adding the kernel and user times.
qwTotalTimeElapsed = qwKernelTimeElapsed + qwUserTimeElapsed;
}
BOOL QueryPerformanceFrequency(LARGE_INTEGER* pliFrequency);
BOOL QueryPerformanceCounter(LARGE_INTEGER* pliCount);
函数QueryPerformanceFrequency返回当前硬件平台的高精度性能计数器(High-resolution Performance Counter,HRPC)的频率,注意该值并不是CPU的主频。假如当前硬件平台不支持HRPC,则函数返回0,否则返回非0值。QueryPerformanceCounter返回当前HRPC的值,若当前硬件平台不支持HRPC,函数返回0,否则返回非0值。要注意这两个函数假设调用者线程不会被抢占,不过大多数高精度的分析是在小段代码块内完成的,因此这一点不用担心。下面是我使用这些函数包装的一个C++类,可以很方便的用来进行时间性能分析:
classCStopwatch{
public:
CStopwatch(){
QueryPerformanceFrequency(&m_liPerfFreq);
Start();
};
voidStart(){
QueryPerformanceCounter(&m_liPerfStart);
}
__int64 Now()const{// 返回自Start调用以来的毫秒数
LARGE_INTEGER liPerfNow;
QueryPerformanceCounter(&liPerfNow);
return(liPerfNow.QuadPart- liPerfStart.QuadPart)*1000/m_liPerfFreq.QuadPart;
}
__int64 NowInMicro()const{// 返回自Start调用以来的微秒数
LARGE_INTEGER liPerfNow;
QueryPerformanceCounter(&liPerfNow);
return(liPerfNow.QuadPart- liPerfStart.QuadPart)*1000000/m_liPerfFreq.QuadPart;
}
private:
LARGE_INTEGER m_liPerfFreq;// HSPC的频率
LARGE_INTEGER m_liPerfStart;// HSPC的初始值
};
下面是一个使用CStopwatch类的例子:
CStopwatch stopwatch;
// 在此处执行待测试的代码...
// 获得代码执行时间
__int64 qwElapsedTime = stopwatch.Now();
除了测试代码执行时间,还可以使用上面的函数估算当前计算机系统CPU的主频,代码如下:
DWORD GetCpuFrequencyInMHz(){
// change the priority to ensure the thread will have more chances
// to be scheduled when Sleep() ends
int currentPriority =GetThreadPriority(GetCurrentThread());
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);
// Keep track of the elapsed time with the other timer
__int64 elapsedTime =0;
// Create a stopwatch timer which defaults to the current time
__int64 perfCountStart = stopwatch.NowInMicro();
// get the current number of cycles
unsigned __int64 cyclesOnStart =ReadTimeStampCounter();
// wait for about 1 second
Sleep(1000);
// get the number of cycles after about 1 second
unsigned __in64 numberOfCycles =ReadTimeStampCounter()- cyclesOnStart;
// Get how much time has elapsed with greater precision
elpasedTime = stopwatch.NowInMicro()- perfCountStart;
// Restore the thread priority
SetThreadPriority(GetCurrentThread(), currentPriority);
// Compute the frequency in MHz
return(DWORD)(numberOfCycles/elaspedTime);
}
BOOL GetThreadContext(
HANDLE hThread,// handle to thread with context
LPCONTEXT lpContext // context structure
);
Priority Class | 符号定义 |
Real-time | REALTIME_PRIORITY_CLASS |
High | HIGH_PRIORITY_CLASS |
Above normal | ABOVE_NORMAL_PRIORITY_CLASS |
Normal | NORMAL_PRIORITY_CLASS |
Below normal | BELOW_NORMAL_PRIORITY_CLASS |
Idle | IDLE_PRIORITY_CLASS |
创建子进程时,默认是normal等级,一旦进程运行,就可以通过SetPriorityClass来改变自己的优先级
BOOL SetPriorityClass(
HANDLE hProcess,// handle to process
DWORD dwPriorityClass // priority class
);
DWORD GetPriorityClass(
HANDLE hProcess // handle to process
);
BOOL SetThreadPriority(
HANDLE hThread,// handle to the thread
int nPriority // thread priority level
);
intGetThreadPriority(//获取的是线程相对优先级
HANDLE hThread // handle to thread
);
BOOL SetProcessPriorityBoost(
HANDLE hProcess,// handle to process
BOOL DisablePriorityBoost// priority boost state
);
BOOL SetThreadPriorityBoost(
HANDLE hThread,// handle to thread
BOOL DisablePriorityBoost// priority boost state
);
BOOL SetProcessAffinityMask(
HANDLE hProcess,// handle to process
DWORD_PTR dwProcessAffinityMask // process affinity mask
);
第二个参数是一个位掩码,代表线程可以在哪些cpu上运行,共32位,每一位代表一个cpu,0x05代表0和2号cpu。当然还有对应的Get函数还有应用于线程的DWORD_PTR SetThreadAffinityMask(
HANDLE hThread,// handle to thread
DWORD_PTR dwThreadAffinityMask // thread affinity mask
);
线程同步情况:
1.访问共享资源
2.传递信息
8.1原子访问: InterLocked系列函数
原子访问:一个线程在访问某个资源的同时能够保证没有其他线程会在同一时刻访问同一资源
InterLocked系列函数用来处理对某个变量的操作(返回初始值):
LONG InterlockedExchangeAdd(
LPLONG volatileAddend,// addend
LONG Value// increment value
);
LONG InterlockedExchange(
LPLONG volatileTarget,// value to exchange
LONG Value// new value
);
InterLocked函数工作方式取决于CPU:在x86上,函数会在总线上维持一个硬件信号,这个信号会阻止其他cpu访问同一个内存地址。
同时我们必须确保传给这些函数的变量地址是经过对齐的,C运行库中的_aligned_malloc函数可分配一块对齐内存:
void *_aligned_malloc(size_t size,size_t alignment) ;size要分配的字节,alignment对齐的字节数,必须为2的整数幂次方
另 该系列函数执行速度极快,只需占用几个CPU周期,也不需在用户和内核模式间转换(这个转化通常要1000个周期以上)
选转锁:(假定被保护的资源只会被占用一小段时间)
旋转锁的原型:
//线程之间进行互斥
bool bNoThreadUsing =true;
//旋转互斥锁
voidLocker()
{
//以原子操作的方式来进行判断
while(InterlockedExchange((volatilelong*)&bNoThreadUsing, TRUE)== TRUE)
{
Sleep(0);
}
/**
在这里写业务逻辑
**/
InterlockedExchange((volatilelong*)&bNoThreadUsing, TRUE);
}
8.2 多线程下的Cache
多CPU时,当一个CPU修改了高速缓存行中的一个字节时,机器中的其他CPU会收到通知,并使自己的高速缓存行作废。
高速缓存行可能包含32字节(老式CPU),64字节,甚至128字节(取决于CPU),它们始终对齐到32字节边界,64字节边界,或128字节边界。高速缓存行存在的目的是为了提高性能。
要想确定CPU高速缓存行的大小,最简单的方法是调用Win32的GetLogicalProcessorInformation函数,这个函数返回一个SYSTEM_LOGICAL_PROCESSOR_INFORMATION结构数组。可以检查每个结构的Cache字段,该成员是一个CACHE_DESCRIPTOR结构,其中的LineSize字段表示CPU的高速缓存行的大小。
可以使用C/C++编译器的__declspec(align(#))指示符来对字段对齐加以控制。如下实例:
8.3 高级线程同步
在配备单处理器的机器上,不应该使用旋转锁,即使在配备多处理器的机器上,在使用旋转锁的时候也应该谨慎。因为,浪费CPU时间是非常糟糕的事情。因此,需要一种机制,它既能让线程等待共享资源的访问权,又不会浪费CPU时间。
既是事件通知的方法,告诉系统需要等待的事件,然后休眠,然后等待响应时间触发,线程被唤醒。实际情况是,大多数线程在大部分情况下都处于等待状态。当系统检测到所有线程都已经在等待状态中度过了好几分钟,系统的电源管理器将会介入。
由于操作系统内建了对线程同步的支持,因此我们在任何时候都不应该使用如下方法:两个线程共享一个变量,其中一个线程不断地读取变量的值(使用while或旋转锁),
volatile BOOL g_fFinishedCalculation = FALSE;
int WINAPI _tWinMain(..){
CreateThread(...,RecalcFunc,...);
...
//wait for the recalculation to complete
while(!g_fFinishedCalculation);
...
}
DWORD WINAPI RecalcFunc(PVOID pvParam){
//perform the recalculation.
... g_fFinishedCalculation = TRUE;
return(0);
}
8.6 条件变量
3、要以原子方式操作一组对象时使用一个锁。一种常见的情况是多个对象聚在一起会构成一个单独的“逻辑”资源。
4、为了减少死锁,必须在代码中的任何地方以完全相同的顺序来获取资源的锁。在调用LeaveCriticalSection的时候顺序无关紧要。
5、不要长时间占用锁。在只需要读取资源快照的时候,完全可以用个变量先存放。