孙鑫VC++第15课:多线程与聊天室01——基本概念与线程互斥

1、基本概念

1.1 程序、进程

程序是计算机指令的集合,它以文件的形式存储在磁盘上。

进程:通常被定义为一个正在运行的程序的实例,是一个程序在其自身的地址空间内的一次执行活动。

一个程序对应多个进程,可以同时打开多个记事本程序的进程。在一个进程中,也可以同时访问多个程序。

进程的资源申请、调度和独立运行的单位,因此,它使用系统中的运行资源;而程序不能申请系统资源,不能被系统调度,也不能作为独立的运行单位,因此它不占用系统的运行资源。

进程由两部分组成:

  1)操作系统用来管理进程的内核对象。内核对象也是系统用来存放关于进程的统计信息的地方。(内核对象是操作系统内部分配的一个内存块,该内存块是一种数据结构。其成员负责维护该对象的各种信息。内核对象的数据结构只能被内核访问,因此应用程序无法访问内核对象的数据结构。只有通过windows提供的函数对内核对象进行操作)

  2)地址空间。它包含所有可执行模块或DLL模块的代码和数据。它还包含动态内存分配的空间。如线程堆栈与堆分配空间。

进程是不活泼的。进程从来不执行任何东西,它只是线程的容器。若要使进程完成某项操作,它必须拥有一个在它的环境中运行的线程,此线程负责执行包含在进程的地址空间中的代码。(真正完成代码执行的线程,而进程只是线程的容器,或者说是线程的执行环境)

单个进程可能包含若干个线程,这些线程都“同时”执行进程地址空间中的代码。

每个进程至少拥有一个线程,来执行进程的地址空间中的代码。当创建一个线程时,操作系统会自动创建这个进程的第一个线程,称为主线程(即执行main函数或winMain函数的线程,可以把Main函数或winmain函数看成是主线程的进入点函数)。此后,该线程可以创建其它线程。

1.2 进程地址空间

系统赋予每个进程独立的虚拟地址空间。对于32位进程而言,这个地址空间的4GB。

每个进程有它自己的私有地址空间。进程A可能有一个存放在它地址空间中的数据结构,地址是0x12345678,而进程B则有一个完全不同的数据结构存放在它的地址空间中,地址是0x12345678。当进程A中运行的线程访问地址为0x12345678的内存时,这些线程访问的是进程A的数据结构。当进程B中运行的线程访问地址为0x12345678的内存时,这些线程访问的是进程B的数据结构。进程A中运行的线程不能访问进程B的地址空间中的数据结构,反之亦然。

4GB是虚拟的地址空间,只是内存地址的一个范围。在你能成功地访问数据而不会出现非法访问之前,必须赋予物理存储器,或者物理存储器映射到各个部分的地址空间。(此处的物理存储器包括了物理内存和页文件大小)

4GB虚拟地址空间中,2GB是内核方式分区,供内核代码、设备驱动程序、设备I/O高速缓冲区、非页面内存池的分配和进程页面表等使用,而用户方式分区使用的地址空间约为2GB,这个分区是进程的私有地址空间所在的地方。一个进程不能读取、写或者以任何方式访问驻留在该分区中的另一个进程的数据。对于所有应用程序来说,该分区是维护进程的大部分数据的地方。

 

1.3 线程

线程由两部分组成:

  1)线程的内核对象,操作系统用它来对线程实施管理。内核对象也是系统用来存放线程统计信息的地方。

  2)线程堆栈,它用于维护线程在执行代码时需要的所有参数和局部变量。

当创建线程时,系统创建一个线程内核对象。该线程内核对象不是线程本身,而是操作系统用来管理线程的较小的数据结构。可以将线程内核对象视为由关于线程的统计信息组成的一个小型数据结构。

线程总是在某个进程环境中创建。系统从进程的地址空间中分配内存,供线程的堆栈使用。新线程运行的进程环境与创建线程的环境相同。因此,新线程可以访问进程的内核对象的所有句柄、进程中的所有内存和在这个相同的进程中所有的其他线程的堆栈。这使得单个进程中的多个线程确实能够非常容易地互相通信。

线程只有一个内核对象和一个堆栈,保留的记录很少,因此所需的内存也很少。

因为线程需要的开销比进程少,因此在编程中经常采用多线程来解决编程问题,而尽量避免创建新的进程。

线程运行:

操作系统为每一个运行线程安排一定的CPU时间——时间片。系统通过一种循环的方式为线程提供时间片,线程在自己的时间片内运行,因时间片相当短,因此,给用户的感觉,就好像线程是同时运行一样。

如果计算机有多个CPU,线程就能真正意义上同时运行了。

1.4 单线程程序与多线程程序的图解

多线程中有一个主线程。类比医生手术任务。主线程类比主刀医生,其它线程类比助手护士,提高了手术的效率。 

采用多线程而不是多进程的理由:

  1)进程的创建,系统要为进程分配4GB虚拟地址空间,占用资源多;而多线程共享同一里程的地址空间,占用资源少。

  2)进程间切换时,需要交换整个地址空间;线程间的切换,只是执行环境的改变,效率高。

 

2、多线程程序设计

2.1 创建线程

使用函数CreateThread函数创建一个新的线程。线程创建成功,则返回新创建线程的句柄。

HANDLE CreateThread(

  LPSECURITY_ATTRIBUTES lpThreadAttributes,  //SD

  DWORD dwStackSize,               //initial stack size

  LPTHREAD_START_ROUTINE lpStartAddress,    //thread function

  LPVOID lpParameter,                 //thread argument

  DWORD dwCreationFlags,              //creation option

  LPWORD lpThreadId                //thread identifier

);

lpThreadAttributes:指向SECURITY_ATTRIBUTES结构体的指针(传递NULL将使用缺省的安全性)

dwStackSize:指定初始提交的栈的大小,以字节为单位。系统会将此值四舍五入为最近的页面。(页面是系统在管理内存时使用的内存单位,不同CPU其页面大小不同。X86使用的页面大小是4kb,当保留地址空间的区域时,系统需要确保该区域的大小是系统页面大小的倍数。如需要保留10kb地址空间区域,系统将自动对请求进行四舍五入,使保留的地址空间为页面大小的倍数,在x86平台上,系统将保留12kb区域。)如果此值是0,或小于缺省提交的大小,则使用调用线程一样的大小。

lpStartAddress:指向LPTHREAD_START_ROUTINE类型的应用程序定义的函数的指针。该函数被线程执行,表示了线程的起始地址。(对于主线程而言,main函数是其入口函数。)本参数则指定了创建线程的入口函数。函数类型参看ThreadProc函数。

  DWORD WINAPI ThreadProc(

    LPVOID lpParameter  //thread data

  );

  在程序中定义一个函数,作为线程的入口函数,函数名可以随意(可以直接使用ThreadProc),函数类型必须遵照ThreadProc声明的类型(参数类型与返回值类型必须与ThreadProc一致)。

lpParameter:指定一个参数值,传递给线程。(main函数可以接收命令行参数,同样线程入口函数也可以接收参数)

dwCreationFlags:用于指定控制线程创建的附加标记。如果值为CREATE_SUSPENDED,线程创建后会处于挂起状态,直到函数ResumeThread被调用才会得到执行可能。如果值为0,线程创建后立即执行。

lpThreadId:[out]用于接收线程标识符的指针。创建线程后,系统会为线程分配ID号。NT/2000下,如果参数设置NULL,线程的ID则不会返回。95/98下此参数不能为NULL。

线程创建成功则返回新创建线程的句柄。

线程创建完成后,调用CloseHandle关闭线程句柄。关闭线程句柄并没有终止刚刚创建的线程,CloseHandle表示在主线程中,对新创建的线程引用关闭。另一方面,关闭线程句柄后,系统会递减新线程的线程内核对象的使用计数。当所创建的线程执行完毕后,系统也会递减线程内核对象的使用计数。当使用计数为0时,系统将释放线程内核对象。如果在主线程中没有关闭线程的句柄,始终会保留一份线程的引用,如此线程内核对象的引用计数就不会为0。即使线程执行完毕,线程内核对象也不会被释放,只有当进程终止时,系统才会为这些残留的对象做清理工作。所以应该在不再使用线程句柄时,及时关闭,使其线程内核对象引用计数减一。

 1 #include <Windows.h>
 2 #include <iostream>
 3 
 4 using namespace std;
 5 DWORD WINAPI Fun1Proc(LPVOID lpParameter);//声明线程入口函数
 6 
 7 int main()//程序启动运行时,产生一个主线程。main函数是主线程的入口函数
 8 {
 9     HANDLE hThread1;
10     hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
11     CloseHandle(hThread1);//关闭线程句柄
12     cout<<"Main is running..."<<endl;
13     system("pause");
14 }
15 
16 DWORD WINAPI Fun1Proc(LPVOID lpParameter)//定义线程入口函数
17 {
18     cout<<"thread 1 is running..."<<endl;    
19     return 0;
20 }

执行上述代码后,控制台能正常打印出主线程和新建线程的数据。(孙老师的演示没有打印出新建线程,原因是主线程执行完成后退出,导致进程退出,所有进程资源包括未执行的线程强制退出。新创建的线程未得到执行机会即被退出。)

让主线程暂停执行,通过调用Sleep函数暂停主线程的执行。

VOID Sleep(

  DWORD dwMillisSeconds  //sleep time(毫秒为单位)

);

 

2.2 线程调度分析

分别在主线程和新建线程中增加循环,查看二者的执行情况。

从控制台输出情况看,二者在其各自时间片内交替执行。

 

2.3 模拟火车售票系统

两个新建线程销售车票。

 1 #include <Windows.h>
 2 #include <iostream>
 3 
 4 using namespace std;
 5 DWORD WINAPI Fun1Proc(LPVOID lpParameter);//声明线程入口函数
 6 DWORD WINAPI Fun2Proc(LPVOID lpParameter);
 7 int index=0;
 8 int tickets = 100;//全局变量,表示待销售的票数
 9 int main()//程序启动运行时,产生一个主线程。main函数是主线程的入口函数
10 {
11     HANDLE hThread1, hThread2;
12     hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
13     hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
14     CloseHandle(hThread1);//关闭线程句柄
15     CloseHandle(hThread2);
16     
17     /*while(index++ < 100)
18         cout<<"Main is running..."<<endl;*/
19     Sleep(4000);//睡眠4s钟,确保票卖完前,主线程不退出,也不占用CPU执行时间
20     //Sleep(1000);
21     system("pause");
22 }
23 
24 DWORD WINAPI Fun1Proc(LPVOID lpParameter)//定义线程入口函数
25 {
26     /*while(index++<100)
27         cout<<"thread 1 is running..."<<endl;*/
28     while(TRUE)
29     {
30         if (tickets > 0)
31         {
32             cout<<"Thread1 sells ticket: "<<tickets--<<endl;
33         }
34         else
35         {
36             break;
37         }
38     }
39     return 0;
40 }
41 DWORD WINAPI Fun2Proc(LPVOID lpParameter)
42 {
43     while(TRUE)
44     {
45         if (tickets>0)
46         {
47             cout<<"Thread2 sells ticket: "<<tickets--<<endl;
48         }
49         else
50         {
51             break;
52         }
53     }
54     return 0;
55 }

上述程序存在一个隐患,竞争线程访问共享资源时,未做访问互斥保护。可能导致同一张票被卖2次。

解决方案是对线程进程同步。当一个线程在车票销售过程中,其它线程在此阶段中不能对同一种资源再进行访问。必须等到前面的线程完成售票过程后,其它线程才能访问同一种资源。类比商场的试衣间,试衣间类似于共享资源。

2.4 线程间同步

2.4.1 创建互斥对象

使用函数创建互斥对象完成线程的同步,CreateMutex

HANDLE CreateMutex(//创建或打开一个命名/无命名的信号量对象

  LPSECURITY_ATTRIBUTES lpMutexAttributes,  //SD传递NULL,使用默认的安全性

  BOOL bInitialOwner,               //initial owner指示互斥对象的初始拥有者。TRUE表示调用函数的线程获得互斥对象的所有权,否则不获得

  LPCTSTR lpName                  //互斥对象的名字。NULL表示匿名互斥对象

);

bInitialOwner:TRUE表示调用函数CreateMutex的线程完成调用后,获得该互斥对象的所有权。否则(FALSE)调用线程不获得互斥对象所有权。

互斥对象:

互斥对象(mutex)属于内核对象,它能够确保线程拥有对单一资源的互斥访问权。

互斥对象包含一个使用数量,一个线程ID和一个计数器。

ID用于标识系统中哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数。

声明一个全局的互斥对象,并在main函数创建该互斥对象

HANDLE hMutex;//声明一个全局互斥对象
int main()//程序启动运行时,产生一个主线程。main函数是主线程的入口函数
{
    HANDLE hThread1, hThread2;
    hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
    hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
    CloseHandle(hThread1);//关闭线程句柄
    CloseHandle(hThread2);
    
    hMutex = CreateThread(NULL, FALSE, NULL);//创建一个默认安全属性的、当前主线程不获得所有权的、匿名互斥对象

2.4.2 获取互斥对象

通过函数WaitForSingleObject获得互斥对象。

DWORD WaiteForSingleObject(

  HANDLE hHandle,      //handle to object对象的句柄(程序中传递的是互斥对象的句柄,一旦互斥对象变成有信号状态,本函数就返回)

  DWORD dwMilliseconds    //time-out interval指示一个超时的时间间隔(毫秒),时间超时后,函数返回(即使所等待对象处于无信号状态)

);

当下面情况之一发生时,返回:

  1)指定对象处于有信号状态

  2)发生了超时

hHandle:如果互斥对象一直未变成有信号状态,即未通知状态,上述函数将会一直等待。函数等待导致线程暂停运行。

dwMilliseconds:如果参数设置为0,函数测试对象的状态后立即返回。如果参数设置为INFIITE,函数超时功能失效,函数将一直等对象出现信号。

返回值:

WAIT_OBJECT_0:所请求互斥对象变为有信号状态

WAIT_ABANDONED:线程异常终止,或终止前未调用ReleaseMutex函数

WAIT_TIMEOUT:超时导致返回

 1 DWORD WINAPI Fun1Proc(LPVOID lpParameter)//定义线程入口函数
 2 {
 3     /*while(index++<100)
 4         cout<<"thread 1 is running..."<<endl;*/
 5     while(TRUE)
 6     {
 7         WaitForSingleObject(hMutex, INFINITE);//等待信号
 8         if (tickets > 0)
 9         {
10             Sleep(1);
11             cout<<"Thread1 sells ticket: "<<tickets--<<endl;
12         }
13         else
14         {
15             break;
16         }
17     }
18     return 0;
19 }
20 DWORD WINAPI Fun2Proc(LPVOID lpParameter)
21 {
22     while(TRUE)
23     {
24         WaitForSingleObject(hMutex, INFINITE);//等待信号
25         if (tickets>0)
26         {
27             Sleep(1);
28             cout<<"Thread2 sells ticket: "<<tickets--<<endl;
29         }
30         else
31         {
32             break;
33         }
34     }
35     return 0;
36 }

  上个步骤创建互斥对象时,第二个参数传递的是FALSE,表示互斥对象创建时,当前没有线程拥有互斥对象。操作系统将互斥对象设置为已通知状态,即有信号状态。当第一个线程开始运行时,调用了WaitForSingleObject(上述代码第7行),请求到了互斥对象。此时操作系统将互斥对象的线程ID设置为获得该互斥对象的线程的ID,并将互斥对象设置为未通知状态。第一个线程得以继续执行。而当第一个线程执行到sleep时(第10行),暂停。操作系统则选择第二个线程开始运行。第二个线程进入while循环(第22行)中,直到到调用WaitForSingleObject(第24行)。而此时互斥对象已经被第一个线程所拥有,第二个线程调用的WaitForSingleObject开始进行等待。第二个线程暂停执行。此时线程一暂停时间到后,得到继续执行,完成出票功能。线程一在对共享资源操作完成后,需要释放互斥对象,将其设置为已通知状态。通过调用ReleaseMutex对互斥对象进行释放。

BOOL ReleaseMutex(

  HANDLE hMetux  //handle to mutex

);

函数释放指定的互斥对象的所有权。如果成功返回非0值(TRUE),失败返回0.

调用了ReleaseMutex后,操作系统会将互斥对象的线程ID设置为0,并将互斥对象设置成已通知状态。如果此时轮到线程二执行,其WaitForSingleObject将得到互斥对象的所有权,并继续执行。

 1 #include <Windows.h>
 2 #include <iostream>
 3 
 4 using namespace std;
 5 DWORD WINAPI Fun1Proc(LPVOID lpParameter);//声明线程入口函数
 6 DWORD WINAPI Fun2Proc(LPVOID lpParameter);
 7 int index=0;
 8 int tickets = 100;//全局变量,表示待销售的票数
 9 HANDLE hMutex;//声明一个全局互斥对象
10 int main()//程序启动运行时,产生一个主线程。main函数是主线程的入口函数
11 {
12     HANDLE hThread1, hThread2;
13     hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
14     hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
15     CloseHandle(hThread1);//关闭线程句柄
16     CloseHandle(hThread2);
17     
18     hMutex = CreateMutex(NULL, FALSE, NULL);//创建一个默认安全属性的、当前主线程不获得所有权的、匿名互斥对象
19     /*while(index++ < 100)
20         cout<<"Main is running..."<<endl;*/
21     Sleep(4000);//睡眠4s钟,确保票卖完前,主线程不退出,也不占用CPU执行时间
22     //Sleep(1000);
23     system("pause");
24 }
25 
26 DWORD WINAPI Fun1Proc(LPVOID lpParameter)//定义线程入口函数
27 {
28     /*while(index++<100)
29         cout<<"thread 1 is running..."<<endl;*/
30     while(TRUE)
31     {
32         WaitForSingleObject(hMutex, INFINITE);//等待信号
33         if (tickets > 0)
34         {
35             Sleep(1);
36             cout<<"Thread1 sells ticket: "<<tickets--<<endl;
37         }
38         else
39         {
40             break;
41         }
42         ReleaseMutex(hMutex);//释放互斥对象
43     }
44     return 0;
45 }
46 DWORD WINAPI Fun2Proc(LPVOID lpParameter)
47 {
48     while(TRUE)
49     {
50         WaitForSingleObject(hMutex, INFINITE);//等待信号
51         if (tickets>0)
52         {
53             Sleep(1);
54             cout<<"Thread2 sells ticket: "<<tickets--<<endl;
55         }
56         else
57         {
58             break;
59         }
60         ReleaseMutex(hMutex);//释放互斥对象
61     }
62     return 0;
63 }

 2.4.3 互斥对象的所有权

在创建互斥对象时,将第二个参数(创建线程是否获取互斥对象的所有权)设置为TRUE,运行代码后发现两个线程入口函数售票动作均未执行。

第二个参数为TRUE时,表明主线程拥有了互斥对象的所有权。主线程没有退出,也没有释放互斥对象,导致其它线程等待不到互斥对象,从而无法继续执行。

此时通过在其它线程内部调用ReleaseMutex是无法释放主线程拥有的互斥对象的。因为在主线程获取互斥对象时,操作系统将互斥对象的ID设置成了主线程的ID,调用ReleaseMutex时,操作系统先判断当前调用ReleaseMute函数的线程其ID与互斥对象内部所维护的线程ID号是否相等,如果不等,互斥对象不能被释放,ReleaseMutex返回FALSE。

因此需要遵守互斥对象谁拥有,谁释放的原则。

2.4.4 互斥对象的引用计数

另外一种情况,当主线程创建互斥对象后,再调用WaitForSingleObject。此时主线程拥有互斥对象多次计数。

当主线程调用WaitForSingleObject请求互斥对象时,操作系统判断所请求互斥对象的线程ID号与拥有互斥对象的线程ID是否相等,如果相等即使互斥对象处于未通知状态,其仍然可获得互斥对象的所有权。一个线程多次获得互斥对象的所有权,是通过互斥对象内部的计数器维护的。当主线程第一次创建互斥对象时,主线程拥有了该互斥对象(第二参数为TRUE),操作系统除了将互斥对象的线程ID设置成主线程ID外,同时将互斥对象的计数器累加变成1.当主线程再次请求互斥对象并成功时,操作系统再次累加互斥对象计数器变成2。

调用ReleaseMutex函数实际上是单次递减互斥对象的引用计数器的值。

如果在同一个线程内多次调用WaitForSingleObject请求了互斥对象,那么在线程内需要多次调用ReleaseMutex去释放互斥对象的引用。

2.4.5 线程运行结束

如果线程在终止前,未调用ReleaseMutex释放其请求过的互斥对象。当其中止后,操作系统会将其拥有的互斥对象收回,将互斥对象的线程ID设置为0,并将其计数器归0.

操作系统维护了线程及其相关的互斥对象信息,操作系统可获知当前运行终止的线程,一旦操作系统判断出当前终止的线程,将收回该线程所拥有的互斥对象。

2.5 命名的互斥对象(实现程序单实例执行)

CreateMutex函数的返回值:如果函数执行成功,返回互斥对象的句柄。如果命名的互斥对象在CreateMutex函数调用之前就已经存在,函数将返回已经存在的互斥对象的句柄。此时调用GetLastError可得到ERROR_ALREADY_EXISTS。否则调用者创建互斥对象。

    hMutex = CreateMutex(NULL, TRUE, (LPCTSTR)"tickets");
    if (hMutex)
    {
        if (ERROR_ALREADY_EXISTS == GetLastError())//实例存在则返回
        {
            cout<<"instance is running..."<<endl;
            return 0;
        }
    }

 

posted @ 2014-11-09 18:02  calkevin  阅读(1168)  评论(0)    收藏  举报