首页 > 代码库 > C++多线程
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):适合不同进程内多线程访问公共区域或代码段时使用,与临界区相似。
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库所支持。用户界面线程通常有窗口,因此,它具有自己的消息循环。辅助线程没有窗口,因此,它不需要处理消息。辅助线程比较易于编程,而且通常更加有用
每个线程都有自己的专有寄存器(栈指针、程序计数器),但代码区是共享的.
程序计数器:程序计数器是用于存放下一条指令所在单元的地址的地方。
C++多线程