《Windows环境下的多线程编程原理与应用》的学习笔记

来源:互联网 发布:微信扫号器数据 编辑:程序博客网 时间:2024/06/02 04:58
一、概念
1、程序:是指计算机指令的静态集合,无实质的意义。
2、进程:简单的讲,进程就是正在运行的程序,如VC++ 6.0、画图程序、记事本等,均属于一个进程。抽象的讲,进程是一些所有权的集合,一个进程拥有内存、CPU运行时间等一系列资源,为线程运行提供环境,其拥有自己的地址空间和动态分配的内存,以及文件、线程与其他模块等,它是操作系统的核心概念,其有一下三种状态:
a、运行(正在使用CPU);
b、就绪(当前能够运行,而由于系统正在运行其他进程需要等待);
c、阻塞(由于不能得到所需资源而不能运行,需要等待外部事件的发生);
3、线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
4、三者之间的所属关系:在一个应用程序中,可以包括一个或多个进程,每个进程由一个或多个线程组成。
在Windows系统中,系统的最小执行单位不是进程而是线程,因此,一个进程至少要有一个主线程,多个线程可以并发执行。
说明一点似乎不相关的,线程拥有独立的堆栈,因为函数的参数和局部变量等分配在堆栈中,堆栈中的特点是先进后出,如果几个线程共同拥有一块堆栈的话,堆栈的进出栈就会出现问题。


二、线程的通信与线程同步概述
1、线程间通信:线程间通信包括全局变量法、参数传递法、消息响应法以及用线程同步实现线程间通信。
2、线程同步:通过同步对象来实现线程的同步。
a、临界段:临界段对象通过提供所有线程必须共享的对象来控制线程。只有拥有临界段对象的线程才可以访问保护的资源(进行临界区操作),在另一个线程可以访问该资源之前,前一个线程必须释放临界段对象,以便另一个线程可以获取对象的访问权。用户应用程序可能会使用临界段对象来阻止两个线程同时访问共享的资源如文件等。
b、互斥量:互斥量的工作方式极类似于临界段,只是互斥量不仅保护一个进程内的资源共享,还可以保护进程间的资源共享,通过命名互斥变量来进行进程间资源共享协调的。
c、事件对象用于给线程传递信号,指示线程中特定的操作可以开始或结束。除非线程已经收到了这个事件信号,否则他将一直处于挂起状态,当事件对象进入信号状态时,等待该事件的线程就可以开始执行了。例如:一个应用程序可以通过事件来通知线程他需要的数据已经准备好。
d、信号量:信号量与互斥相似,但是互斥只允许在同一时刻一个线程访问他的数据,而信号量可以在同一时刻多个线程来访问他的数据,Win32不知道哪一个线程拥有信号量。它只保证信号量使用的资源计数被正确的设置。


三、Windows环境中的多线程实现
1、多线程不能同时访问静态变量,需要序列化的访问,如printf即不能同时访问,因为printf需要调用stdout这个静态变量(在程序开始执行时startup代码<在程序执行main()或Winmain()之前系统调用的代码>自动代开三个标准流,输入-stdin;输出-stdout;错误-stderr。三者都是静态的,用户不得改变其值)。而多线程可以同时访问堆栈变量,因为线程有自己的堆栈,堆栈是不共享的。
2、与线程相关的Win32 API函数:
CreateThread()----------------->创建一个新的线程
CreateRemoteThread()----------->在另一个线程中创建一个新的线程
ExitThread()------------------->正常的结束一个线程的执行
TerminateThread()-------------->终止一个线程的执行
GetExitCodeThread()------------>得到一个线程的退出码
Get/SetThreadPriority---------->得到/设置线程的优先级
Suspend/ResumeThread()--------->挂起/重启一个线程
CloseHandle()------------------>关闭一个线程的句柄
3、关于死锁的介绍
死锁:(用两个现成的运行来解释)假设有两个(或更多)资源X和Y,线程A的运行需要同时拥有X和Y,而线程B的运行也需要同时拥有X和Y,现在线程A得到了X资源,而线程B得到了Y资源,线程A因为等待Y资源而不能运行,线程B因为等待X资源而不能运行,这样,两个线程均处于等待状态,造成拥塞,也就是死锁。
死锁在多线程编程中是遇到的最为严重的问题,一是死锁造成的后果极其严重,二是死锁的分析和检测十分困难,而且可能发生死锁的程序又不是每次都会发生,这就进一步加大了分析死锁的困难。
防止死锁的产生,常见的有一下两种方法:一、资源请求时,不许以相同的顺序执行,这样能够大大降低死锁的发生。二、在等待函数中设置超时时间,超过此时间即撤销该操作。


四、关于互斥的介绍
互斥大体上与临界段类似,但是区别于临界段,区别在于互斥可以完成进程之间的同步,而临界段只能完成同一个进程内的线程之间的同步。
1、使用WinApi来实现互斥:首先定义互斥句柄:HANDLE hMutex;  然后创建互斥句柄,创建的时候可以设置互斥变量名,也可以不设置互斥变量名,如果不设置的话,此互斥不能用于不同进程间的同步,因为在不同进程间同步是靠这个互斥变量名来判断互斥的:hMutex = CreateMutex(NULL,FALSE,NULL),此创建过程在进程的主线程中完成。然后在子线程中进行同步的操作,WaitForSignalObject(hMutex,INFINITE);ReleaseMutex(hMutex);最后使用完成之后要关闭句柄:CloseHandle(hMutex);
2、使用MFC类中的CMutex来完成互斥同步,CMutex其实是对Api函数进行了封装,原理是一样的。首先要包含头文件:include "afxmt.h";然后定义全局的Mutex:CMutex hMutex;然后在子进程中通过上锁解锁即可使用:hMutex.clock();hMutex.unclock();
3、原子操作:即为不可分割的操作,不能被其他的线程中断,因此需要使用同步机制来保证,可以操作开始前进行上锁,完成之后进行解锁。
4、快照:即为几个线程共同操作数据缓冲区,A线程先拿到数据后,即释放同步锁,使用很短的时间来占用锁,之后B线程即可去访问数据,当A线程需要将缓冲区数据覆盖时,可以再继续上锁来完成,此操作即为快照操作。


五、关于临界段的介绍
临界段的使用情况与互斥的使用是非常相似的,也分为WinApi函数的使用和MFC封装过的函数的使用,MFC的封装无非是对API函数进行封装,所以干脆就直接使用API函数即可,因为临界段只适用于单个进程,所以如果只涉及到单进程内部的线程通信,使用临界段要稍好一点,因为这个不是内核对象,速度要优于互斥使用的内核对象。
使用:1、创建临界段对象:CRITICAL_SECTION cs;  2、初始化临界段对象:InitializeCriticalSection(&cs);  3、进行同步的使用:EnterCriticalSection(&cs);LeaveCriticalSection(&cs);


六、关于事件的介绍
事件类似于互斥,相似之处在于都是操作内核对象,都能够进行进程之间的同步,在进程间同步时也是依靠事件的名称来索引事件的,但是事件又超强于互斥,因为使用事件进行同步,线程的执行顺序可以控制,考的就是信号状态,分为两种方式:自动事件和手动事件。通过使用信号可以决定哪个线程先执行,哪个线程后执行,这方面在对缓冲区的读写方面有很大的优势。而互斥不能决定线程执行的先后顺序,只能靠操作系统来维持。
使用:这里只简单介绍几个Win32API函数,在MFC中也有封装好的函数,那样用起来或许会方便一些,但是他们也是对Win32API函数的封装,所以还不如直接使用API函数,这样既能够提高速度,也便于记忆事件相关的操作函数(至少我这样认为)。
Windows系统中与事件有关的函数
CreateEvent() 创建一个事件
OpenEvent() 打开一个已经创建的事件
SetEvent() 触发一个事件
ResetEvent() 复位一个事件
PulseEvent() 触发并重置一个事件
WaitForSignalObject()等待某个事件
WaitForMultipleObject()等待多个事件
只是进行了简单的说明,在使用的时候再查相关的文档,争取把这几个函数的使用全部掌握。


七、关于信号量的介绍
线程之间同步的原因是由于竞争和协作。而互斥可以解决竞争的问题,而事件可以解决协作的问题(当然也能解决竞争问题),信号量就可以称为二者的组合体,既能够解决竞争也能够解决协作。信号量允许一个以上的资源同时访问共享资源,这是信号量与其他同步对象最大的不同。在信号量中有一个内置的计数值,用于对资源进行计数,同时它通过内置的互斥机制保证在有多个线程试图对计数值进行修改时,在任意时刻只有一个线程对计数值进行修改。
信号量的两个核心操作是提高计数值(up)和降低计数值(down),down操作是检查信号量的计数值是否大于0,如果信号量的计数值大于0则信号量的计数值减1(用掉一个信号量计数),线程继续运行,如果计数值为0,线程由于等待进入睡眠状态。每当一个等待函数(WaitForSignalObject())释放一个正在等待该信号量的一个线程时,信号量的计数值就减1,每当一个线程调用函数ReleaseSemaphone()时,信号量对象计数值就增加一个特定的值。
API函数介绍:CreateSemaphone(),创建一个信号量;OpenSemaphone(),打开一个已经创建的信号量;ReleaseSemaphone(),释放对信号量的所有权;对于函数的具体参数和实现需查看相关文档。


八、对同步的总结

以上只是简单的区分了四种同步方式分别适用的环境,在什么情况下使用哪种同步方式最好,在实际中需要使用哪种同步时,在仔细研究该模块,要透彻,不要模棱两可。

注:如果有需要此书的朋友,可在我的资源中下载,希望大家能够共同学习!

原创粉丝点击