windows下c++编写多线程

一.进程

首先来看进程,它是一个正在运行的程序的实例,是一个程序在其自身的地址空间的一次执行活动。进程是资源申请、调度、和独立运行的基本单位。进程有两部分组成:

1操作系统用来管理进程的内核对象,内核对象是系统用来存放关于进程的统计信息的地方,它是操作系统内部分配的一块内存块,该内存块是一种数据结构,其成员负责维护该对象的各种信息。

2地址空间,它包含所有可执行模块、dll模块的代码和数据,也包含动态内存分配的空间。

二.线程

真正完成代码执行的是线程,而进程只是线程的容器,或者说是线程的执行环境。

每个进程至少拥有一个线程,来执行进程的地址空间的代码。当创建一个进程时,操作系统自动创建该进程的第一个线程,称为主线程,也就是执行main或WinMain函数的线程,也可以把main或WinMain函数看作是主线程的进入点函数,然后主线程可以创建其他线程。

线程由两部分组成:

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

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

操作系统为每一个运行线程安排一定的CPU时间—时间片,

创建线程可以使用系统提供的API函数CreateThread来完成,该函数原型如下:

HANDLECreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes, DWORD dwStackSize, LPTHREAD_START_ROUTINElpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId);

lpThreadAttributes:指向SECURITY_ATTRIBUTES的指针,这里可以传递NULL,让该线程使用默认的安全性。

dwStackSize 设置线程初始栈的大小,如果为0,或小于默认值的大小,那么默认将使用与调用该函数的线程相同的栈空间大小。

lpStartAddress 指向应用程序定义的THREAD_START_ROUTINE类型的函数的指针,这个函数将由新线程执行,表明新线程的起始地址。我们知道main函数是主线程的起始地址,同样,新创建的线程也需要有一个入口函数,这个函数的地址就由此参数决定,,这就要求在程序中定义一个函数作为新线程的入口函数,遵照下述声明形式

DWORD WINAPIThreadFunc(LPVOID lpParameter);

新线程入口函数有一个LPVOID类型的参数,并且返回值是DWORD类型,函数名称可以改变。

lpParameter,通过这个参数给新创建的线程传递参数,该参数提供了一种将初始化值传递给线程的手段。

dwCreationFlags,设置用于控制线程创建的附加标记,他可以是两个值中的一个:CREATE_SUSPENDED或0,。如果该值为CREATE_SUSPENDED,那么线程创建后处于暂停状态,直到程序调用了ResumeThread函数为止,如果该值为0,那么线程在创建后立即执行。

lpThreadId 该参数是一个返回值,指向一个变量,用来接收线程ID。当创建一个线程后,系统会为线程分配一个ID。

三.线程同步

看一个例子,火车站售票模拟程序。

int index = 0;

int tickets = 100;

HANDLE hMutex = NULL;

CRITICAL_SECTION g_cs = NULL;

HANDLE hEvent = NULL;

DWORD WINAPI Func1(LPVOID pParam)

{

   while(true)

   {

      WaitForSingleObject(hMutex, INFINITE);

//    WaitForSingleObject(hEvent,INFINITE);

//    EnterCriticalSection(&g_cs);

      if(tickets> 0)

      {

        Sleep(1);

        cout<<"thread1 sell tickets:"<<tickets--<<endl;

      }

      else

        break;

      ReleaseMutex(hMutex);

//    SetEvent(hEvent);

//    LeaveCriticalSection(&g_cs);

   }

   return0;

}

DWORD WINAPI Func2(LPVOID pParam)

{

   while(true)

   {

      WaitForSingleObject(hMutex,INFINITE);

//    WaitForSingleObject(hEvent,INFINITE);

//    EnterCriticalSection(&g_cs);

      if(tickets> 0)

      {

        Sleep(1);

        cout<<"thread2 sell tickets:"<<tickets--<<endl;

      }

      else

        break;

      ReleaseMutex(hMutex);

//    SetEvent(hEvent);

//    LeaveCriticalSection(&g_cs);

   }

   return0;

}

voidmain()

{

   HANDLE hThread1 =CreateThread(NULL, 0, Func1, NULL, 0, NULL);

   HANDLE hThread2 =CreateThread(NULL, 0, Func2, NULL, 0, NULL);

   CloseHandle(hThread1);

   CloseHandle(hThread2);

   hMutex = CreateMutex(NULL,FALSE,NULL);

   //hEvent =CreateEvent(NULL, FALSE, TRUE,NULL);

   //InitializeCriticalSection(&g_cs);

   Sleep(4000);

   //DeleteCriticalSection(&g_cs);

}

1利用互斥对象实现线程同步

互斥对象属于内核对象,它能够确保线程拥有对单个资源的互斥访问权,互斥对象包含一个使用数量、线程ID和一个计数器。其中ID用于标识系统中哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数。

创建互斥对象使用函数CreateMutex,该函数可以创建或打开一个命名的或匿名的互斥对象,然后程序就可以利用该互斥对象完成线程间的同步。CreateMutex的声明如下:

HANDLE CreateMutex(LPSECURITY_ATTRIBUTES  lpMutexAttributes, BOOL bInitialOwner, LPCTSTRlpName)

该函数具有三个参数,

lpMutexAttributes指向SECURITY_ATTRIBUTES结构的指针,这里可以传递NULL,让该互斥对象使用默认的安全性。

bInitialOwner BOOL类型,指定互斥对象初始的拥有者,如果该值为真,则创建这个互斥对象的线程获得该对象的所有权,否则该线程将不获得该对象的所有权。

lpName 指定互斥对象的名称,如果该参数为NULL,则创建一个匿名的互斥对象。

如果调用成功,该函数将返回所创建的互斥对象的句柄。如果创建的是命名的互斥对象,并且在调用CreateMutex之前,该命名的互斥对象存在,那么该函数将返回已经存在的这个互斥对象的句柄,而这时调用GetLastError函数将返回ERROR_ALREADY_EXISTS.

函数ReleaseMutex释放指定对象的访问权,原型如下

BOOLReleaseMutex(HANDE hMutex);

线程必须主动请求该共享对象的使用权才有可能获得该所有权,这可以通过调用WaitFofSingleObject函数来实现,原型如下

DWORDWaitFofSingleObject(HANDE hHandle, DWORD dwMilliseconds);

hHandle 所请求的对象句柄,一旦互斥对象处于有信号状态,该函数就返回,否则一直等待。

dwMilliseconds     指定等待的时间间隔,以毫秒为单位,如果该参数为INFINITE,会一直等待。


思考以下问题

1在函数Fun1和Fun2中调用WaitForSingleObject和ReleaseMutex的位置能否放在while循环之外?

2main函数创建互斥对象时,第二参数指定为TRUE,即主线程获得互斥对象的所有权,为了保证子线程正常运行,应怎么办?

3如果main函数创建互斥对象后立即调用WaitForSingleObject,为了保证子线程正常运行,应怎么办?

为了保证应用程序只有一个实例运行,利用CreateMutex的返回值,如果返回一个有效的句柄,接着调用GetLastError函数,如果该函数返回ERROR_ALREADY_EXISTS,就表明已经创建了这个命名的互斥对象。代码如下:

void main()

{

   HANDLE hThread1 =CreateThread(NULL, 0, Func1, NULL, 0, NULL);

   HANDLE hThread2 =CreateThread(NULL, 0, Func2, NULL, 0, NULL);

   CloseHandle(hThread1);

   CloseHandle(hThread2);

   hMutex = CreateMutex(NULL, FALSE,"tickets");

   if(hMutex)

      if(ERROR_ALREADY_EXISTS == GetLastError())

      {

        cout<<"Only one instance can run"<<endl;

        return;

      }

Sleep(4000);

}

2事件对象

事件对象也属于内核对象,它包含以下三个成员:

使用计数

用于指明该事件是一个自动重置的事件还是人工重置的事件的布尔值

用于指明该事件处于已通知状态还是未通知状态的布尔值。

事件对象有两种不同的类型,人工重置的事件对象和自动重置的事件对象。当人工重置的事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程,当自动重置的事件对象得到通知时,等待该事件对象的线程中只有一个线程变为可调度线程。

在程序中使用CreateEvent函数创建或打开一个匿名的或命名的事件对象,原型如下:

HANDLE CreateEvent(LPSECURITY_ATTRIBUTES   lpEventAttributes, BOOL bManualReset, BOOL bInitialState, LPCTSTR lpName);

lpEventAttributes:指向SECURITY_ATTRIBUTES的指针,这里可以传递NULL,则使用默认的安全性。

bManualReset,指定创建的是人工重置对象还是自动重置对象。

bInitialState,指定事件对象的初始状态。

lpName指定事件对象的名称,如果值为NULL,创建一个匿名的对象。

设置事件对象状态BOOL SetEvent(HANDLE  hEvent);

重置事件对象状态 BOOL ResetEvent(HANDLE  hEvent);

为了实现线程的同步,应该使用自动重置对象。

2关键段代码,也称临界区,工作在用户方式下,它是指一个小代码段,在代码能够执行前,他必须独占对某些资源的访问权。

相关函数

voidInitializeCriticalSection(LPCRITICAL_SECTION  lpCriticalSection);

参数lpCriticalSection作为返回值来用.

EnterCriticalSection(LPCRITICAL_SECTION  lpCriticalSection);

LeaveCriticalSection(LPCRITICAL_SECTION  lpCriticalSection);

DeleteCriticalSection(LPCRITICAL_SECTION  lpCriticalSection);

三种同步方式比较:

1互斥对象和事件对象都属于内核对象,利用内核对象进行线程同步时,速度较慢,但可以在多个进程中的各个线程 间进行同步。

2关键段代码工作在用户方式下,同步速度较快,但在使用关键段代码时,很容易进入死锁状态,因为在等待进入关键段代码时无法设定超时值。

 总结自孙鑫《vc++编程详解》

 


展开阅读全文

没有更多推荐了,返回首页