MFC 多线程编程总结

MFC 多线程编程总结

************ 写在前面*************

虽然用过多线程写过一些小软件,但是时间一长就忘记了。所以本着“好记性不如烂笔头”的原则,写这么一篇博客当做复习笔记,看自己写的东西的目录就能想起来。可能会有错误和不足,欢迎大家指正,共同进步。

1、进程与线程

1.1、进程

进程是什么?首先先了解一下程序是什么?
程序是一堆指定的有序集合,本身没有任何运行的含义,是一个静态的实体。而进程是一个程序在一个数据集上的一次执行活动。它是一个动态的实体,有自己的生命周期。创建时产生,调度时运行,等待资源或事件时等待,完成任务时被销毁,这反应了一个进程在一定的数据集运行的全部动态过程。
进程是进行资源申请和调度的基本单位 。

1.2、线程

线程是进程的一个可执行单元,是进程中的一个实体,是进行CPU申请和分配的基本单位。

1.3、进程与线程的区别(常见面试题)

  1. 进程是资源申请和分配的基本单位,线程是CPU申请和调度的基本单位。
  2. 进程和线程是一对多的关系,一个进程至少有一个线程,称为主线程。这个主线程退出,则整个程序也就退出了。
  3. 多个进程之间是逻辑独立的关系,各有各的资源和内存空间。在操作系统正常的情况下,一个进程的崩溃不会影响到另一个进程的运行
  4. (同一个进程内的)多个线程之间是共享该进程的所有资源。
  5. 线程创建和切换较进程所需代价要小。
  6. 多进程程序更加安全,原因是第3点。
  7. 多线程程序的好处?(为什么要引入多线程?)1)提供更好的资源利用率。比如 线程A在用CPU计算,线程B在使用I/O读取文件,这就同时在使用两种资源,如果是单一线程,同一时刻只能使用一种资源;2)更快的响应速度。比如服务器需要监听请求,当请求到达之后,如果处理时间过长,就无法即时监听新的请求。如果是多线程,线程A用来监听请求,然后转发给线程B,之后立即继续监听,线程B只用来处理请求一个线程。这样就保证了服务器能够快速返回监听。就能够有更多的客户端发送请求到服务端,而服务器变得更加及时的响应。

2、线程创建的三种方式

MFC中有三种创建线程的方式,如下:

2.1、CreateThread(需要手动关闭线程句柄)

  1. 函数原型为:
    HANDLE CreateThread(
    LPSECURITY_ATTRIBUTES lpThreadAttributes,
    SIZE_T dwStackSize,
    LPTHREAD_START_ROUTINE lpStartAddress,
    LPVOID lpParameter,
    DWORD dwCreationFlags,
    LPDWORD lpThreadId
    );
  2. 参数说明:
    -参数一参数:线程安全属性,一般默认为NULL
    -参数二参数:线程栈空间的大小,为0时默认为1MB
    -参数三参数:线程函数地址(需要该线程做什么事,就写在此函数中),声明:DWORD WINAPI ThreadFunc(LPVOID pParam)
    -参数四参数:线程函数的参数,指针类型(多个参数可以用结构体或者类打包传递)
    -参数五参数:线程是否立即运行,为0表示线程创建之后立即就可以进行调度,如果为CREATE_SUSPENDED(创建时挂起)则表示线程创建后暂停运行,这样它就无法调度,直到调用ResumeThread()
    -参数六参数:线程ID,创建成功之后该参数返回线程ID,可NULL
    -线程创建成功返回新线程的句柄,失败返回NULL

例子:

#include <iostream>
#include <windows.h>
using namespace std;

DWORD WINAPI ThreadFunc(LPVOID pParam)
{
	cout << "子线程" << endl;
	return 0;
}

int main() 
{
	HANDLE hThread = CreateThread(NULL, 0, ThreadFunc, 0, 0, 0);
	Sleep(2000);
	CloseHandle(hThread);
}

2.2、AfxBeginThread(会自动释放)

MFC提供了两个重载版本的AfxBeginThread,一个用于用户界面线程,一个用于工作者线程。

2.2.1、用户界面线程
  1. 函数原型:
    CWinThread* AfxBeginThread(
    CRuntimeClass* pThreadClass,
    int nPriority,
    UINT nStackSize,
    DWORD dwCreateFlags,
    LPSECURITY_ATTRIBUTES lpSecurityAttrs
    );
  2. 参数说明:
    -参数一参数:从CWinThread派生的RUNTIME_CLASS类
    -参数二参数:线程优先级
    -参数三参数:线程栈空间的大小,为0时默认为1MB
    -参数四参数:线程是否立即运行,为0表示线程创建之后立即就可以进行调度,如果为CREATE_SUSPENDED(创建时挂起)则表示线程创建后暂停运行,这样它就无法调度,直到调用ResumeThread()
    -参数五参数:线程安全属性,一般默认为NULL
    -线程创建成功返回CWinThread*,失败返回NULL
    一般只需要设置第一个参数,后面的四个参数均有默认值。
  3. 界面线程的创建
    -1)从CWinThread类派生自己的子类:假设为CUIThreadApp
    -2)重载CUIThreadApp的InitInstance(必须重载)与ExitInstance(可选重载)函数
    -3)在InitInstance函数中进行界面的创建
    -4)调用AfxBeginThread函数开始界面线程:AfBeginThread(RUNTIME_CLASS(CUIThreadApp))
2.2.2、工作者线程线程
  1. 函数原型:
    CWinThread* AfxBeginThread(
    AFX_THREADPROC pfnThreadProc,
    LPVOID pParam,
    int nPriority,
    UINT nStackSize,
    DWORD dwCreateFlags,
    LPSECURITY_ATTRIBUTES lpSecurityAttrs
    );

  2. 参数说明:
    -参数一参数:线程函数地址(需要该线程做什么事,就写在此函数中),声明如:UINT ThreadProc( LPVOID pParam );
    -参数二参数:线程函数的参数,指针类型(多个参数可以用结构体或者类打包传递)
    -参数三参数:线程优先级
    -参数四参数:线程栈空间的大小,为0时默认为1MB
    -参数五参数:线程是否立即运行,为0表示线程创建之后立即就可以进行调度,如果为CREATE_SUSPENDED(创建时挂起)则表示线程创建后暂停运行,这样它就无法调度,直到调用ResumeThread()
    -参数六参数:线程安全属性,一般默认为NULL
    -线程创建成功返回CWinThread*,失败返回NULL
    一般只需要设置前两个参数,后面的四个参数均有默认值。前两个参数中,第一个是线程函数的指针,第二个是传递给这个函数的参数。实际中我们经常这样用

    AfxBeginThread(ThreadProc,this);

把this传过去,线程函数就可以使用和操作类的成员了。千万要注意线程函数是静态类函数成员。

例子:

UINT ThreadProc(LPVOID pParam)
{
	int tipMsg = (int)pParam;
	CString str;
	str.Format(Text("%d"), tipMsg);
	AfxMessageBox(str);
	return 0;
}
void CThreadTestDlg::OnBnCliekedBtn()
{
	CWinThread* pThread = AfxBeginThread(ThreadProc, (LPVOID)123);
}
2.2.3、用户界面线程与工作者线程线程的区别

二者的主要区别在于工作者线程没有消息循环,而用户界面线程有自己的消息队列和消息循环
工作者没有消息机制,通常用来执行后台计算和维护任务,如冗长的计算过程,打印机的打印等。用户界面线程一般用于处理独立于其他线程执行之外的用户输入、响应用户及系统所产生的事件和消息等。但对于Win32的API编程而言,这两种线程是没有区别的,它们都只需要线程的启动地址即可启动线程来执行任务。

2.3、_beginthreadex(需要手动释放)

  1. 函数原型:
    unsigned long _beginthreadex(
    void* security,
    unsigned stack_size,
    unsigned ( __stdcall start_address )( void * ),
    void
    arglist,
    unsigned initflag,
    unsigned* thrdaddr
    );
  2. 参数说明:
    -第一个参数:安全属性,NULL为默认安全属性
    -第二个参数:指定线程堆栈的大小。如果为0,则线程堆栈大小和创建它的线程的相同。一般用0
    -第三个参数:指定线程函数的地址,也就是线程调用执行的函数地址(用函数名称即可,函数名称就表示地址)
    -第四个参数:传递给线程的参数的指针,可以通过传入对象的指针,在线程函数中再转化为对应类的指针
    -第五个参数:线程初始状态,0:立即运行;CREATE_SUSPEND:suspended(悬挂)
    -第六个参数:用于记录线程ID的地址

例子:

#include <iostream>
#include <windows.h>
#include <process.h>
using namespace std;

struct agrclist {
	char *data;
	int count;
};

unsigned __stdcall DoTest(void *mArgclist) {

	agrclist *pagrclist;
	pagrclist = (struct agrclist *)mArgclist;
	cout << pagrclist->data;
	cout << pagrclist->count;
	_endthreadex(0);
	return 0;
}
void main(void) {
	char buf[128];
	unsigned int threadid;
	agrclist magrclist, *pmagrclist;
	pmagrclist = &magrclist;
	pmagrclist->data = buf;
	pmagrclist->count = 1;

	HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, &DoTest, pmagrclist, NULL, &threadid);
	CloseHandle(hThread);
}

3、线程通信

一般而言,在一个应用程序中(即进程),一个线程往往不是孤立存在的,常常需要和其它线程通信,以完成特定的任务。如主线程和次线程,次线程与次线程,工作线程和用户界面线程等。这样,线程与线程间必定有一个信息传递的渠道。这种线程间的通信不但是难以避免的,而且在多线程编程中也是复杂和频繁的。线程间的通信涉及到4个问题:

(1) 线程间如何传递信息

(2) 线程之间如何同步,以使一个线程的活动不会破坏另一个线程的活动,以保证计算结果的正确合理

(3) 当线程间具有依赖关系时,如何调度多个线程的处理顺序

(4) 如何避免死锁问题

在windows系统中线程间的通信一般采用四种方式:全局变量方式消息传递方式参数传递方式线程同步法。下面分别作介绍。

3.1、全局变量方式

例子:
在.cpp文件中定义一个全局变量,最好使用volatile修饰,它告诉编译器无需对该变量作任何的优化,即无需将它放到一个寄存器中,并且该值可被外部改变。然后线程函数就可以像普通函数一样共同使用这个全局变量。

volatile int gNum = 0;
UINT ThreadWriteProc(LPVOID pParam) 
{
	gNum += 3;
	return 0;
}
UINT ThreadReadProc(LPVOID pParam)
{
	CString str;
	str.Format(TEXT("%d"), gNum);
	AfxMessageBox(str);
	return 0;
}

void CThreadTestDlg::OnBnClickedCreateBtn()
{
	CWinThread* pWriteThread = AfxBeginThread(ThreadWriteProc, NULL);
	CWinThread* pReadThread = AfxBeginThread(ThreadReadProc, NULL);
}

3.2、消息传递方式

例子:
首先在.cpp文件中自定义一个消息,然后给需要发送消息的线程传递另一个接收消息的线程的线程ID,再使用专门的线程传递消息的PostThreadMessage发送消息,

#define WM_MYTHREADMSG WM_USER+5

UINT ThreadWriteProc(LPVOID pParam) 
{
	int num = 3;
	DWORD dwThreadReadID = (DWORD)pParam;
	//使用PostThreadMessage发送自定义消息WM_MYTHREADMSG给另一个线程
	PostThreadMessage(dwThreadReadID, WM_MYTHREADMSG, num, NULL);
	return 0;
}
UINT ThreadReadProc(LPVOID pParam)
{
	MSG msg = { 0 };
	while (GetMessage(&msg,0,0,0))
	{
		switch (msg.message)
		{
		//接收到WM_MYTHREADMSG,执行响应的操作
		case WM_MYTHREADMSG:
		{
			int num = (int)msg.wParam;
			CString str;
			str.Format(TEXT("%d"), num);
			AfxMessageBox(str); 
		}
			break;
		default:
			break;
		}
	}
	return 0;
}

void CThreadTestDlg::OnBnClickedCreateBtn()
{
	CWinThread* pReadThread = AfxBeginThread(ThreadReadProc, NULL);
	CWinThread* pWriteThread = AfxBeginThread(ThreadWriteProc, (LPVOID)pReadThread->m_nThreadID);
}

或者
通过消息映射的方式。
在接受消息的线程中,先定义消息响应函数,如:

afx_msg void OnThreadMessage(WPARAM wParam,LPARAM lParam);

然后在消息映射表中添加该消息的映射,如:

BEGIN_MESSAGE_MAP(CThreadTestDlg, CDialog)
ON_THREAD_MESSAGE(WM_THREADMSG,OnThreadMessage)
END_MESSAGE_MAP()

最后,实现该消息函数,如:

//显示消息处理函数

void XXXXThread::OnThreadMessage(WPARAM wParam,LPARAM lParam)

{

  ;//消息处理

}
注意事项:

线程消息的映射方式是:

ON_THREAD_MESSAGE(WM_PROGRESS, &CXXXdlg::OnXXX)

和一般的窗口消息映射不同,不要搞错了。

3.3、参数传递方式

参数传递是线程通信的官方标准方法,多数情况下,主线程创建子线程并让其子线程为其完成特定的任务,主线程在创建子线程时,可以通过传给线程函数的参数和其通信,所传递的参数是一个32位的指针,该指针不但可以指向简单的数据,而且可以指向结构体或类等复杂的抽象数据类型。
最常见的方法:AfxBeginThread(ThreadProc,this);这样该线程就可以操作该主对话框类的成员变量。
或者可以自定义一个结构体或类:
struct/class MyData
{
//变量
}
然后
MyData mydata;
AfxBeginThread(ThreadProc, &mydata);

3.4、线程同步方式

线程同步就是指:例如有两个线程,线程A写入数据,线程B读出线程A准备好的数据并进行一些操作。这种情况下,只有当线程A写好数据后线程B才能读出,只有线程B读出数据后线程A才能继续写入数据。这两个线程之间需要同步进行通信。关于线程同步的方法和控制是编写多线程的核心和难点,将放在后面细致讲解。

4、线程同步

为什么要使用线程同步?假设多个线程访问同一全局变量,如果都是读操作,则不会出现问题;如果有一个线程对全局变量进行修改,则其他线程无法保证读取的全局变量是修改过得值,这种情况在操作系统中称为“读脏数据”。
为了确保读线程读取到的是正确(修改后)的值,就必须在修改变量时禁止其他线程任何访问,直至修改结束后再解除对其他线程的访问限制。像这种保证线程能得到其他线程任务处理结束后的正确处理结果而采取的保护措施即为线程同步。
MFC提供了多种同步对象,最常用的有四种:

4.1、临界区(CCriticalSection)

临界区是一段只允许被独占的代码,在任意时刻只允许一个线程进行访问,其他线程访问时会被挂起,直到访问临界区的线程离开。再临界区被释放之后,其他线程可以争抢,以此达到以原子方式操作共享资源的目的。
注意:
在使用临界区时,一般不允许其运行时间过长,因为只要临界区的资源没有释放,其他试图使用临界区的线程都会被挂起而进入等待状态,这会在一定程度上影响程序的运行效率。另外,虽然临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。

在不使用临界区的情况下,举个例子:
创建50个线程,每个线程对同一个全局变量进行100次的加一和减一操作。理论上这个例子的结果应该是0,但事实上结果是个不确定的值。原因在于线程在进行切换的时候并不能保证100次的加一和减一操作都执行完或者都没有执行,有可能在执行了一半就被切换到另一个线程,此时该线程读取的是错误的值。

int gNum = 0;//全局变量,共享资源

UINT ThreadWriteProc(LPVOID pParam) 
{
	for (int i = 0; i < 100; i++)
	{
		gNum++;
		CString str;
		str.Format(TEXT("%d
"), gNum);
		OutputDebugString(str);
		gNum--;
	}
	return 0;
}

void CThreadTestDlg::OnBnClickedCreateBtn()
{
	//创建50个线程
	for (int i = 0; i < 50; i++)
	{
		AfxBeginThread(ThreadWriteProc, NULL);
	}
}

使用方式

1、使用CRITICAL_SECTION结构体创建临界区

先定义一个全局CRITICAL_SECTION结构对象保护共享资源,并分别用EnterCriticalSection()和LeaveCriticalSection()函数去标识和释放一个临界区。所用到的CRITICAL_SECTION结构对象必须经过InitializeCriticalSection()的初始化后才能使用。
上面的代码可以改为:

CRITICAL_SECTION gcs;  //全局临界区结构体对象

int gNum = 0; //全局变量,共享资源

UINT ThreadWriteProc(LPVOID pParam) 
{
	EnterCriticalSection(&gcs); //进入临界区
	for (int i = 0; i < 100; i++)
	{
		gNum++;
		CString str;
		str.Format(TEXT("%d
"), gNum);
		OutputDebugString(str);
		gNum--;
	}
	LeaveCriticalSection(&gcs); //离开临界区
	return 0;
}

void CThreadTestDlg::OnBnClickedCreateBtn()
{
	InitializeCriticalSection(&gcs); //初始化临界区结构对象
	//创建50个线程
	for (int i = 0; i < 50; i++)
	{
		AfxBeginThread(ThreadWriteProc, NULL);
	}
}

2、使用CCriticalSection类创建临界区

在MFC中提供了一个CCriticalSection用于线程同步,操作方式是定义一个全局的CCriticalSection对象,然后使用成员函数Lock()和UnLock()标定出需要被独占的代码片段即可。对于上述代码,可通过CCriticalSection类将其改写如下:

CCriticalSection gccs; //全局临界区对象

int gNum = 0; //全局变量,共享资源

UINT ThreadWriteProc(LPVOID pParam) 
{
	gccs.Lock();//进入临界区
	for (int i = 0; i < 100; i++)
	{
		gNum++;
		CString str;
		str.Format(TEXT("%d
"), gNum);
		OutputDebugString(str);
		gNum--;
	}
	gccs.Unlock();//释放,离开临界区
	return 0;
}

void CThreadTestDlg::OnBnClickedCreateBtn()
{
	//创建50个线程
	for (int i = 0; i < 50; i++)
	{
		AfxBeginThread(ThreadWriteProc, NULL);
	}
}

4.2、事件(CEvent)

事件是一个允许一个线程在某种情况发生时,唤醒另外一个线程的同步对象。例如在某些网络应用程序中,一个线程(记为A)负责监听通讯端口,另外一个线程(记为B)负责更新用户数据。线程A可以通过事件通知线程B何时更新数据。
MFC使用CEvent类提供了对事件的支持。每个CEvent 类对象都有两种类型(人工事件和自动事件)和两种状态(有信号状态和无信号状态)。线程监视对应CEvent 类对象的状态,并在相应的时候采取相应的操作。
自动事件:一个自动CEvent 对象在被至少一个线程释放后会自动返回到无信号状态;在创建CEvent 类的对象时,默认创建的是自动事件。
人工事件:人工事件对象获得信号后,释放可利用线程,但直到调用成员函数ReSetEvent()才将其设置为无信号状态。

(1)CEvent 类对象创建:

CEvent(	
		BOOL 					bInitiallyOwn = FALSE,  //信号初试状态,TRUE为有信号状态,FALSE为无信号状态
		BOOL 					bManualReset = FALSE,	//自动事件(FALSE),人工事件(TRUE)
		LPCTSTR 				lpszName = NULL,
		LPSECURITY_ATTRIBUTES 	lpsaAttribute = NULL
);

(2)SetEvent函数:

BOOL CEvent::SetEvent();

此函数将CEvent 类对象的状态设置为有信号状态。如果事件是人工事件,则一直保持为有信号状态,直到手动调用成员函数ResetEvent()将其设为无信号状态时为止。如果CEvent 类对象为自动事件,则在SetEvent()将事件设置为有信号状态后,系统将自动设置CEvent 类对象为无信号状态。

(3)ResetEvent()函数:

BOOL CEvent::ResetEvent();

此函数将 CEvent 类对象的状态设置为无信号状态,并保持该状态直至SetEvent()被调用时为止。由于自动事件是由系统自动重置,故自动事件不需要调用该函数。
(4)WaitForSingleObject()函数:

DWORD WaitForSingleObject(
		HANDLE 	hObject,		//指明一个事件对象的句柄
		DWORD 	dwMilliseconds	//等待时间
); 

如果事件对象处于无信号状态,则该函数使线程进入阻塞状态;如果事件对象处于有信号状态,则该函数立即返回WAIT_OBJECT_0。第二个参数指明了需要等待的时间(毫秒),可以传递INFINITE指明要无限期等待下去,如果第二个参数为0,那么函数就测试同步对象的状态并立即返回。如果等待超时,该函数返回WAIT_TIMEOUT。如果该函数失败,返回WAIT_FAILED。

(5)WaitForMultiplyObject()函数:

DWORD WaitForMultipleObjects(
		DWORD 			dwCount, //等待的事件对象个数
		CONST HANDLE* 	phObjects, //一个存放被等待的事件对象句柄的数组
		BOOL 			bWaitAll, //是否等到所有事件对象为有信号状态后才返回
		DWORD 			dwMilliseconds//等待时间
); 

该函数的第一个参数指明等待的事件对象的个数,可以是WAIT_OBJECT_0到WAIT_OBJECT_0 + nCount-1中的一个值。phObjects参数是一个存放等待的事件对象句柄的数组。bWaitAll参数如果为TRUE,则只有当等待的所有事件对象为有信号状态时函数才返回,如果为FALSE,则只要一个事件对象为有信号状态,则该函数返回。第四个参数和WaitForSingleObject中的dwMilliseconds参数类似。
该函数失败,返回WAIT_FAILED;如果超时,返回WAIT_TIMEOUT;如果bWaitAll参数为TRUE,函数成功则返回WAIT_OBJECT_0,如果bWaitAll为FALSE,函数成功则返回值指明是哪个内核对象收到通知。

问题:当调用SetEvent()函数将自动事件设置为有信号状态以后,为什么系统又要将其设置为无信号状态???
原因是:WaitForSingleObject()函数监视事件状态时,如果事件处于无信号状态,则该函数导致线程进入阻塞状态(一段时间或者一直阻塞,取决于参数设置),只有当事件处于有信号状态时,才会返回并继续执行。这就需要调用SetEvent()。系统又将自动对象设置为无信号状态,是因为自动对象一次只能启动一个处于等待状态的线程,因此在调用SetEvent()将自动对象设置为有信号状态后,被阻塞的线程中的第一个线程被启动,然后系统将自动对象又设置为无信号状态,是其他线程继续阻塞。

使用例子:

CEvent ce;//定义全局自动事件对象,方便各个线程使用

int gNum = 0; //全局变量,共享资源

UINT ThreadWriteProc1(LPVOID pParam) 
{
	gNum += 10;
	CString str;
	str.Format(TEXT("%d
"), gNum);
	OutputDebugString(str);

	ce.SetEvent();//
	return 0;
}

UINT ThreadWriteProc2(LPVOID pParam)
{
	WaitForSingleObject(ce.m_hObject, INFINITE);
	for (int i = 0; i < 100; i++)
	{
		gNum++;
		CString str;
		str.Format(TEXT("%d
"), gNum);
		OutputDebugString(str);
		gNum--;
	}
	return 0;
}

void CThreadTestDlg::OnBnClickedCreateBtn()
{
	AfxBeginThread(ThreadWriteProc2, NULL);

	AfxBeginThread(ThreadWriteProc1, NULL);
	
}

因为ce对象是自动事件且初始状态为无信号状态,所以ThreadWriteProc2在执行到WaitForSingleObject时阻塞,直到ThreadWriteProc1执行到ce.SetEvent()将事件ce设置为有信号时该线程才往下执行。因为ce对象是自动事件,则当WaitForSingleObject()返回时,系统自动把ce对象重置为无信号状态。

4.3、互斥对象(CMutex)

互斥(Mutex)是一种用途非常广泛的内核对象,能够保证多个线程对同一共享资源的互斥访问。类似于临界区,只有拥有互斥对象的线程才具有访问资源的权限,由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线程所访问。当占据资源的线程在任务处理完后将拥有的互斥对象交出,其他线程才能访问资源。与临界区的区别在于:互斥对象可以在进程间使用,而临界区对象只能在同一进程的各线程间使用。当然,互斥对象也可以用于同一进程的各个线程间,但是在这种情况下,使用临界区会更节省系统资源,更有效率。

以互斥对象来保持线程同步可能用到的函数主要有CreateMutex()、OpenMutex()、ReleaseMutex()、WaitForSingleObject()和WaitForMultipleObjects()等。在使用互斥对象前,首先要通过CreateMutex()或OpenMutex()创建或打开一个互斥对象。

CreateMutex()函数原型为:

HANDLE CreateMutex(
		LPSECURITY_ATTRIBUTES 	lpMutexAttributes, // 安全属性指针
		BOOL 					bInitialOwner, // 初始状态(FALSE为空闲状态)
		LPCTSTR 				lpName // 互斥对象名
);

如果在创建互斥对象时指定了对象名,那么可以在本进程其他地方或是在其他进程通过OpenMutex()函数得到此互斥对象的句柄。OpenMutex()函数原型为:

HANDLE OpenMutex(
		DWORD 		dwDesiredAccess, // 访问标志
		BOOL 		bInheritHandle, // 继承标志
		LPCTSTR 	lpName // 互斥对象名
);

当对资源具有访问权的线程不再需要访问此资源而要离开时,必须通过ReleaseMutex()函数来释放其拥有的互斥对象,其函数原型为:

BOOL ReleaseMutex(HANDLE hMutex);

WaitForSingleObject()和WaitForMultipleObjects()等待函数在互斥对象保持线程同步中所起的作用与在其他内核对象中的作用是基本一致的。但唯一的区别是返回值改变了,对WaitForSingleObject(),由原来的WAIT_OBJECT_0变为WAIT_ABANDONED_0;对WaitForMultipleObjects(),由原来的WAIT_OBJECT_0到WAIT_OBJECT_0 + nCount-1中一个值变为WAIT_ABANDONED_0到WAIT_ABANDONED_0+ nCount-1。
除此之外,使用互斥对象的方法在等待线程的可调度性上同使用其他几种内核对象的方法也有所不同,其他内核对象在没有得到通知时,受调用等待函数的作用,线程将会挂起,同时失去可调度性,而使用互斥的方法却可以在等待的同时仍具有可调度性,这也正是互斥对象所能完成的非常规操作之一。
使用例子:

HANDLE hMutex = NULL;;// 定义全局互斥对象,方便各个线程使用

int gNum = 0; //全局变量,共享资源

UINT ThreadWriteProc1(LPVOID pParam) 
{
	WaitForSingleObject(hMutex, INFINITE);
	gNum += 10;
	CString str;
	str.Format(TEXT("%d
"), gNum);
	OutputDebugString(str);

	ReleaseMutex(hMutex);
	return 0;
}

UINT ThreadWriteProc2(LPVOID pParam)
{
	WaitForSingleObject(hMutex, INFINITE);
	for (int i = 0; i < 100; i++)
	{
		gNum++;
		CString str;
		str.Format(TEXT("%d
"), gNum);
		OutputDebugString(str);
		gNum--;
	}
	ReleaseMutex(hMutex);
	return 0;
}

void CThreadTestDlg::OnBnClickedCreateBtn()
{
	hMutex = CreateMutex(NULL, FALSE, NULL);// 创建互斥对象
	
	AfxBeginThread(ThreadWriteProc2, NULL);
	AfxBeginThread(ThreadWriteProc1, NULL);
	
}

在MFC中通过CMutex类对互斥提供支持。使用CMutex类的方法非常简单,和临界量类似。CMutex类也是只含有构造函数这唯一的成员函数,当完成对互斥对象保护资源的访问后,可通过调用从父类CSyncObject继承的UnLock()函数完成对互斥对象的释放。CMutex类构造函数原型为:

CMutex(
BOOL 						bInitiallyOwn = FALSE,//初始状态
LPCTSTR 					lpszName = NULL,//互斥对象名
LPSECURITY_ATTRIBUTES 		lpsaAttribute = NULL //安全属性
);

使用例子:

CMutex cm; //全局互斥对象

int gNum = 0; //全局变量,共享资源

UINT ThreadWriteProc(LPVOID pParam)
{
	cm.Lock();
	for (int i = 0; i < 100; i++)
	{
		gNum++;
		CString str;
		str.Format(TEXT("%d
"), gNum);
		OutputDebugString(str);
		gNum--;
	}
	cm.Unlock();//释放
	return 0;
}

void CThreadTestDlg::OnBnClickedCreateBtn()
{
	//创建50个线程
	for (int i = 0; i < 50; i++)
	{
		AfxBeginThread(ThreadWriteProc, NULL);
	}
}

4.4、信号量(CSemaphore)

信号量(Semaphore)对线程的同步方式与前面几种方法不同,它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。在用CreateSemaphore()创建信号量时就需要同时指出允许可访问的最大资源计数和当前可访问资源计数。一般是将当前可访问资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可访问资源计数就会减1,只要当前可访问资源计数是大于0的,就可以发出信号量信号。但是当前可用计数减小到0时则说明当前占用资源的线程数已经达到了所允许的最大数目,不能在允许其他线程的进入,此时的信号量信号将无法发出。线程在处理完共享资源后,应在离开的同时使用ReleaseSemaphore()函数将可访问资源+1。在任何时候当前可访问资源计数决不可能大于最大资源计数。

利用信号量做线程同步需要的函数有:CreateSemaphore()、OpenSemaphore()、ReleaseSemaphore()、WaitForSingleObject()和WaitForMultipleObjects()等函数。其中,CreateSemaphore()用来创建一个信号量内核对象,其函数原型为:

HANDLE CreateSemaphore(
	LPSECURITY_ATTRIBUTES 	lpSemaphoreAttributes, // 安全属性指针
	LONG 					lInitialCount,   // 当前可用计数
	LONG 					lMaximumCount,   // 最大计数
	LPCTSTR 				lpName    	 // 对象名指针
);  

lpName参数可以为创建的信号量定义一个名字,在其他进程中可以通过该名字而得到此信号量。OpenSemaphore()函数即可用来根据信号量名打开在其他进程中创建的信号量,函数原型如下:

HANDLE OpenSemaphore(
	DWORD 		dwDesiredAccess,   // 访问标志
	BOOL 		bInheritHandle,    // 继承标志
	LPCTSTR 	lpName        // 信号量名
); 

在线程离开对资源的处理时,必须通过ReleaseSemaphore()来增加当前可访问资源计数。否则将会出现当前正在处理共享资源的实际线程数并没有达到要限制的数值,而其他线程却因为当前可用资源计数为0而仍无法进入的情况。ReleaseSemaphore()的函数原型为:

BOOL ReleaseSemaphore(
	HANDLE 		hSemaphore,    // 信号量句柄
	LONG 		lReleaseCount,   // 计数递增数量
	LPLONG 		lpPreviousCount  // 先前计数
);

该函数将lReleaseCount中的值添加给信号量的当前资源计数,一般将lReleaseCount设置为1,如果需要也可以设置其他的值。WaitForSingleObject()和WaitForMultipleObjects()主要用在试图进入共享资源的线程函数入口处,主要用来判断信号量的当前可用资源计数是否允许本线程的进入。只有在当前可用资源计数值大于0时,被监视的信号量内核对象才会得到通知。
信号量的使用特点使其更适用于对Socket(套接字)程序中线程的同步。例如,网络上的HTTP服务器要对同一时间内访问同一页面的用户数加以限制,这时可以为没一个用户对服务器的页面请求设置一个线程,而页面则是待保护的共享资源,通过使用信号量对线程的同步作用可以确保在任一时刻无论有多少用户对某一页面进行访问,只有不大于设定的最大用户数目的线程能够进行访问,而其他的访问企图则被挂起,只有在有用户退出对此页面的访问后才有可能进入。
例子:

HANDLE hSemaphore = NULL; // 定义全局信号量对象句柄,方便各个线程使用

int gNum = 0; //全局变量,共享资源

UINT ThreadWriteProc1(LPVOID pParam) 
{
	WaitForSingleObject(hSemaphore, INFINITE);
	gNum += 10;
	CString str;
	str.Format(TEXT("%d
"), gNum);
	OutputDebugString(str);

	ReleaseSemaphore(hSemaphore, 1, NULL);
	return 0;
}

UINT ThreadWriteProc2(LPVOID pParam)
{
	WaitForSingleObject(hSemaphore, INFINITE);
	for (int i = 0; i < 100; i++)
	{
		gNum++;
		CString str;
		str.Format(TEXT("%d
"), gNum);
		OutputDebugString(str);
		gNum--;
	}
	ReleaseSemaphore(hSemaphore, 1, NULL);
	return 0;
}

void CThreadTestDlg::OnBnClickedCreateBtn()
{
	hSemaphore = CreateSemaphore(NULL, 1, 1, NULL);  // 创建信号量对象
	
	AfxBeginThread(ThreadWriteProc2, NULL);
	AfxBeginThread(ThreadWriteProc1, NULL);	
}

在MFC中,使用CSemaphore类提供对信号量的支持。该类只具有一个构造函数,可以构造一个信号量对象,并对初始资源计数、最大资源计数、对象名和安全属性等进行初始化,其原型如下:

CSemaphore(
		LONG 						lInitialCount = 1,
		LONG 						lMaxCount = 1, 
		LPCTSTR 					pstrName = NULL,
		LPSECURITY_ATTRIBUTES 		lpsaAttributes = NULL 
);

在构造了CSemaphore类对象后,任何一个访问受保护共享资源的线程都必须通过CSemaphore从父类CSyncObject类继承得到的Lock()和UnLock()成员函数来访问或释放CSemaphore对象。与前面介绍的几种通过MFC类保持线程同步的方法类似:

CSemaphore gcs; //全局信号量对象

int gNum = 0; //全局变量,共享资源

UINT ThreadWriteProc(LPVOID pParam) 
{
	gcs.Lock();
	for (int i = 0; i < 100; i++)
	{
		gNum++;
		CString str;
		str.Format(TEXT("%d
"), gNum);
		OutputDebugString(str);
		gNum--;
	}
	gcs.Unlock();
	return 0;
}

void CThreadTestDlg::OnBnClickedCreateBtn()
{
	//创建50个线程
	for (int i = 0; i < 50; i++)
	{
		AfxBeginThread(ThreadWriteProc, NULL);
	}
}

4.5、四种同步方式比较

  1. 临界区(Critical Section) 使用临界区域的第一个忠告就是不要长时间锁住一份资源。这里的长时间是相对的,视不同程序而定。对一些控制软件来说,可能是数毫秒,但是对另外一些程序来说,可以长达数分钟。但进入临界区后必须尽快地离开,释放资源。如果不释放的话,会如何?答案是不会怎样,但是,如果是主线程(UI 线程)要进入一个没有被释放的临界区,程序就会崩掉!【 临界区域的一个缺点】:不是一个核心对象,无法获知进入临界区的线程是生是死,如果进入临界区的线程挂了,没有释放临界资源,系统也无法获知,而且没有办法释放该临界资源。这个缺点在互斥(Mutex)中得到了弥补。
  2. Mutex 互斥量与临界区区别:
    -1. 互斥量与临界区类似
    -2. 互斥量是可以命名的,也就是说它可以跨越进程使用。创建互斥量需要的资源更多,互斥量所花费的时间比临界区多的多,但是互斥量是核心对象(事件Event、信号量Semaphore也是),可以跨进程使用,而且等待一个被锁住的互斥量可以设定 TIMEOUT,不会像临界区那样无法得知临界区域的情况,而一直死等。所以如果只是在进程内部各线程间使用的话,临界区会更有效率,更少的资源占用量。
    -3. 如果一个拥有互斥量的线程在返回之前没有调用ReleaseMutex(), 那么这个 互斥量就被舍弃了, 但是当其他线程等待(WaitForSingleObject 等)这个互斥量时,仍能返回,并得到一个 WAIT_ABANDONED_0 返回值。能够知道一个互斥量被舍弃是 Mutex 特有的
  3. 互斥量(Mutex) ,信号灯(Semaphore) ,事件(Event)都可以被跨越进程使用来进行同步数据操作。
  4. Event 用来同步线程是最具弹性的。一个事件有两种状态:有信号状态和无信号状态。事件又分两种类型:人工(手动)事件和自动事件,人工事件被设置为有信号状态后,会唤醒所有等待的线程,而且一直保持为有信号状态,直到调用ResetEvent()函数将其设置为无信号状态为止。自动事件被设置为有信号状态后,会唤醒“第一个”等待中的线程,然后自动恢复为无信号状态。所以用自动事件来同步两个线程比较理想。

参考博客:

  1. 进程,线程,程序的区别和联系
  2. 进程与线程的区别(面试题)
  3. MFC中的线程同步
  4. MFC多线程编程之四——线程的同步
  5. 温故而知新之多线程同步互斥对像:临界区(CriticalSection),互斥量(Mutex),信号量(Semphore)与事件(Handle)
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Winsock 是 Windows 操作系统提供的用于进行网络编程的 API,而 MFC 是基于 Win32 API 的一个 C++ 类库,用于快速开发 Windows 程序。在 MFC使用 Winsock 进行多线程编程,可以实现网络通信的并发处理。 下面是一个使用 MFC 和 Winsock 实现的简单的多线程网络程序的示例: ```cpp // MySocket.h class CMySocket : public CSocket { public: CMySocket(); virtual ~CMySocket(); void SetParent(CWnd* pParentWnd); void SetPort(UINT nPort); void SetIPAddress(LPCTSTR lpszIPAddress); protected: virtual void OnReceive(int nErrorCode); private: CWnd* m_pParentWnd; UINT m_nPort; CString m_strIPAddress; }; // MySocket.cpp CMySocket::CMySocket() { m_pParentWnd = NULL; m_nPort = 0; m_strIPAddress.Empty(); } CMySocket::~CMySocket() { } void CMySocket::SetParent(CWnd* pParentWnd) { m_pParentWnd = pParentWnd; } void CMySocket::SetPort(UINT nPort) { m_nPort = nPort; } void CMySocket::SetIPAddress(LPCTSTR lpszIPAddress) { m_strIPAddress = lpszIPAddress; } void CMySocket::OnReceive(int nErrorCode) { if (nErrorCode == 0) { char szBuffer[1024]; int nBytes = Receive(szBuffer, sizeof(szBuffer)); if (nBytes > 0) { // 处理接收到的数据 CString strData(szBuffer, nBytes); m_pParentWnd->SendMessage(WM_MY_SOCKET_RECEIVE, (WPARAM)this, (LPARAM)&strData); } } CSocket::OnReceive(nErrorCode); } // MyThread.h class CMyThread : public CWinThread { public: CMyThread(); virtual ~CMyThread(); void SetParent(CWnd* pParentWnd); void SetPort(UINT nPort); protected: virtual BOOL InitInstance(); virtual int ExitInstance(); private: CWnd* m_pParentWnd; UINT m_nPort; }; // MyThread.cpp CMyThread::CMyThread() { m_pParentWnd = NULL; m_nPort = 0; } CMyThread::~CMyThread() { } void CMyThread::SetParent(CWnd* pParentWnd) { m_pParentWnd = pParentWnd; } void CMyThread::SetPort(UINT nPort) { m_nPort = nPort; } BOOL CMyThread::InitInstance() { // 创建服务器端套接字 CMySocket socketServer; socketServer.SetParent(m_pParentWnd); socketServer.Create(m_nPort); socketServer.Listen(); while (TRUE) { // 等待客户端连接 CMySocket* pSocketClient = new CMySocket; pSocketClient->SetParent(m_pParentWnd); socketServer.Accept(*pSocketClient); // 创建客户端线程 CMyThread* pThreadClient = new CMyThread; pThreadClient->SetParent(m_pParentWnd); pThreadClient->SetPort(0); pThreadClient->m_bAutoDelete = TRUE; pThreadClient->CreateThread(); } return TRUE; } int CMyThread::ExitInstance() { return CWinThread::ExitInstance(); } // MyDialog.h class CMyDialog : public CDialog { public: CMyDialog(CWnd* pParent = NULL); virtual ~CMyDialog(); enum { IDD = IDD_MY_DIALOG }; protected: virtual void DoDataExchange(CDataExchange* pDX); virtual BOOL OnInitDialog(); afx_msg void OnBtnStart(); afx_msg void OnBtnStop(); afx_msg LRESULT OnSocketReceive(WPARAM wParam, LPARAM lParam); DECLARE_MESSAGE_MAP() private: CMyThread* m_pThreadServer; CList<CMySocket*, CMySocket*> m_listSocketClient; }; // MyDialog.cpp BEGIN_MESSAGE_MAP(CMyDialog, CDialog) ON_BN_CLICKED(IDC_BTN_START, &CMyDialog::OnBtnStart) ON_BN_CLICKED(IDC_BTN_STOP, &CMyDialog::OnBtnStop) ON_MESSAGE(WM_MY_SOCKET_RECEIVE, &CMyDialog::OnSocketReceive) END_MESSAGE_MAP() CMyDialog::CMyDialog(CWnd* pParent /*=NULL*/) : CDialog(CMyDialog::IDD, pParent) { m_pThreadServer = NULL; } CMyDialog::~CMyDialog() { } void CMyDialog::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); } BOOL CMyDialog::OnInitDialog() { CDialog::OnInitDialog(); return TRUE; } void CMyDialog::OnBtnStart() { // 创建服务器线程 m_pThreadServer = new CMyThread; m_pThreadServer->SetParent(this); m_pThreadServer->SetPort(12345); m_pThreadServer->m_bAutoDelete = TRUE; m_pThreadServer->CreateThread(); } void CMyDialog::OnBtnStop() { // 关闭服务器线程和所有客户端套接字 if (m_pThreadServer != NULL) { m_pThreadServer->PostThreadMessage(WM_QUIT, 0, 0); m_pThreadServer = NULL; } POSITION pos = m_listSocketClient.GetHeadPosition(); while (pos != NULL) { CMySocket* pSocket = m_listSocketClient.GetNext(pos); pSocket->Close(); delete pSocket; } m_listSocketClient.RemoveAll(); } LRESULT CMyDialog::OnSocketReceive(WPARAM wParam, LPARAM lParam) { // 处理客户端套接字的接收事件 CMySocket* pSocket = (CMySocket*)wParam; CString* pstrData = (CString*)lParam; // 发送数据到客户端 pSocket->Send(pstrData->GetBuffer(), pstrData->GetLength()); return 0; } ``` 在上面的示例中,`CMySocket` 类继承自 `CSocket`,重载了 `OnReceive` 方法,处理套接字的接收事件,将接收到的数据发送给主窗口。`CMyThread` 类继承自 `CWinThread`,重载了 `InitInstance` 方法,创建服务器端套接字,并循环接收客户端连接,每次接收到客户端连接时创建一个客户端线程。`CMyDialog` 类继承自 `CDialog`,包含了开始和停止按钮,点击开始按钮创建服务器线程,点击停止按钮关闭服务器线程和所有客户端套接字,同时处理客户端套接字的接收事件,将接收到的数据发送回客户端。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值