C++多线程
1 为什么使用多线程
耗时的操作使用线程,提高应用程序响应(对图形界面的程序尤为重要,多线程保证界面不卡,仍然可以响应键鼠)
并行操作使用线程,比如服务器响应客户的请求。
多CPU或者多核系统中,多线程提高CPU利用率(OS保证线程数不大于CPU数目时,不同的线程在不同的CPU上)
改善程序结构。
2 线程的优点
与进程相比,它是一种花销小,切换快,更节俭的多任务的操作方式。启动一个新的进程必须分配独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,是一种昂贵的多任务工作方式;而如果在一个进程中用多线程,彼此之间使用相同的地址空间,共享数据,线程切换的代价很小。
另外是线程之间通信机制。不同的进程有独立的数据空间,要进行数据传递只能通过通信的方式,费时又不方便。由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其他线程所用,方便。当然,数据在线程之间共享也带来一些问题,所以需要同步。
3 理解线程
进程:每个进程由私有的虚拟地址空间、代码、数据和其他系统资源组成。进程在运行时创建的资源随着进程的终止而死亡。
因为进程有独立的地址空间,一个进程崩溃后,在保护模式下,不会对其他进程产生影响,多进程程序比多线程健壮。
线程:独立的执行流。缺省的运行包含一个主线恒,主线程以函数地址的形式,如main()或WinMain函数,提供程序的启动点,主线程终止时,进程也随之终止。
一个进程中的所有线程都在该进程的虚拟空间中,使用该进程的全局变量和系统资源。CPU时间片轮转的方式调度,优先级高的先运行。
4 线程分类
用户界面线程:通常用来处理用户输入并响应各种事件和消息,其实,应用程序的主执行线程CWinAPP对象就是一个用户界面线程,当应用程序启动时自动创建和启动,同样它的终止也意味着程序结束,进程终止。
工作线程(后台线程):执行程序的后台处理任务,比如计算、调度、对串口的读写操作等,和用户界面的区别是,不用从CWinThread类派生来创建。工作线程和用户界面线程启动时要调用同一个函数的不同版本。
5 线程
5.1 线程组成:线程由线程ID、当前指令指针(PC),寄存器集合和堆栈组成。
5.2 线程状态:就绪(等待处理机),阻塞(等待事件),运行。
6 线程同步互斥的4种方式
线程之间同步分两大类:用户模式和内核模式。内核模式就是利用内核对象的单一性来进行同步,使用的时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。
用户模式: 临界区;
内核模式: 事件, 信号量,互斥量。
6.1 临界区(Cirtical Section) :适合一个进程内多个线程访问公共区域或代码段时使用。
InitializeCriticalSection , DeleteCriticalSection, EnterCriticalSection, LeaveCriticalSection
优点:效率高,不是内核对象,涉及到内核态和用户态的切换。
缺点:不是内核对象,无法获知进入临界区的线程是生还是死,如果进入临界区的线程挂了,没有释放资源,系统无法获知,而且没办法释放。
注意:EnterCriticalSection,一个线程可以多次进入关键区域
先找到关键段CRITICAL_SECTION的定义吧,它在WinBase.h中被定义成RTL_CRITICAL_SECTION。而RTL_CRITICAL_SECTION在WinNT.h中声明,它其实是个结构体:
typedefstruct _RTL_CRITICAL_SECTION {
PRTL_CRITICAL_SECTION_DEBUG DebugInfo; // 调试相关的
LONG LockCount; // n表示有n个线程在等待
LONGRecursionCount; //拥有该关键段的线程对此资源获得的关键段次数
HANDLEOwningThread; // 拥有该关键段的线程句柄,from the thread's ClientId->UniqueThread
HANDLE LockSemaphore; // 一个自复位事件
DWORD SpinCount; // 旋转锁的设置,单CPU下忽略
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;
由这个结构可以知道关键段会记录拥有该关键段的线程句柄即关键段是有“线程所有权”概念的。第四个参数OwningThread记录获准进入关键区域的线程句柄,如果这个线程再次进入,EnterCiritcalSection()会更新第三个参数RecursionCount,来记录该线程进入的次数并立即返回让该线程进入。一旦拥有线程所有权的线程调用LeaveCriticalSection()使其进入的次数为0时,系统会自动更新关键段并将等待中的线程换回可调度状态。
注意:拥有线程所有权的线程可以重复进入关键代码区域。
另外,由于将线程切换到等待状态的开销较大,因此为了提高关键段的性能,Microsoft将旋转锁合并到关键段中,这样EnterCriticalSection()会先用一个旋转锁不断循环,尝试一段时间才会将线程切换到等待状态。《Windows核心编程》第五版的第八章推荐在使用关键段的时候同时使用旋转锁,这样有助于提高性能。值得注意的是如果主机只有一个处理器,那么设置旋转锁是无效的。无法进入关键区域的线程总会被系统将其切换到等待状态。
下面是配合了旋转锁的关键段初始化函数
函数功能:初始化关键段并设置旋转次数
函数原型:
BOOLInitializeCriticalSectionAndSpinCount(
LPCRITICAL_SECTIONlpCriticalSection,
DWORDdwSpinCount);
函数说明:旋转次数一般设置为4000。
函数功能:修改关键段的旋转次数
函数原型:
DWORDSetCriticalSectionSpinCount(
LPCRITICAL_SECTIONlpCriticalSection,
DWORDdwSpinCount);
6.2 互斥量(Mutex):适合不同进程内多线程访问公共区域或代码段时使用,与临界区相似。
内核态,可以跨进程同步,虽然比临界区低效率,但是不会像临界区那样死等,waitforsingleobject可以设定TIMEOUT时间。
如果一个拥有Mutex的线程在返回之前没有调用ReleaseMutex(),那么这个mutex就被舍弃了,但是当其他线程等待这个Mutex
时,仍能返回,并得到一个WAIT_ABANDONED_0返回值。 能够找到Mutex被舍弃是MUTEXTC特有的。
CreateMutex , CloseHandle, WaitForSingleObject, ReleaseMutex.
互斥量也是一个内核对象,它用来确保一个线程独占一个资源的访问。互斥量与关键段的行为非常相似,并且互斥量可以用于不同进程中的线程互斥访问资源。使用互斥量Mutex主要将用到四个函数。
HANDLECreateMutex(
LPSECURITY_ATTRIBUTESlpMutexAttributes, //表示安全控制,一般直接传入NULL
BOOLbInitialOwner, // 是否让创建者(此例中是主线程)拥有该互斥对象,和临界区一样,也有所有权的问题
// 如果是ture, 表示当前创建线程拥有这个mutex;
// 如果是false,表示第一个调用waitforsngleobject的线程会拥有这个mutex.
LPCTSTRlpName,
);
注意:Mutex也有线程所有权的问题,拥有这个Mutex的线程调用waitforsngleobject,会得到这个mutex,不会阻塞。
6.3 事件(Event):通过线程间触发事件实现同步互斥。
CreateEvent, CloseHandle, SetEvent, ResetEvent
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, // 表示安全控制,一般直接传入NULL。
BOOLb ManualReset, // 手动还是自动复位。
BOOL bInitialState, // 初始状态是否是已经触发的。
LPCTSTR lpName // 事件的名称,NULL表示匿名事件。
);
6.4 信号量(Semaphore):与临界区和互斥量不同,可以实现多个线程同时访问公共区域数据,原理与OS中的PV操作类似,先设置一个访问公共区域的线程最大连接数,每有一个线程访问共享数就减一,直到资源小与等于0.
CreateSemaphore, CLoseHandle, WaitForSingleObject, ReleaseSemaphore
注意:Semaphore没有所有权的观念,一个线程可以反复调用wait...()函数以产生新的锁定。这和mutex绝不相同:拥有mutex的线程不论再调用多少次wait...()函数,也不会被阻塞住。
6 线程解析
Windows提供了两种线程,辅助线程和用户界面线程。两种线程均为MFC库所支持。用户界面线程通常有窗口,因此,它具有自己的消息循环。辅助线程没有窗口,因此,它不需要处理消息。辅助线程比较易于编程,而且通常更加有用
每个线程都有自己的专有寄存器(栈指针、程序计数器),但代码区是共享的.
程序计数器:程序计数器是用于存放下一条指令所在单元的地址的地方。
7 创建线程的三种方法
CreateThread() : WIndows API
_beginthread():CRT
_beginthreadEx(): 比 _beginthread多几个参数
AfxBeginThread() : MFC
8 退出线程的方法函数
return: 这个是最安全的方法。return后,会析构线程函数内的对象,自动调用_endthreadex()清理_beginthreadex() 申请的资源。
调用_endthreadex()函数或_ExitThread()函数(最好不用,不会调用线程函数内申请的类的对象的析构函数,会导致内存泄漏)
调用TerminateThread(必须避免)
9 线程的挂起,唤醒,和终止
SuspendThread: 挂起
RessumeThread: 唤醒
TerminateThread: 停止
10 进程 VS 线程
A:进程是资源分配的基本单位,线程是CPU调度的的最小单位。
B: 进程有独立的地址空间,建立数据表来维护代码段,堆栈段和数据段。而一个进程中的多个线程共享大部分数据,使用相同的地址空间,线程之间切换
快,当然,线程有自己的局部变量和栈,
C:线程之间通信方便,比如一些共享的数据(全局变量,静态变量),线程之间
要通过同步和互斥。 而进程之间通信通过 进程通信的方式进行。
D:进程比线程健壮,线程死掉,整个进程就死掉;进程对其他进程没有影响。
E: 进程有个PCB表,线程有自己的TCB。但是TCB的信息比PCB要少很多。
11 进程之间通信方式
Socket
COM/DCOM
共享内存
管道:匿名管道
命名管道
邮件槽
信号量
12 线程函数
DWORD WINAPI 函数名(LPVOID lpParam)
13 互斥量与临界区 区别
A:互斥量是内核对象,比临界区耗费资源,但是可以命名,可以被其他进程访问
B::临界区是通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据
访问。
互斥量是为协调共同对一个共享资源的单独访问而设计。
C:临界区是表示一段代码不能同时执行;mutext可以用来同步两段代码。
14 生产者 消费者
两个信号量:一个empty,初始值N(N是缓冲区大小);另外一个是full,初始值哦;
一个Mutex: 生成者之间,消费者之间,生产者和消费者之间都需要互斥。
15:读写锁:同时可以有多个读,但是只能被一个写者拥有。
readcount - 记录读的个数,需要互斥访问。
READ:
Repeat;
P(mutex); // 对readcount互斥访问
readcount:=readcount+1; // 读线程个数加1
if(readcount=1) // 如果是第一个读线程,就请求读/写锁
P(write);
V(mutex); // 对readcount操作完毕
读文件 操作
P(mutex); // 读完,又要操作readcount
readcount:=readcount-1;
if(readcount=0) // 如果是最后一个读线程,也要释放读/写锁
V(write);
V(mutex); // 操作完readcount
Until false
WRITE:
Repeat
P(write);
写文件操作
V(write);
Until false;
上述方法有可可能造成读一直进行,但是写没有机会执行:可以在读的时候,如果发现有写操作在等待,读操作就等待