MFC——9.多线程与线程同步
Posted langtaol
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MFC——9.多线程与线程同步相关的知识,希望对你有一定的参考价值。
Lesson9:多线程与线程同步
程序、进程和线程是操作系统的重点,在计算机编程中,多线程技术是提高程序性能的重要手段。本文主要讲解操作系统中程序、进程和线程之间的关系,并通过互斥对象和事件对象实例说明多线程和线程同步技术。
1. 程序、进程和线程
1.1 程序和进程
程序是计算机指令的集合,它以文件的形式存储在磁盘上。进程通常被定义为一个正在运行的程序的实例,是一个程序在其自身的地址空间中的一次执行活动。进程是资源申请、调度和独立运行的单位,因此,它使用系统中的运行资源;而程序不能申请系统资源,不能被系统调度,也不能作为独立运行的单位,因此,它不占用系统的运行资源。
进程由两个部分组成:
1、操作系统用来管理进程的内核对象。内核对象也是系统用来存放关于进程的统计信息的地方。
2、地址空间。它包含所有可执行模块或DLL模块的代码和数据。它还包含动态内存分配的空间。如线程堆栈和堆分配空间。
每个进程有它自己的私有地址空间。进程A可能有一个存放在它的地址空间中的数据结构,地址是0x12345678,而进程B则有一个完全不同的数据结构存放在它的地址空间中,地址是0x12345678。当进程A中运行的线程访问地址为0x12345678的内存时,这些线程访问的是进程A的数据结构。当进程B中运行的线程访问地址为0x12345678的内存时,这些线程访问的是进程B的数据结构。进程A中运行的线程不能访问进程B的地址空间中的数据结构,反之亦然。一个进程不能读取、写入、或者以任何方式访问驻留在该分区中的另一个进程的数据。对于所有应用程序来说,该分区是维护进程的大部分数据的地方。
1.2 进程和线程
进程是不活泼的。进程从来不执行任何东西,它只是线程的容器。若要使进程完成某项操作,它必须拥有一个在它的环境中运行的线程,此线程负责执行包含在进程的地址空间中的代码。单个进程可能包含若干个线程,这些线程都“同时” 执行进程地址空间中的代码。每个进程至少拥有一个线程,来执行进程的地址空间中的代码。当创建一个进程时,操作系统会自动创建这个进程的第一个线程,称为主线程。此后,该线程可以创建其他的线程。操作系统为每一个运行线程安排一定的CPU时间——时间片。系统通过一种循环的方式为线程提供时间片,线程在自己的时间内运行,因时间片相当短,因此,给用户的感觉,就好像线程是同时运行的一样。如果计算机拥有多个CPU,线程就能真正意义上同时运行了。
线程由两个部分组成:
1、线程的内核对象,操作系统用它来对线程实施管理。内核对象也是系统用来存放线程统计信息的地方。当创建线程时,系统创建一个线程内核对象。该线程内核对象不是线程本身,而是操作系统用来管理线程的较小的数据结构。可以将线程内核对象视为由关于线程的统计信息组成的一个小型数据结构。
2、 线程堆栈,它用于维护线程在执行代码时需要的所有参数和局部变量。
线程总是在某个进程环境中创建。系统从进程的地址空间中分配内存,供线程的堆栈使用。新线程运行的进程环境与创建线程的环境相同。因此,新线程可以访问进程的内核对象的所有句柄、进程中的所有内存和在这个相同的进程中的所有其他线程的堆栈。这使得单个进程中的多个线程确实能够非常容易地互相通信。线程只有一个内核对象和一个堆栈,保留的记录很少,因此所需要的内存也很少。因为线程需要的开销比进程少,因此在编程中经常采用多线程来解决编程问题,而尽量避免创建新的进程。
2. 多线程
主线程可以创建多个子线程,每个线程有自己的时间片,当时间片到了就执行下一个线程。为了让线程能严格交替执行,可以用互斥对象和事件对象实现。
2.1 互斥对象实现线程同步
//实现主线程里的新线程在交替时间片内运行 #include <windows.h> #include <iostream> using namespace std; //线程函数 原型声明 DWORD WINAPI Fun1Proc(LPVOIDlpParameter); // thread data DWORD WINAPI Fun2Proc(LPVOIDlpParameter); // thread data //int index = 0; //定义一个循环计数 int tickets = 100; HANDLE hMutex; //定义一个全局的互斥对象 void main() { //主程序运行时,自动创建主线程,我们用CreateThread()函数创建线程 HANDLEhThread1; HANDLEhThread2; hThread1= CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL); //(1null表示使用缺省的安全性,2 0表示和调用线程一样的大小,3指定线程的入口函数地址,4传递给线程的参数, 5 0表示一旦创建立即运行如果设置为CREATE_SUSPENDED 表示遇到 ResumeThread function 时调用,6线程的ID ,不使用用NULL ) hThread2= CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL); CloseHandle(hThread1); //这里刚创建线程就关闭,其实并没有终止创建的线程。只是主线程中对新线程的引用不感兴趣,关闭后可以减小线程内核的引用计数 CloseHandle(hThread2); hMutex=CreateMutex(NULL,FALSE, NULL); //创建互斥对象,如果false改为true,则表示主线程拥有互斥对象,所以如果主线程不释放互斥对象,别的线程是得不到互斥对象的。 //通过命名的互斥对象,让应用程序只有一个实例运行 /*hMutex= CreateMutex(NULL, FALSE, "tickets"); if(hMutex) { if(ERROR_ALREADY_EXISTS == GetLastError()) { cout<< "only instance is running"<< endl; return; } }*/ //如果为true,则表示拥有互斥对象,若再次请求互斥对象,互斥对象的计数器会加一,表示又请求了一次,而且请求成功 /*hMutex= CreateMutex(NULL,TRUE, NULL); WaitForSingleObject(hMutex,INFINITE); //虽然主线程拥有互斥对象,所以它现在为未通知状态,但请求互斥对象的ID 和拥有互斥对象的ID 是同一个ID ,所以可以请求到 ReleaseMutex(hMutex); //拥有了两次,释放两次(多次请求,多次释放。通过计数器记录的) ReleaseMutex(hMutex);*/ //主线程不能在卖完100张票前结束,主线程睡眠足够时间让子线程有足够时间片执行 Sleep(4000); //Sleep(10); //暂停函数,这里暂停10ms, sleep time in milliseconds system("pause"); } //线程函数 实现 DWORD WINAPI Fun1Proc(LPVOID lpParameter) { while(TRUE) { //线程1得到互斥对象,互斥对象的ID就为线程1的线程ID,互斥对象变为未通知状态, WaitForSingleObject(hMutex,INFINITE); //互斥对象相当于一个钥匙,有了它,才能往下执行,此时钥匙在我这,即使线程睡觉了,别人进不了这个房间,当离开房间,交出钥匙,别人才能拿到钥匙,进去房间 if(tickets > 0) { Sleep(1); //执行sleep表示操作系统暂时放弃当前线程一段时间,执行下一个线程,此时这个线程停止在这里,当下次这个线程运行时,接着运行下一行 cout<< "Thread1 sell ticket:" << tickets-- << endl; } else break; ReleaseMutex(hMutex); //释放互斥对象,释放后操作系统将互斥对象线程ID为0,互斥对象为已通知状态,这样线程2才能获得互斥对象 } /*WaitForSingleObject(hMutex,INFINITE);//如果操作系统认为线程结束,那么在这个线程里请求的互斥对象引用计数和线程ID为零,线程2就可以得到互斥对象了 cout<< "Thread1 is running!" <<endl;*/ return0; } DWORD WINAPI Fun2Proc(LPVOID lpParameter) { while(TRUE) { WaitForSingleObject(hMutex,INFINITE); //线程1在sleep时,执行线程2,但线程2这里发生互斥,执行不了,然后当线程1睡醒了就继续执行线程1, if(tickets > 0) { Sleep(1); cout<< "Thread2 sell ticket:" << tickets-- << endl; } else break; ReleaseMutex(hMutex); } /*WaitForSingleObject(hMutex,INFINITE); cout<< "Thread2 is running!" << endl;*/ return0; }
程序中通过互斥对象实现在每个子线程的时间片内交替循环执行一次,tickes是全局变量。互斥对象(mutex)属于内核对象,它能够确保线程拥有对单个资源的互斥访问权。互斥对象包含一个使用数量,一个线程ID和一个计数器。ID用于标识系统中的哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数。
下面是程序中几个关键函数
1.创建互斥对象
HANDLE CreateThread( //The CreateThread function creates a thread to execute within theaddress space of the calling process. LPSECURITY_ATTRIBUTESlpThreadAttributes, // pointer to securityattributes,结构体指针, DWORDdwStackSize, // initial thread stacksize,指定初始栈大小 LPTHREAD_START_ROUTINElpStartAddress, // pointer to threadfunction,指向一个应用程序的线程的指针 LPVOIDlpParameter, // argument for new thread,指定一个单独的参数值传递给线程 DWORDdwCreationFlags, // creation flags,指定控制线程创建的附加标记 LPDWORDlpThreadId // pointer to receivethread ID,函数的返回值,指向一个变量,接收线程的标示符ID );
2.创建互斥对象
HANDLE CreateMutex( //创建互斥对象,完成线程的同步 TheCreateMutex function creates a named or unnamed mutex object. LPSECURITY_ATTRIBUTES lpMutexAttributes, // pointer to security attributes,结构指针,NULL 表示默认的安全性 BOOL bInitialOwner, //flag for initial ownership,互斥对象初始拥有者,真表示调用者创建互斥对象,调用的线程获得互斥对象的所有权,否则不获得 LPCTSTR lpName //pointer to mutex-object name),互斥对象名字,null表示没有名字 )
3.请求互斥对象
WaitForSingleObject //下面两种情况发生时,这个函数返回 The specified object is in the signaled state. //指定的对象处于有信号状态 The time - out interval elapses. //超时的时间间隔流逝了 DWORD WaitForSingleObject( //The WaitForSingleObject function returns when one of the following occurs : HANDLE hHandle, // handle to object to wait for,等待的互斥对象的句柄 DWORD dwMilliseconds // time-out interval in milliseconds,超时的时间间隔,如果时间流逝了,即使所等待的对象处于非信号状态的,函数返回。参数为0,表示测试对象状态,立即返回,参数为INFINITE,表示一直等待,直到等待对象处于有信号状态 );
4.释放互斥对象 //那个线程拥有互斥对象,哪个线程释放互斥对象
ReleaseMutex //释放指定互斥对象的所有权,执行成功返回非0.失败返回0 BOOL ReleaseMutex( //The ReleaseMutex function releasesownership of the specified mutex object. HANDLE hMutex // handle to mutex object );
2.2 事件对象实现线程同步
#include<Windows.h> #include<iostream> using namespace std; //线程函数 原型声明 DWORD WINAPI Fun1Proc(LPVOIDlpParameter); // thread data DWORD WINAPI Fun2Proc(LPVOIDlpParameter); // thread data int tickets = 100; HANDLE g_hEvent; //定义一个全局的互斥对象的句柄,保存时间对象的句柄 void main() { HANDLEhThread1; HANDLEhThread2; hThread1= CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL); //(1null表示使用缺省的安全性,2 0表示和调用线程一样的大小,3指定线程的入口函数地址,4传递给线程的参数, 5 0表示一旦创建立即运行如果设置为CREATE_SUSPENDED 表示遇到 ResumeThread function 时调用,6线程的ID ,不使用用NULL ) hThread2= CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL); CloseHandle(hThread1); //这里刚创建线程就关闭,其实并没有终止创建的线程。只是主线程中对新线程的引用不感兴趣,关闭后可以减小线程内核的引用计数 CloseHandle(hThread2); /* HANDLE CreateEvent(The CreateEvent function creates a named or unnamedevent object. LPSECURITY_ATTRIBUTESlpEventAttributes, // pointer tosecurity attributes NULL表示默认安全性 BOOLbManualReset, // flag for manual-reset event,指定是否人工重置或自动重置的对象被创建。真表示人工重置,假表示自动重置 BOOLbInitialState, // flag for initial state,指定事件初始化状态 LPCTSTRlpName // pointer to event-object name,事件对象的名字 );*/ //g_hEvent= CreateEvent(NULL,FALSE,FALSE,NULL); //第三个参数指定初始化为无信号状态 g_hEvent= CreateEvent(NULL, FALSE, FALSE, "tickets"); //创建命名的互斥对象 if(g_hEvent) { if(ERROR_ALIAS_EXISTS==GetLastError()) { cout<< "only instance can run!" << endl; return; } } SetEvent(g_hEvent); //设定为有信号状态,人工重置的对象为有信号状态时,所以等待该时间的线程,都可以调度,可以同时运行。 //自动重置的对象,为有信号状态时,当线程得到该对象,操作系统自动设置为无信号状态,这样别的线程无法得到此对象。 Sleep(4000); CloseHandle(g_hEvent); } //线程函数 实现 DWORD WINAPI Fun1Proc(LPVOID lpParameter) { while(TRUE) { //线程1得到互斥对象,互斥对象的ID就为线程1的线程ID,互斥对象变为未通知状态, WaitForSingleObject(g_hEvent,INFINITE); //互斥对象相当于一个钥匙,有了它,才能往下执行,此时钥匙在我这,即使线程睡觉了,别人进不了这个房间,当离开房间,交出钥匙,别人才能拿到钥匙,进去房间 if(tickets > 0) { Sleep(1); //执行sleep表示操作系统暂时放弃当前线程一段时间,执行下一个线程,此时这个线程停止在这里,当下次这个线程运行时,接着运行下一行 cout<< "Thread1 sell ticket:" << tickets-- << endl; } else break; SetEvent(g_hEvent); //将事件对象设置为有信号状态,释放互斥对象的控制权,不再运行此线程,这个线程释放了互斥对象的控制权后,如果其他进程在等待互斥对象置位,则等待的线程可以得到该互斥对象,等待函数返回,互斥对象被新的线程所拥有。 } return0; } DWORD WINAPI Fun2Proc(LPVOID lpParameter) { while(TRUE) { WaitForSingleObject(g_hEvent,INFINITE); //线程1在sleep时,执行线程2,但线程2这里发生互斥,执行不了,然后当线程1睡醒了就继续执行线程1 if(tickets > 0) { Sleep(1); cout<< "Thread2 sell ticket:" << tickets-- << endl; } else break; SetEvent(g_hEvent); } return0; }
事件对象也属于内核对象,包含一个使用计数,一个用于指明该事件是一个自动重置的事件还是一个人工重置的事件的布尔值,另一个用于指明该事件处于已通知状态还是未通知状态的布尔值。有两种不同类型的事件对象。一种是人工重置的事件,另一种是自动重置的事件。当人工重置的事件得到通知时,等待该事件的所有线程均变为可调度线程。当一个自动重置的事件得到通知时,等待该事件的线程中只有一个线程变为可调度线程。
互斥对象和事件对象属于内核对象,利用内核对象进行线程同步,速度较慢,但利用互斥对象和事件对象这样的内核对象,可以在多个进程中的各个线程间进行同步。
以上是关于MFC——9.多线程与线程同步的主要内容,如果未能解决你的问题,请参考以下文章