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、 线程堆栈,它用于维护线程在运行代码时须要的全部參数和局部变量。
线程总是在某个进程环境中创建。
系统从进程的地址空间中分配内存,供线程的堆栈使用。新线程执行的进程环境与创建线程的环境同样。因此。新线程可以訪问进程的内核对象的全部句柄、进程中的全部内存和在这个同样的进程中的全部其它线程的堆栈。这使得单个进程中的多个线程确实可以非常easy地互相通信。
线程仅仅有一个内核对象和一个堆栈。保留的记录非常少,因此所须要的内存也非常少。由于线程须要的开销比进程少。因此在编程中常常採用多线程来解决编程问题,而尽量避免创建新的进程。
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; }
事件对象也属于内核对象。包括一个使用计数。一个用于指明该事件是一个自己主动重置的事件还是一个人工重置的事件的布尔值。还有一个用于指明该事件处于已通知状态还是未通知状态的布尔值。
有两种不同类型的事件对象。一种是人工重置的事件。还有一种是自己主动重置的事件。
当人工重置的事件得到通知时,等待该事件的全部线程均变为可调度线程。
当一个自己主动重置的事件得到通知时。等待该事件的线程中仅仅有一个线程变为可调度线程。
相互排斥对象和事件对象属于内核对象。利用内核对象进行线程同步。速度较慢,但利用相互排斥对象和事件对象这种内核对象,能够在多个进程中的各个线程间进行同步。