线程

一、概念:

 1.1、其实我们看到的main函数也是一个线程函数(因为我们线程在启动的时候,必须告诉它当前线程要来使用哪个函数来运行,
 此时线程运行的时候就会来找到我们的当前函数,找到当前函数的时候就会挨个的依次执行)。

 1.2、每个线程都有自己的推栈,这些推栈都是属于进程的。而在进程当中我们对那块内存都有访问的权限。这赋予了线程之间数据的交互的可能。

 1.3、CreateThread() 函数会创建一个线程内核对象。

 1.4、

HANDLE WINAPI CreateThread(
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,    //安全扫描符,一般被设置成null
_In_ SIZE_T dwStackSize,    //线程推栈所使用的空间的大小。0表示默认1MB,一般设默认值
_In_ LPTHREAD_START_ROUTINE lpStartAddress,    //专递当前线程函数的入口。开始的地址。函数地址
_In_opt_ LPVOID lpParameter,
_In_ DWORD dwCreationFlags, 
_Out_opt_ LPDWORD lpThreadId
);

  1.4.1、我们的主线程默认的栈大小是1MB。

  1.4.2、如果我们设置的栈大小超出了默认大小,此时会抛出栈溢出异常。这个异常会被当前程序所捕获,捕获之后会再次分配更多的大小。

 1.5、内核对象都是同级别的。

 1.6、主线程在结束的时候会强行关闭所有的子线程,这样会导致资源无法被正确的回收。

 1.7、所以我们应该能够确保,在主线程结束之前或者结束之时,所有的子线程能够正常退出。

 1.8、阻塞的意义在于要让阻塞的进程或者线程先执行完毕,才会进行下面代码的执行。

 1.9、主线程使用WaitForSigleOhject,WaitForMultipleObject。来等待主线程完成,这样会更安全的执行我们的程序,因为这样可以保证子线程全部退出之后主线程才退出。

 1.10、注意线程之间参数的专递问题,因为线程先后退出是不定的,所以在线程之间参数专递一定要特别注意。

 1.11、线程是以抢占时间片的方式进行运行的,而时间片是我们无法去控制的。

 1.12、操作系统会将休眠的线程中提取时间片来进行其他线程的运行。

 1.13、如果想要控制哪条线程先执行哪条线程后执行,这是几乎不可能的事情。

 1.14、时间片是随机的。

 1.15、线程的退出:

  1.15.1、当一个进程销毁的时候:1、销毁所有的临时对象。2、释放堆栈。3、将返回值设置成退出代码。4、减少内核对象的使用计数。

  1.15.2、当一个线程销毁的时候:1、销毁所有的临时对象,调用析构函数。2、释放线程里面分配的所有堆栈。3、将返回值设置成退出代码,线程入口函数的返回值设置成退出代   码。4、减少线程内核对象的使用计数。

  1.15.3、进程的退出其实上是主线程的退出。进程是惰性的。

  1.15.4、ExitThread()会立即结束当前线程。会将属于当前线程的栈销毁、但是不会去调用析构函数,此时就很容易内存的泄漏。

  1.15.5、TerminateThread()可以结束其他线程。

  1.15.6、在一个合格的程序中,不应该出现以上的两个函数。

  1.15.7、在线程结束的时候,还同时释放窗口对象,和Hook对象。

  1.15.8、启动:线程去进程当中申请一块内存,作为当前线程的栈。

  1.15.9、内核对象:使用计数、ExitCode、Signaled(代表当前这个对象是否是能够接收信号的状态,一般来说当这个对象变得有信号的时候CPU会去运行它,当为没有信号状态的  时候CPU不会去运行当前线程)。

  1.15.10、CONTEXT(线程上下文):包含了当前CPU寄存器的状态。每个线程都有一个自己的CONTEXT。

  1.15.11、IP(指令)、SP(栈寄存器):这两个是线程上下文想要存储的基础。因为这里面存储了当前线程的状态。

  1.15.12、IP(指令):指令寄存器指明下一条要执行的指令。

  1.15.13、SP(栈寄存器):指明了要分配的内存空间在哪里。然后会去分配这个空间作为线程的栈。分配完成之后会马上来使用这个栈,会传递两个参数:

  1.15.14、线程启动时的lpParam、传递给线程的函数的开始StartAddress(当前线程的入口函数)。

  1.15.15、线程上下文:当我们进行线程切换的时候,必须把当前CPU的状态进行保存,当切换回来的时候需要将CPU的状态复原。

  1.15.16、所有这些做完了之后,会去进程当中申请一块空间。作为当前线程的栈。

  1.15.17、然后会将lpParam和LPStartAddress放在这个栈的最前面。

 1.16、CreateThread,之后操作系统所做的事情:

  1.16.1、首先创建一个线程内核对象、实用计数为2、暂停计数为1、退出代码为STILL_ACTIVE、Signaled为FALSE、CONTEXT为空。

  1.16.2、然后是分配好栈,然后将lpParam、lpfnAddr压入栈中。

  1.16.3、第三步:CONTEXT(线程上下文结构体)存的是线程上一次运行时的寄存器状态。

   1.16.3.1、IP(指令寄存器):指示下一条代码在哪里运行。(此时指向了void RtlUserThreadStart(未公开的函数:意思是我们并不能直接调用的)(lpParam,lpFnAddr))

   1.16.3.2、SP(栈寄存器):指向栈顶(此时也就是指向了lpfnAddr回调函数)。

   1.16.3.3、以上两个寄存器是非常之重要的两个寄存器。这两个寄存器指示了当前栈顶在哪里、指示了下一条要执行的指令在哪里。

  1.16.4、当CONTEXT被填充完成,进入到第四步:操作系统会来检查CreateThread的标志位,这个标志位是否被设置为CREATE_SUSPENDED,如果没有则:将暂停计数减一。

  然后将以上设置好的所有东西交给CPU去执行。CPU执行时会将CONTEXT加载到CPU中(在线程切换的时候完全是靠CONTEXT来做的),从此来进行线程中的一系列工作。

  1.16.5、交给CPU调度。

  1.16.6、最后:

  1.16.7、进入RtlUserThreadStart

   1.16.7.1、第一件事情:设置一个结构化异常(SEH),能够让操作系统进行当前系统的一些异常的处理。

   1.16.7.2、然后:调用线程函数,并将lpParam参数专递进去。

   1.16.7.3、然后等待线程函数的返回。

   1.16.7.4、当线程函数返回的之后,会在内部调用ExitThread,并且将线程函数的返回给ExitThread。此时使用计数递减。

 1.17、线程上下文切换:当切换线程的时候其实就是在每一个不同的CONTEXT中进行切换。每一次切换进来之后,第一件事情:将CONTEXT里面的所有的寄存器状态加载到当前  的物理寄存器当中。然后物理寄存器会按照IP指令寄存器来进行代码的执行。

 1.18、_beginthreadex():

  1.18.1、errno是非线程安全的。

  1.18.2、该函数:首先分配空间、之后会调用CreateThread()函数。

  1.18.3、_endthreadex(),成对出现,因为_endthread()会去是否_beginthreadex()多分配处理的那段空间。

 1.19、线程的状态:

  1.19.1、启动:CONTEXT、使用计数 = 2、暂停计数 = 1(整个CreateThread完成的时候会-1此时就 = 0,此时可以进入CPU的调度,当前线程进入可执行的状态)。

  1.19.2、运行:CPU调度、执行我们的函数、时不时的切换我们的线程(CPU信息写入CONTEXT,此时暂停计数是不会被计1的)、读取CONTEXT。

  1.19.3、挂起:暂停线程的运行(此时会将暂停计数 +1)、调用SuspendThread()函数线程会被挂起。ResumeThread()函数会是暂定计数-1。

  一个良好的设计中我们不应该进行线程的挂起(因为在线程挂起的时候,我们无法确保安全)。

  CPU调度挂起和调用SuspendThread函数进程挂起(将线程从CPU调度池中取出)是两个不同的概念。

  1.19.4、等待(伪休眠)、休眠:调用Sleep();会做一些事情:1、放弃剩余的时间片。2、等待Sleep();完成。

  Sleep(0)已经切换,Sleep(INFINITE)永远等待直到进程消亡。

  SwitchThread();会帮我们调度另一个线程。

  1.19.5、消亡

 1.20、_CONTEXT结构体:当前线程时间片结束的时候,会把当前线程的寄存器保存到_CONTEXT结构体中,然后当我线程再次获得时间片运行的时候又会把当前的线程的  _CONTEXT加载到寄存器中。总的来说就是:保存现场,现场还原。

  1.20.1、这个结构体是非常之重要的:计算机是通过寄存器来运行计算的,而_CONTEXT结构体控制了寄存器的值。

  1.20.2、切换线程的话就会加载自己的CONTEXT。所以在多线程中使用全局变量就会造成各种麻烦。原子操作可以解决这个问题。

 1.21、原子操作(锁的API):同一资源在同一时间只有一个线程能够访问(系统级的操作)。

  1.21.1、上锁后的代码一定是线程安全的。

 1.22、在声明一个变量的时候加上volatile,告诉编译器不要对我的这个变量进行优化。

 1.23、线程不安全:当某一条线程去访问某一个资源的时候,因为前一个线程的修改导致当前线程访问出错了。

 1.24、线程的优先级:

  1.24.1、可调度区会被分配时间片。

   1.24.2、在可调度的状态下面,操作系统会选择优先级较高的线程进行执行。

   1.24.3、按照这个说法:如果有优先级较高的线程,优先级低的线程就永远的不会被执行。

    但是事实并非如此。

  1.24.4、优先级高的线程会去抢优先级低的线程的时间片。

  1.24.5、0优先级的线程:在操作系统中有且只有一个,在操作系统被启动的时候就被启动了,为页面清零线程,它负责在系统空闲时清理所有闲置的内存,将所有的闲置的内存进  行清零操作。

  1.24.6、0优先级是只能有一个的,所以我们是不能设置0优先级的线程的。

 1.25、进程优先级:

  1.25.1、因为系统设计的需要,Windows需要不停的切换进程或者选择某一进程进行运行,所以Windows对进程进行了优先级的设置。

  1.25.2、存在有:

   1.25.2.1、real_time(实时):这是管理员才能进行设置的权限。

   不应该有任何时候的进程运行在实时优先级下,实时优先级可能会影响到操作系统的任务,可能会导致磁盘、网络通信、键盘、鼠标等的使用。

   1.25.2.2、high(立即):我们在设置权限的时候也不应该高于此权限。设置到此权限的话就很危险了。

   1.25.2.3、above normal(较高):如果你希望你的进程能抢到更多的资源的话最多你设置到此权限下面。

   1.25.2.4、normal(正常)、

   1.25.2.5、below normal(较低)、

   1.25.2.6、idle(低)这些优先级。

   1.25.2.7、17 - 21 优先级是留给操作系统使用的。

   1.25.2.8、默认启动的进程都为normal优先级。

   1.25.2.9、Windows资源管理器的进程为high,是为了实时关闭进程。

   1.25.2.10、CreateProcess函数可以设置优先级,其实他是通过SetPriontyClass来改变自己的优先级,而GetPriorityClass可以获取当前进程的优先级。

   1.25.2.11、SetThreadPriority可以改变线程的优先级、GetThreadPriority可以获取线程优先级。

   1.25.2.12、特别值得注意的是CreateThread函数无法设置现在优先级,线程默认是以normal优先级运行的,

   1.25.2.13、当某一条饥饿线程,被饥饿两秒之后,操作系统会将此线程进行权限的提升。

   (操作系统会根据系统需要动态提升一些线程的优先级,默认每次会给优先级提升两级,在操作完一个时间片后递减1,直到恢复原优先级。)

   1.25.2.14、

SetProcessAffinityMask(GetCurrentProcess(), 0x1);//该函数可以设置使用核心数(CPU)。

  1.25.3、线程的优先级其实是基于进程的优先级而做出来的。

  1.25.4、我们为什么需要进程的优先级:

   1.25.4.1、来帮我们决定线程的优先级是多少。

   1.25.4.2、能够帮我们更加理性的来看待优先级的这个事情。

 1.26、临界区及线程函数中使用静态变量:

  1.26.1、CRITICAL_SECTION gCs;结构体:临界区 -> 关键段。

  1.26.2、InitializeCriticalSection(&gCs); 这个函数会分配一些内存(是我们无法看到的)。

  1.26.3、InitializeCriticalSectionAndSpinCount(); 以旋转锁的方式使用临界区。

  1.26.3、DeleteCriticalCalSection(&gCs); 在关闭的时候不仅仅会把gCs(临界区数据)清零,还会将分配好的内存进行清理。

  1.26.4、EnterCriticalSection(&gCs); 进入临界区:有两种效果:

    1.26.4.1、获准访问:在没有其他线程访问的时候,通过临界区结构对象得到获准,然后访问这个函数之后的资源。

    在得到获准之后临界区结构体对象中的数据将会被更新,

    1.26.4.2、等待状态:等待状态是有一个周期的(从调度池进入了不可调度状态)

  1.26.5、TryEnterCriticalSection(&gCs); 判断是否能够进入临界区。

 1.27、_Slim锁及线程休眠及等待及挂机及阻塞:

  1.27.1、SRWLOCK gSRW; //Silm锁。

  1.27.2、InitializeSRWLock(&gSRW); //只有初始化没有DELETE。也没有对应的函数。

  1.27.3、AcqireSRWLockExclusive(&gSRW);//以独占的方式进入锁。

  1.27.4、AcquireSRWLockShared(&gSRW);//以共享方式进入锁。

posted @ 2017-07-03 03:44  _xiaohaige  阅读(209)  评论(0编辑  收藏  举报