Windows核心编程 线程基础 线程调度、优先级、关联性 用户模式下的线程同步

线程基础

  ​进程有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
);
调用CreateThread函数,进程会创建一个线程内核对象,os用其管理线程,新线程可以访问进程内核对象的所有句柄,进程中的所有内容和同一进程中的其他所有线程的 栈(Mircosoft C++运行库提供的是_beginthreadex函数)
6.4.1 lpThreadAttributes参数
    同其他函数。。。

6.4.2 dwStackSize

设定线程栈大小,可以使用链接器的/STACK:[reserve] [,commit]开关来控制这个值,默认是一个页面的大小。取dwStackSize和开关的较大值。

设置栈上限1.可以防止耗尽物理内存区域 2.可以尽早察觉程序的bug

6.4.3 lpStartAddress和lpParameter

lpStartAddress线程函数地址(回调函数)如下:

  
  
DWORD WINAPI ThreadProc(
LPVOID lpParameter // thread data
);
lpParameter传给线程的参数
6.4.4 dwCreationFlags
Specifies additional flags that control the creation of the thread. If the CREATE_SUSPENDED flag is specified, the thread is created in a suspended state, and will not run until the ResumeThread  function is called. If this value is zero, the thread runs immediately after creation. At this time, no other values are supported.
6.4.5 lpThreadId
必须是一个有效地址,用来存储系统分配给新线程的ID,可以传递NULL。
6.5 终止线程
4种方式:类似于进程
1.线程函数返回(推荐)
2.调用ExitThread(避免使用)
3.TerminateThread(避免使用)
4.进程终止(避免使用)
可以调用GetExitCodeThread来检测线程是否终止运行

一个线程终止时,系统会一次执行以下操作: 

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函数。 

  6.7 C\c++运行库注意事项
        

为了保证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 );
_beginthreadex 源代码:
   
   
_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);
}
对于 _beginthreadex函数:
1.每个线程都有自己专用的 _ptiddata 内存块
2.传给_beginthreadex的线程函数地址保存在_tiddata中
3._beginthreadex内部会调用CreateThread,因为os只知道这种方式
我们应该避免使用ExitThread函数,因为此函数会“杀死”主调线程,而且不允许它从当前执行的函数返回。由于函数没有返回,所以构造的任何C++对象都不会被析构;它还会阻止线程的_tiddata内存块被释放,使应用程序出现内存泄露(直到整个进程终止)。   我们应该尽量用C/C++运行库函数(_beginthreadex,_endthreadex)而尽量避免使用操作系统提供的函数(CreateThread,ExitThread)。 
6.8 了解自己的身份

            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>

线程调度、优先级、关联性

7.1 线程的挂起和恢复
线程内核中有个挂起计数,创建线程时初始化为1,因为初始化需要时间,不能让线程一创建就开始执行,在初始化之后,函数查看是否有CREATE_SUSPENDED标志传入。
DWORD ResumeThread(HANDLE hThread);恢复线程,成功返回挂起计数,失败返回0xFFFFFFFFF.
DWORD SuspendThread(HANDLE hThread);挂起线程,同上。
注意:当挂起线程时,我们必须知道线程在做什么,避免出现因挂起而引起死锁和其他问题
7.2 进程的挂起和恢复
没有进程的挂起,一般是通过遍历所有线程来实现。
有一个特殊的情况,即调试器处理WaitForDebugEvent返回的调试事件时,Windows将冻结被调试进程中的所有线程,直至调试器调用ContinueDebugEvent。
7.3 睡眠
Void Sleep(DWORD dwMilliseconds) 在一段时间内不需要调度。
注意:1.调用Sleep会使线程放弃属于他的剩下的时间片
2. 时间不会很准确,Windows不是实时系统
3.传入INFINITE,永远不在调用,但不如让线程退出
3.传入0,会放弃剩余的时间片,调用下一个线程
7.4 切换到另一个线程
BOOL SwitchToThread() 调用急需CPU的饥饿线程,没有,立即返回。
7.5 在超线程CPU上切换到另一个线程
7.6 线程的执行时间
不能使用clock或time,GetTickCount64等,使用GetThreadTimes函数:
   
   
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
);
获得的时间是相对时间,以100ns为单位,从格林尼治时间1601.1.1子夜开始计算,使用
   
   
_int64 FileTimeToQuadWord(PFILETIME pft)
{
return(Int64shllmod32(pft->dwHighDateTime,32)| pft->dwLowDateTime);
}
或函数 FileTimeToSystemTime。
   
   
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;
 
 
}
上面讨论的方法适合Vista及之前的系统,但在Vista中,查询线程CPU时间的方法有些变化。Vista不再依赖间隔约为10~15毫秒的系统内部定时器,而是使用处理器的时间戳计数器(Time Stamp Counter,TSC),该计数器使用64位的值记录系统启动以来的CPU周期数。在目前广泛使用的GHz级处理器上,这种方法显然要比毫秒值精确的多。
当线程被停止调度时,系统会计算当前TSC和线程开始调度时TSC的差值,并将该值累加到线程消耗的CPU周期数中。QueryThreadCycleTime函数和QueryProcessCycleTime函数返回指定线程消耗的CPU周期数或指定进程中所有线程的消耗的CPU周期数之和。此外,可以使用ReadTimeStampCounter返回自上次重置以来系统的TSC值,ReadTimeStampCounter是定义在WinNT.h中的宏,指向C++编译器提供内置函数__rdtsc。
对于精度要求较高的分析,GetThreadTimes可能无法胜任,为此Windows提供了以下两个高精度性能分析函数: 
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);
 
}
上面代码的含义比较清晰,只是要注意用这种方法得到的只是CPU频率的估计值,因为Sleep函数的调用效果大多数情况下不可能精确到指定的1秒,且上述方法假设CPU不具备自动调频的能力。
7.7 在实际上下文中谈CONTEXT结构
在windows操作系统中,context结构是唯一一个特定于CPU的
保存有cpu的各种寄存器内容,分为几个部分:CONTEXT_CONTROL cpu的控制寄存器 CONTEXT_INTEGER 整数寄存器CONTEXT_FLOATING_POINT 浮点数寄存器 CONTEXT_SEGMENTS 标识cpu的段寄存器 CONTEXT_DEBUG_REGISTERS cpu调试寄存器  CONTEXT_EXTENDED_REGISTERS  cpu的扩展寄存器
使用GetThreadContext获取上下文(一个线程有2个上下文,内核模式和用户模式 。函数只能返回线程的用户模式上下文):
   
   
BOOL GetThreadContext(
HANDLE hThread,// handle to thread with context
LPCONTEXT lpContext // context structure
);
使用前记得挂起线程。还有 SetThreadText函数,改变结构体的内容并返回线程内核对象中。
7.8 线程优先级
windows分为0-31(越大越高),较高优先级的线程总会抢占较低优先级的线程,无论较低优先级的线程是否正在执行。
顺便一说:系统启动时会创建一个优先级为0的页面清零线程,是系统中唯一一个优先级为0的线程,当没有其他线程执行时,该线程将系统中所有闲置页面清零
7.9 进程优先级
抽象优先级,分为了几个优先级类
7.10 优先级编程
使用fdwcreate参数给进程指派优先级。优先级别定义

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
);
 CreateThread函数没有为线程提供设置优先级的方法,我们必须调用以下函数:
   
   
BOOL SetThreadPriority(
HANDLE hThread,// handle to the thread
int nPriority // thread priority level
);
   
   
intGetThreadPriority(//获取的是线程相对优先级
HANDLE hThread // handle to thread
);
相对优先级:系统通过线程的相对优先级加上线程所属进程的优先级来确定线程的优先级。系统也会不时提升线程的优先级以响应某些事件,比如I/O,窗口消息或磁盘读取,注意,系统只提升优先级在1-15(16及以上属于real time)。
可以使用如下函数禁止线程优先级的动态提升,以及对于的Get。。函数回去信息
   
   
BOOL SetProcessPriorityBoost(
HANDLE hProcess,// handle to process
BOOL DisablePriorityBoost// priority boost state
);
   
   
BOOL SetThreadPriorityBoost(
HANDLE hThread,// handle to thread
BOOL DisablePriorityBoost// priority boost state
);
用户需要使用的某个进程的窗口成为 前台进程,系统会为前台进程进行微调,分配更多的时间片。
7.11 关联性
软关联性:在分配处理器时,如果其他因素都一样,系统将使线程在上一次运行的处理器上运行,这样有利于重用仍在处理器告诉缓存中的数据
硬关联性:我们可以自己控制cpu运行哪些线程
系统在启动时将确定cpu的个数,使用GetystemInfo查询cpu是数量。如果要限制某些线程只在可用CPU的一个子集上运行,则可以调用SetProcessAffinityMask:
   
   
     
     
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);
}
由于主线程没有使用睡眠,而是while,CPU会不断的给它时间,其他线程可能不会得到执行时间
8.4 关键段 (见MFC 线程同步笔记)
          关键段(critical section)是一小段代码,它在执行之前需要独占对一些共享资源的访问权。这种方式可以让多行代码以“原子方式”来对资源进行操控。这里的原子方式,指的是代码知道除了当前线程之外,没有其他任何线程会同时访问该资源。系统仍然可以暂停当前线程去调度其他线程(可被中断),但是,在当前线程离开关键段之前,系统是不会去调度任何想要访问同意资源的其它线程的。
          CRITICAL_SECTION结构,对一种资源进行保护。在访问该资源的时候,必须调用EnterCriticalSection,并传入该资源的CRITICAL_SECTION保护结构地址。用来检测资源的占用标志的函数,如果发现无人使用资源,则继续访问。否则,则会等待。访问资源后,应该调用LeaveCriticalSection
          关键段的最大好处在于它们非常容易使用,而且它们在内部也使用了Interlocked函数,因此执行速度非常快。关键段的最大缺点在于它们无法用在多个进程之间对线程进行同步。
          CRITICAL_SECTION在WinBase.h被定义为RTL_CRITICAL_SECTION,该结构又在WinNT.h中被定义
         要初始化 InitializeCriticalSection(设置CRITICAL_SECTION结构的一些变量)和DeleteCriticalSection函数。不初始化会导致结果不可预料
          TryEnterCriticalSection从来不会让调用线程进入等待状态,它会通过返回值来表示调用线程是否获准访问资源。如果TryEnterCriticalSection返回TRUE,那么CRITICAL_SECTION的成员变量已经更新过了,以表示该线程正在访问资源。所以每个返回值为TRUE的TryEnterCriticalSection调用必须有一个对应的LeaveCriticalSection
 8.4.2 关键段和旋转锁
          当线程试图进入一个关键段,但这个关键段正被另一个线程占用的时候,函数会立即把调用线程切换到等待状态。这意味着线程必须从用户模式切换到内核模式(大约1000个CPU周期),这个切换的开销非常大。事实上,在需要等待的线程完全切换到内核模式之前,占用资源的线程可能就已经释放了资源。为了提高关键段的性能,MS把旋转锁合并得到了关键段中。因此,当调用EnterCriticalSection的时候,它会用一个旋转锁不断地循环,尝试在一段时间内获得对资源的访问权。只有当尝试失败的时候,线程才会切换到内核模式并进入等待状态。 在使用关键段的同时使用旋转锁,需要调用下面的函数来初始化关键段,InitializeCriticalSectionAndSpinCount。第一个参数为关键段结构,第二个参数为希望旋转锁循环的次数。可以从0到0x00ffffff之间的任何一个值。在单处理器上调用这个函数,函数会忽略则合格参数,因此次数总是为0。(一般大约为4000)
8.4.3 关键段和错误处理
          有一种情况下InitializeCriticalSection函数会失败,不过可能性很小。失败的原因是它会分配一块内存,原因是在内存不足时,会发生争夺现象,此时关键段会在内部创建一个事件内核对象,这样系统就可以提供一些内部调试信息。如果内存分配失败,那么函数会抛出STATUS_NO_MEMORY异常。
8.5 Slim读\写锁
          SRWLock的目的和关键段相同:对一个资源进行保护,不让其他线程访问它。但是与关键段不同的是,SRWLock运行我们区分那些想要读取资源的值的线程(读者线程)和想要更新资源的值的线程(写者线程)。只有当写者进程想要对资源进行访问更新的时候才需要进行同步。这就是SRWLock提供的全部功能。  
          首先需要分配一个SRWLOCK结构并用InitializeSRWLock函数对它进程初始化。(SRWLOCK结构在WinBase.h中被定义为RTL_SRWLOCK,后者在WinNT.h中定义)。一旦SRWLock的初始化完成之后,写者线程可以调用AcquireSRWLockExclusive,以尝试获得对被保护的资源的独占访问权。完成对资源的更新之后,应该调用ReleaseSRWLockExclusive解除对资源的锁定。对读者线程来说,同样有两个步骤,AcquireSRWLockShared和ReleaseSRWLockShared。仅此而已,不存在用来删除或销毁SRWLOCK的函数,系统会自动执行清理工作。
          与关键段相比,有两点不同:不存在Try之类的函数,如果锁已经被占用,那么调用会阻塞调用线程;不能递归地获得SRWLock。就是不能为了多次写入资源而多次锁定资源,然后再多次调用Release释放锁定。

 8.6 条件变量
        条件变量是线程中的东西,就是等待某一条件的发生,和信号一样。
条件变量使我们可以睡眠等待某种条件出现。
条件变量是利用线程间共享的 全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个 互斥锁结合在一起。
1、有时候我们想让线程以原子方式把锁释放并将自己阻塞,直到某一个条件成立为止。要实现这样的线程同步比较复杂。Windows通过SleepConditonVariableCS或SleepConditionVariableSRWLock函数,提供了一种条件变量。
2、条件变量结构CONDITION_VARIBALE结构。
3、要以原子方式操作一组对象时使用一个锁。一种常见的情况是多个对象聚在一起会构成一个单独的“逻辑”资源。
4、为了减少死锁,必须在代码中的任何地方以完全相同的顺序来获取资源的锁。在调用LeaveCriticalSection的时候顺序无关紧要。
5、不要长时间占用锁。在只需要读取资源快照的时候,完全可以用个变量先存放。
 






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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值