使用 C++ 和 MFC 进行多线程编程

Microsoft 基础类库 (MFC) 提供对多线程应用程序的支持。此文章描述什么是进程、什么是线程以及多线程编程的 MFC 方法。

“进程”是应用程序的执行实例。例如,双击“记事本”图标时,将启动运行“记事本”的进程。

“线程”是进程内的执行路径。启动“记事本”时,操作系统创建进程并开始执行该进程的主线程。此线程终止时,进程也终止。启动代码以函数地址的形式将此主线程提供给操作系统。通常是所提供的 main 函数或 WinMain 函数的地址。

如果愿意,可以在应用程序中创建其他线程。要处理后台或维护任务但又不想让用户等待完成这些任务时,您可能希望进行此操作。MFC 应用程序中的所有线程都由 CWinThread 对象表示。大多数情况下,甚至不必显式创建这些对象,而只需调用框架 Helper 函数 AfxBeginThread,该函数将为您创建 CWinThread 对象。

MFC 区分两种类型的线程:用户界面线程和辅助线程。用户界面线程通常用于处理用户输入及响应用户生成的事件和消息。辅助线程通常用于完成不需要用户输入的任务(如重新计算)。Win32 API 不区分线程类型;它只需要了解线程的起始地址以开始执行线程。MFC 处理用户界面线程的方式比较特殊,即通过在用户界面中为事件提供消息泵来进行处理。CWinApp 是用户界面线程对象的一个示例,该对象是从 CWinThread 中导出的,处理用户生成的事件和消息。

应特别注意这一给定情况:可能有不止一个线程需要访问同一对象。文章多线程编程:编程提示描述的技术可以用来避免这些情况下可能会发生的问题。文章多线程编程:如何使用同步类描述如何使用可用的类同步从多个线程访问一个对象。

写入并调试多线程编程实际上是一个复杂棘手的任务,必须确保一次只能有一个线程访问对象。“多线程编程”组中的文章没有讲述多线程编程的基础,只讲述了如何在多线程程序中使用 MFC。Visual C++ 中包含的多线程 MFC 示例阐释了几种多线程“添加功能”和 MFC 不包含的 Win32 API,但只是一些入门知识。

有关操作系统如何处理进程和线程的更多信息,请参见 Platform SDK 中的进程和线程。

有关 MFC 多线程支持的信息,请参见下列文章:

用户界面线程通常用于处理用户输入和响应用户事件,这些行为独立于执行该应用程序其他部分的线程。已经创建并启动主应用程序线程(在 CWinApp 导出的类中提供)。此文章描述创建其他用户界面线程所需的步骤。

创建用户界面线程时,必须首先从 CWinThread 派生类。必须使用 DECLARE_DYNCREATE 和 IMPLEMENT_DYNCREATE 宏声明并实现此类。此类必须重写某些函数,也可以重写其他函数。下表列出了这些函数及其用途。

创建用户界面线程时要重写的函数

函数名称目的
ExitInstance线程终止时执行清除。通常重写。
InitInstance执行线程实例初始化。必须重写。
OnIdle执行线程特定的闲置时间处理。通常不重写。
PreTranslateMessage将消息调度到 TranslateMessageDispatchMessage 之前对其进行筛选。通常不重写。
ProcessWndProcException截获由线程的消息和命令处理程序引发的未处理异常。通常不重写。
Run控制线程的函数。包含消息泵。一般不重写。

MFC 通过参数重载提供两个版本的 AfxBeginThread:一个用于用户界面线程,另一个用于辅助线程。若要启动用户界面线程,请调用 AfxBeginThread,提供下列信息:

  • CWinThread 派生的类的 RUNTIME_CLASS。
  • (可选)所需的优先级级别。默认值为正常优先级。有关可用的优先级级别的更多信息,请参见 Platform SDK 中的 SetThreadPriority。
  • (可选)所需的线程堆栈大小。默认值与创建线程的堆栈大小相同。
  • (可选)CREATE_SUSPENDED,如果希望在挂起状态中创建线程。默认值为 0,即正常启动线程。
  • (可选)所需的安全属性。默认值与父线程具有相同的访问权。有关此安全信息格式的更多信息,请参见 Platform SDK 中的 SECURITY_ATTRIBUTES。

AfxBeginThread 为您完成大部分工作。它创建类的新对象、使用您提供的信息初始化该对象并调用 CWinThread::CreateThread 开始执行线程。在整个过程中进行检查,确保假如创建过程的任何部分出现故障,所有对象都能被正确地解除分配。

辅助线程通常用于处理后台任务,用户不必等待就可以继续使用应用程序。重新计算和后台打印等任务是很好的辅助线程示例。此文章详细介绍了创建辅助线程所需的步骤。主题包括:

创建辅助线程是一个相对较为简单的任务。只需两步即可以使线程运行:实现控制函数和启动线程。不必从 CWinThread 派生类。如果需要特殊版本的 CWinThread,也可以从 CWinThread 派生类,但是大多数简单的辅助线程都不需要。无需修改即可使用 CWinThread

启动线程实现控制函数多线程编程:编程提示一文。

AfxBeginThread 有两个重载版本:一个用于用户界面线程,一个用于辅助线程。若要开始执行辅助线程,请调用 AfxBeginThread 提供下列信息:

  • 控制函数的地址。
  • 要传递到控制函数的参数。
  • (可选)所需的线程优先级。默认值为正常优先级。有关可用的优先级级别的更多信息,请参见 Platform SDK 中的 SetThreadPriority。
  • (可选)所需的线程堆栈大小。默认值与创建线程的堆栈大小相同。
  • (可选)CREATE_SUSPENDED,如果希望在挂起状态中创建线程。默认值为 0,即正常启动线程。
  • (可选)所需的安全属性。默认值与父线程具有相同的访问权。有关此安全信息格式的更多信息,请参见 Platform SDK 中的 SECURITY_ATTRIBUTES。

AfxBeginThread 为您创建和初始化 CWinThread 对象、启动该对象并返回其地址,以便以后引用。在整个过程中进行检查,确保假如创建过程的任何部分出现故障,所有对象都能被正确地解除分配。

控制函数定义线程。输入此函数后线程开始,此函数退出时线程终止。此函数的原型应为:

UINT MyControllingFunction( LPVOID pParam );

参数是单个 32 位值。函数在此参数中接收的值是在创建线程对象时传递到构造函数的值。控制函数可以用其选择的任何方式解释此值。可以将其视为标量值,或指向包含多个参数的结构的指针,也可以忽略此值。如果参数引用结构,则既可以使用该结构将数据从调用方传递到线程,也可以用该结构将数据从线程传递回调用方。如果使用此类结构将数据传递回调用方,当结果准备好时,线程将需要修改调用方。有关从辅助线程向调用方进行通信的信息,请参见

函数终止后,应该返回指示终止原因的 UINT 值。一般情况下,此退出代码为 0 指示成功;若为其他值,则指示不同类型的错误。这只依赖于实现。某些线程可以维护对象的使用计数,并返回该对象的当前使用数。若要查看应用程序如何检索此值,请参见多线程编程:终止线程一文。

对在使用 Microsoft 基础类库写入的多线程程序中可以进行的操作有一些限制。有关这些限制的说明以及使用线程的其他提示,请参见多线程编程:编程提示一文。

控制函数示例多线程编程:如何使用同步类设计线程安全类多线程编程:何时使用同步类MTGDIMUTEXES。可以在 Visual C++ 示例中找到这些和其他 MFC 示例程序。

此示例说明如何定义控制函数并从程序的其他部分使用此函数。

UINT MyThreadProc( LPVOID pParam ){    CMyObject* pObject = (CMyObject*)pParam;    if (pObject == NULL ||        !pObject->IsKindOf(RUNTIME_CLASS(CMyObject)))    return 1;   // if pObject is not valid    // do something with 'pObject'    return 0;   // thread completed successfully}// inside a different function in the program...pNewObject = new CMyObject;AfxBeginThread(MyThreadProc, pNewObject);...
     
     

写入多线程应用程序时,线程间的同步资源访问是一个常见问题。两个或多个线程同时访问同一数据会导致不合需要的、不可预知的结果。例如,一个线程可能正在更新结构的内容,而另一个线程正在读取同一结构的内容。无法得知读取线程将会收到何种数据:旧数据、新写入的数据或两种数据都有。MFC 提供了多个同步和同步访问类以帮助解决此问题。此文章解释了可用的类以及如何在典型的多线程应用程序中使用它们创建线程安全类。

典型的多线程应用程序具有代表各个线程间要共享的资源的类。正确设计的完全线程安全类不需要调用任何同步函数。该类的任何事情都在内部处理,使您可以将精力集中于如何更好地使用类,而不是它如何会损坏。创建完全线程安全类的最佳技术是将同步类合并到资源类中。将同步类合并到共享类是一个简单的过程。

以维护链接的帐户列表的应用程序为例。此应用程序允许在独立的窗口中最多检查三个帐户,但是在任何特定的时间,只能更新一个帐户。更新帐户后,通过网络将更新的数据发送到数据存档。

此示例应用程序使用所有这三种类型的同步类。因为它允许一次最多检查三个帐户,因此使用 CSemaphore 限制对三个视图对象的访问。当试图查看第四个帐户时,应用程序或者等到前三个窗口中有一个关闭,或者该尝试失败。更新帐户时,应用程序使用 CCriticalSection 确保一次只更新一个帐户。更新成功后,发出信号 CEvent 以释放等待该事件信号发送的线程。此线程将新数据发送到数据存档。

若要使类完全线程安全,首先将适当的同步类作为数据成员添加到共享类中。在前面的帐户管理示例中,将 CSemaphore 数据成员添加到视图类,将 CCriticalSection 数据成员添加到链接的列表类,将 CEvent 数据成员添加到数据存储类。

下一步,将同步调用添加到修改类中的数据或访问受控资源的所有成员函数中。应该在每个函数中创建 CSingleLock 或 CMultiLock 对象,并调用对象的 Lock 函数。当锁定对象超出范围并被销毁时,该对象的析构函数调用 Unlock 以释放资源。当然,如果愿意,可直接调用 Unlock

用这种方式设计线程安全类使得在多线程应用程序中使用该类与使用非线程安全类一样容易,但却具有完全安全性。将同步对象和同步访问权对象封装到资源的类将提供完全线程安全编程的所有优点,而不会有维护同步代码的缺点。

以下代码示例通过使用在共享资源类和 CSingleLock 对象中声明的数据成员 m_CritSectionCCriticalSection 类型),对此方法进行了说明。通过使用 m_CritSection 对象的地址创建 CSingleLock 对象,来试图同步共享资源(从 CWinThread 派生)。试图锁定资源,一旦锁定,即完成了共享对象上的工作。一旦完成工作,即调用 Unlock 取消锁定资源。

CSingleLock singleLock(&m_CritSection);singleLock.Lock();// resource locked//.usage of shared resource...singleLock.Unlock();
注意   与其他 MFC 同步类不同, CCriticalSection 没有计时锁定请求选项。等待释放线程的时间是无限的。

此方法的缺点是类将要比没有添加同步对象的相同类慢一些。而且,如果有一个以上的线程可能删除对象,合并方法不一定总是有效。在这种情况下,最好维持单独同步对象。

 

 

MFC 提供的六种多线程类分为两类:同步对象(CSyncObject、CSemaphore、CMutex、CCriticalSection 和 CEvent)和同步访问对象(CMultiLock 和 CSingleLock)。

当必须控制对资源的访问以确保资源的完整性时,使用同步类。同步访问类用于获取对这些资源的访问权。此文章描述各个类的使用时间。

若要确定应使用的同步类,请询问以下一系列问题:

  1. 应用程序必须等到发生某事才能访问资源(例如,在将数据写入文件之前,必须先从通信端口接收它)吗?

    如果是,则使用 CEvent

  2. 同一应用程序内一个以上的线程可以同时访问此资源(例如,应用程序允许在同一文档上最多同时打开五个带有视图的窗口)吗?

    如果是,请使用 CSemaphore

  3. 可以有一个以上的应用程序使用此资源(例如,资源在 DLL 中)吗?

    如果是,请使用 CMutex

    如果不是,请使用 CCriticalSection

从不直接使用 CSyncObject。它是其他四个同步类的基类。

示例 1:使用三个同步类

以维护链接的帐户列表的应用程序为例。此应用程序允许在独立的窗口中最多检查三个帐户,但是在任何特定的时间,只能更新一个帐户。更新帐户后,通过网络将更新的数据发送到数据存档。

此示例应用程序使用所有这三种类型的同步类。因为它一次最多允许检查三个帐户,因此使用 CSemaphore 限制对三个视图对象的访问。当试图查看第四个帐户时,应用程序或者等到前三个窗口中有一个关闭,或者该尝试失败。更新帐户时,应用程序使用 CCriticalSection 确保一次只更新一个帐户。更新成功后,发出信号 CEvent 以释放等待该事件信号发送的线程。此线程将新数据发送到数据存档。

示例 2:使用同步访问类

选择要使用的同步访问类更为简单。如果应用程序只与访问单个受控资源有关,请使用 CSingleLock。如果需要访问多个受控资源中的任何一个,则使用 CMultiLock。在示例 1 中,应该使用 CSingleLock,因为在每种情况下,任何特定时间都只需要一个资源。

有关使用同步类的示例代码,请参见 MFC 示例程序

有关如何使用同步类的信息,请参见多线程编程:如何使用同步类一文。有关同步的更多信息,请参见 Platform SDK 中的同步。有关 MFC 中的多线程支持的更多信息,请参见使用 C++ 和 MFC 进行多线程编程一文。

 

导致线程终止的两种正常情况是:控制函数退出或不允许线程完成运行。如果字处理器使用后台打印线程,若成功完成打印,则控制函数将正常终止。但是,如果用户要取消打印,则必须过早终止后台打印线程。此文章解释了如何实现每种情况以及在终止后如何获取退出代码。

正常线程终止过早的线程终止检索线程的退出代码多线程编程:编程提示从多线程访问对象

对于辅助线程,正常线程终止很简单:退出控制函数并返回表示终止原因的值。可以使用 AfxEndThread 函数或 return 语句。一般情况下,0 表示成功完成,但这取决于您自己。

对于用户界面线程,该过程也很简单:从用户界面线程内调用 Platform SDK 中的 PostQuitMessage。PostQuitMessage 采用的唯一参数是线程的退出代码。对于辅助线程,0 通常表示成功完成。

过早终止线程几乎一样简单:从线程内调用 AfxEndThread。将所需的退出代码作为唯一参数传递。这将停止执行线程、解除对线程堆栈的分配、分离附加到线程的所有 DLL 并从内存中删除线程对象。

必须从要终止的线程内调用 AfxEndThread。如果要从其他线程终止线程,必须设置两个线程间的通信方法。

若要获取辅助线程或用户界面线程的退出代码,请调用 GetExitCodeThread 函数。有关此函数的信息,请参见 Platform SDK。此函数获取线程(存储在 CWinThread 对象的 m_hThread 数据成员中)的句柄和 DWORD 的地址。

如果线程仍然是活动的,GetExitCodeThread 会将 STILL_ACTIVE 放在提供的 DWORD 地址中;否则将退出代码放在此地址中。

检索 CWinThread 对象的退出代码还需要一步。默认情况下,当 CWinThread 线程终止时,删除该线程对象。这意味着不能访问 m_hThread 数据成员,因为 CWinThread 对象不再存在。若要避免此情况,请执行以下两个操作之一:

  • m_bAutoDelete 数据成员设置为 FALSE。这使 CWinThread 对象在线程终止后仍可以继续存在。然后可以在线程终止后,访问 m_hThread 数据成员。但是如果使用此技术,您有责任销毁 CWinThread 对象,因为框架不会自动为您删除该对象。这是首选方法。

    - 或 -

  • 单独存储线程的句柄。创建线程后,(使用 ::DuplicateHandle)将其 m_hThread 数据成员复制到其他变量,并通过该变量访问该成员。这样,终止后即可以自动删除对象,并且仍然可以查出线程终止的原因。请注意:在可以复制句柄之前,线程不终止。执行此操作的最安全的方式是将 CREATE_SUSPENDED 传递到 AfxBeginThread,存储句柄,然后通过调用 ResumeThread 继续执行线程。

任一方法都可以使您确定 CWinThread 对象终止的原因。

 

访问数据时,使用多线程应用程序比使用单线程应用程序要更加小心。因为在多线程应用程序中同时有多个独立的执行路径正在使用,算法或数据或两者都必须注意:可以有一个以上的线程同时使用数据。此文章解释了在使用 Microsoft 基础类库 (MFC) 编制多线程应用程序时避免发生潜在问题的技术。

从多线程访问对象从非 MFC 线程访问 MFC 对象多线程编程:创建用户界面线程多线程编程:创建辅助线程文章中所述的方法之一创建该线程。只有这些方法才允许类库初始化处理多线程应用程序所需的内部变量。

由于大小和性能原因,MFC 对象在对象级别不是线程安全的,而只是在类级别线程安全。这表明可以有两个独立的线程操作两个不同的 CString 对象,但不能有两个线程操作同一个 CString 对象。如果一定要有多个线程操作同一个对象,请用适当的 Win32 同步机制(如临界区)保护此类访问权。有关临界区和其他相关对象的更多信息,请参见 Platform SDK 中的同步。

类库内部使用临界区以保护全局数据结构,例如调试存储分配使用的结构。

如果多线程应用程序使用 CWinThread 对象以外的方式创建线程,则不能从该线程访问其他 MFC 对象。换句话说,如果要从次要线程访问任何 MFC 对象,则必须用

Windows 句柄映射线程间通信

作为通用规则,线程只能访问它创建的 MFC 对象。这是因为临时和永久性 Windows 句柄映射保留在线程本地存储中,以对它进行保护,确保不能有多个线程同时访问它。例如,辅助线程不能执行计算并调用文档的 UpdateAllViews 成员函数来修改包含新数据视图的窗口。此操作将不会有任何效果,因为从 CWnd 对象到 HWND 的映射是主线程的本地映射。这意味着一个线程可能有从 Windows 句柄到 C++ 对象的映射,但是另一个线程可能会将此句柄映射到其他 C++ 对象。在一个线程内所做的更改将不会反映在另一个线程中。

有几种方式可以避免此问题。首先是将各个句柄(如 HWND)而不是 C++ 对象传递到辅助线程。然后,辅助线程通过调用适当 FromHandle 成员函数将这些对象添加到它的临时映射。还可以通过调用 Attach 将对象添加到线程的永久映射,但只有在保证对象比线程存在的时间长时,才应当进行此操作。

另一个方法是创建新的与辅助线程将要执行的不同任务相对应的用户定义消息,并使用 ::PostMessage 将这些消息发布到应用程序的主窗口。此通信方法类似于两个不同的应用程序在对话,只不过这两个线程在同一地址空间中执行。

有关句柄映射的更多信息,请参见技术说明 3。有关线程本地存储的更多信息,请参见 Platform SDK 中的线程本地存储和使用线程本地存储区。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
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`,包含了开始和停止按钮,点击开始按钮创建服务器线程,点击停止按钮关闭服务器线程和所有客户端套接字,同时处理客户端套接字的接收事件,将接收到的数据发送回客户端。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值