WinSock 的多线程编程

目录

概述

Winsock 为什么需要多线程

阻塞模式和非阻塞模式

单线程和多线程的优缺点

Win32 系统下的多进程多线程机制

进程和线程

线程创建

线程同步

线程通信   

VC++ 对多线程网络编程的支持

MFC 中的多线程支持

ATL 中的多线程支持

多线程 FTP 客户端实例

头文件包含

线程函数

主函数

总结


概述

        Winsock(Windows Sockets)是 Windows 操作系统中用于实现网络通信的一套 API(应用程序编程接口)。Winsock 提供了两种 I/O 模式:阻塞模式和非阻塞模式。在阻塞模式下,应用程序在发送或接收数据时是阻塞的,直到操作完成或超时。而在非阻塞模式下,应用程序可以同时执行其他任务,在发送或接收数据时不会被阻塞。

        Winsock 的多线程编程通常用于实现非阻塞模式,它允许应用程序在等待网络操作完成的同时执行其他任务,提高了应用程序的响应能力和效率。

Winsock 为什么需要多线程

        Winsock 需要多线程主要有两个原因:提高应用程序的响应能力和效率。

阻塞模式和非阻塞模式

  1. 阻塞模式

    • 在阻塞模式下,当应用程序发送或接收数据时,程序会一直等待直到操作完成或超时。
    • 这意味着在网络操作期间,应用程序无法执行其他任务,因为它被“阻塞”了。
    • 阻塞模式通常简单易用,但可能会导致应用程序的响应性和效率降低,特别是在处理大量连接或数据时。
  2. 非阻塞模式

    • 在非阻塞模式下,应用程序可以同时执行其他任务,而不会在发送或接收数据时被阻塞。
    • 即使网络操作没有完成,应用程序也可以继续执行其他任务,从而提高了应用程序的响应能力和效率。
    • 非阻塞模式通常需要更复杂的编程技巧,因为程序需要不断地轮询或使用回调函数来检查网络操作的状态,以决定何时发送或接收数据。

单线程和多线程的优缺点

  1. 单线程

    • 优点
      • 实现简单:单线程编程模型相对简单,因为只有一个执行流程,避免了复杂的线程管理和同步操作。
      • 适合简单的网络应用程序:对于简单的网络应用,如少量用户的服务端或客户端,单线程可能已经足够满足需求。
    • 缺点
      • 效率低下:在单线程模式下,应用程序在等待网络操作完成时无法执行其他任务,导致效率较低。
      • 不适合高并发:对于需要处理大量并发连接或复杂计算的网络应用程序,单线程模式可能无法满足性能需求。
  2. 多线程

    • 优点
      • 提高了应用程序的响应能力和效率:多线程允许应用程序同时执行多个任务,从而提高了系统的响应能力和处理效率。
      • 适合处理复杂任务和高并发:对于需要处理大量并发连接或复杂计算的网络应用程序,多线程模式可以更好地满足性能需求。
    • 缺点
      • 实现复杂:多线程编程涉及到线程的创建、销毁、同步、通信等问题,因此需要更复杂的代码和设计。
      • 容易引发问题:线程之间的并发访问可能导致数据竞态、死锁等问题,需要仔细处理线程同步和通信,否则会导致程序出现不确定的行为

Win32 系统下的多进程多线程机制

        Win32 系统提供了多进程和多线程机制,支持多线程编程。

进程和线程

        进程是操作系统中的一个执行实体,它是程序执行过程中的一个实例。每个进程都有自己独立的地址空间、数据栈、文件描述符和系统资源,包括打开的文件、信号处理等。进程之间的通信通常需要通过进程间通信(IPC)机制,如管道、消息队列、共享内存等。

        线程是进程中的一个执行流,是操作系统调度的基本单位。一个进程可以拥有多个线程,它们共享进程的地址空间和系统资源,包括堆、全局变量、文件描述符等。线程之间的通信可以直接读写共享的内存空间,因此线程间通信更加高效。然而,由于线程共享进程的资源,因此线程间的竞争和同步问题需要仔细处理,以避免出现数据竞态、死锁等问题。

线程创建

        Win32 系统提供了 CreateThread() 函数来创建线程。该函数需要指定线程函数和线程参数。

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES lpThreadAttributes,
  SIZE_T dwStackSize,
  LPTHREAD_START_ROUTINE lpStartAddress,
  LPVOID lpParameter,
  DWORD dwCreationFlags,
  LPDWORD lpThreadId
);

各参数的含义如下:

  • lpThreadAttributes:指定线程的安全属性,一般传入 NULL 表示使用默认安全属性。
  • dwStackSize:指定线程栈的大小,如果传入 0,则使用默认的线程栈大小。
  • lpStartAddress:指定线程函数的起始地址,也就是线程将要执行的函数。
  • lpParameter:传递给线程函数的参数,可以为 NULL
  • dwCreationFlags:指定线程的创建标志,一般传入 0 表示使用默认标志。
  • lpThreadId:用来接收新线程的线程标识符(ID)的指针。

示例代码如下所示:

#include <Windows.h>
#include <iostream>

using namespace std;

DWORD WINAPI ThreadFunc(LPVOID lpParam) {
    int* p = (int*)lpParam;
    cout << "Thread function executing with parameter: " << *p << endl;
    return 0;
}

int main() {
    DWORD dwThreadId;
    int param = 123;
    HANDLE hThread = CreateThread(NULL, 0, ThreadFunc, &param, 0, &dwThreadId);
    if (hThread == NULL) {
        cerr << "Failed to create thread!" << endl;
        return 1;
    }

    // 等待线程结束
    WaitForSingleObject(hThread, INFINITE);

    // 关闭线程句柄
    CloseHandle(hThread);

    return 0;
}

线程同步

        Win32系统提供了多种线程同步机制,用于解决多线程环境下资源共享和程序正确执行的问题。这些机制包括:

  • 临界区(Critical Section):是一种最基本的同步机制,用于保护一段关键代码,使其只能由一个线程独占执行。临界区由内核维护,并通过自旋锁实现同步。由于自旋锁的效率较高,因此临界区适用于对资源的短时间访问。
  • 互斥体(Mutex):与临界区类似,互斥体也用于保护共享资源,使其只能由一个线程独占。但是,互斥体比临界区更加灵活,它可以支持多个线程在不同时间段独占资源。此外,互斥体还可以用于实现进程间的同步。
  • 事件(Event):用于通知一个或多个线程发生某种事件。事件可以处于**已触发(signaled)未触发(non-signaled)**两种状态。当事件被触发时,等待该事件的线程将被唤醒。事件常用于线程间的通信和协调。
  • 信号量(Semaphore):用于控制对共享资源的访问。信号量维护着一个计数器,该计数器表示资源的可用数量。线程只能在资源可用时才能获取信号量。信号量常用于限制对资源的访问数量,以防止资源过载。
  • 条件变量(Condition Variable):用于在互斥体环境下实现更复杂的同步。条件变量允许线程等待某个条件满足后再继续执行。条件变量常用于实现生产者-消费者模式等多线程协作模式。

选择合适的同步机制

        在实际应用中,需要根据具体情况选择合适的同步机制。一般来说,应遵循以下原则:

  • 优先使用效率更高的同步机制:在性能关键的代码中,应优先使用自旋锁等效率较高的同步机制。
  • 避免长时间持有同步锁:应尽量减少持有同步锁的时间,以免造成其他线程阻塞。
  • 使用适当的锁粒度:锁的粒度应尽可能小,以减少对其他线程的影响。

以下是一些额外的同步机制

  • 读写锁(Reader/Writer Lock):用于协调对可读写资源的访问。读写锁允许多个线程同时读取资源,但只能有一个线程写入资源。
  • 轻量级互斥体(Slim Mutex):是一种更轻量级的互斥体,适用于对资源的短暂访问。
  • 无锁数据结构:对于一些简单的共享数据,可以使用无锁数据结构来避免同步开销。

线程通信   

        在 Win32 系统中,线程之间可以使用多种机制进行通信,其中包括:

  1. 共享内存:可以通过在内存中创建共享的数据结构,使得多个线程可以读写相同的内存区域来进行通信。Win32 提供了 CreateFileMapping()MapViewOfFile() 函数来创建共享内存,线程可以使用这些共享内存来传递数据。

  2. 信号量:信号量是一种用于控制多个线程对共享资源访问的同步机制。Win32 提供了 CreateSemaphore()WaitForSingleObject() 函数来创建和等待信号量。线程可以使用信号量来协调对共享资源的访问。

  3. 管道:管道是一种用于在两个进程或线程之间进行通信的方法。Win32 提供了匿名管道和命名管道两种类型的管道。线程可以使用管道来传输数据。

  4. 消息队列:消息队列是一种线程间通信的方式,其中一个线程将消息发送到队列,而另一个线程则从队列中接收消息。Win32 提供了 PostThreadMessage()GetMessage() 函数来实现消息队列。

  5. 事件:事件是一种用于线程间同步的对象,用于通知其他线程发生了某种情况。Win32 提供了 CreateEvent()SetEvent() 函数来创建和设置事件,线程可以使用事件来同步操作。

VC++ 对多线程网络编程的支持

        VC++ 提供了对多线程网络编程的支持,包括 Winsock 的多线程编程。

MFC 中的多线程支持

        MFC(Microsoft Foundation Class)是微软公司提供的 C++ 应用程序框架,它简化了 Windows 应用程序的开发。MFC 也提供了对多线程编程的支持,使得开发人员能够轻松地编写多线程应用程序。

MFC 中的多线程支持主要包括以下几个方面:

  • CWinThread 类:封装了 Win32 系统的线程,提供了一些方法和属性来管理线程的创建、启动、暂停、终止等操作。
  • 线程消息循环:每个 CWinThread 对象都有自己的消息循环,用于处理与线程相关的消息。
  • 线程同步机制:MFC 提供了多种线程同步机制,用于协调多线程之间的执行。

CWinThread 类

        CWinThread 类是 MFC 中用于管理线程的主要类。它封装了 Win32 系统的线程,并提供了一些方法和属性来管理线程的创建、启动、暂停、终止等操作。CWinThread 类的主要成员函数包括:

  • CreateThread():创建线程。
  • Run():线程的入口函数。
  • ResumeThread():恢复暂停的线程。
  • SuspendThread():暂停正在运行的线程。
  • TerminateThread():终止线程。

线程消息循环

        每个 CWinThread 对象都有自己的消息循环,用于处理与线程相关的消息。线程消息循环与应用程序主窗口的消息循环类似,但它运行在不同的线程中。线程消息循环可以处理来自其他线程的消息,也可以处理来自 Windows 系统的消息。

线程同步机制

        MFC 提供了多种线程同步机制,用于协调多线程之间的执行。这些机制包括:

  • 互斥体(Mutex):用于保护共享资源,使其只能由一个线程独占。
  • 事件(Event):用于通知一个或多个线程发生某种事件。
  • 信号量(Semaphore):用于控制对共享资源的访问。
  • 批判区(Critical Section):用于保护一段关键代码,使其只能由一个线程独占执行。

示例

        以下代码演示了如何使用 CWinThread 类来创建一个简单的多线程应用程序:

#include <afxwin.h>

class CMyThread : public CWinThread
{
public:
    CMyThread() : m_dwCount(0) {}

    DECLARE_DYNCREATE(CMyThread)

protected:
    virtual BOOL InitInstance() override
    {
        return TRUE;
    }

    virtual UINT Run() override
    {
        for (int i = 0; i < 100; ++i)
        {
            // 使用互斥体保护共享变量 m_dwCount
            CWinMutex mutex(TRUE);
            mutex.Lock();
            ++m_dwCount;
            mutex.Unlock();

            // 输出线程 ID 和计数器值
            TRACE("Thread ID: %d, Count: %d\n", GetCurrentThreadId(), m_dwCount);
        }

        return 0;
    }

private:
    CWinMutex m_mutex;
    DWORD m_dwCount;
};

IMPLEMENT_DYNCREATE(CMyThread)

int main()
{
    AfxInitApp();

    // 创建两个 CMyThread 对象
    CMyThread thread1;
    CMyThread thread2;

    // 启动两个线程
    thread1.CreateThread();
    thread2.CreateThread();

    // 等待两个线程终止
    thread1.WaitForSingleObject(INFINITE);
    thread2.WaitForSingleObject(INFINITE);

    AfxExitApp();

    return 0;
}

        在该示例中,CMyThread 类派生自 CWinThread 类,并重写了 InitInstance() 和 Run() 方法。InitInstance() 方法用于初始化线程,Run() 方法是线程的入口函数。在 Run() 方法中,线程会循环 100 次,每次都会递增共享变量 m_dwCount 的值。为了保护 m_dwCount,使用 CWinMutex 锁定了互斥体。

        CWinThread 类还提供了一些方便的方法来处理线程的同步,例如:

  • PostThreadMessage():向线程发送消息。
  • WaitForSingleObject():等待线程终止。
  • SignalEvent():触发事件。
  • WaitForMultipleObjects():等待多个线程或事件终止。

ATL 中的多线程支持

        ATL(Active Template Library)是 Microsoft 公司提供的一套 C++ 模板库,它简化了 COM 组件和应用程序的开发。ATL 也提供了对多线程编程的支持,使得开发人员能够轻松地编写多线程应用程序。

ATL 中的多线程支持主要包括以下几个方面:

  • CComAutoThreadModule 类:封装了多线程模块,简化了多线程模块的创建和管理。
  • 线程安全机制:ATL 提供了多种线程安全机制,用于保护共享资源,防止数据损坏。
  • 同步机制:ATL 提供了多种同步机制,用于协调多线程之间的执行。

CComAutoThreadModule 类

        CComAutoThreadModule 类是 ATL 中用于管理多线程模块的主要类。它封装了线程初始化和清理的细节,使得开发人员可以专注于应用程序的逻辑。CComAutoThreadModule 类提供了以下功能:

  • 自动初始化和清理线程:CComAutoThreadModule 类会在应用程序启动时自动初始化线程,并在应用程序退出时自动清理线程。
  • 管理 COM 对象:CComAutoThreadModule 类可以管理 COM 对象的生命周期,并确保 COM 对象在多线程环境下安全运行。
  • 支持 COM 计时器:CComAutoThreadModule 类支持 COM 计时器,用于在多线程环境下调度任务。

线程安全机制

        ATL 提供了多种线程安全机制,用于保护共享资源,防止数据损坏。这些机制包括:

  • 临界区(Critical Section):用于保护一段关键代码,使其只能由一个线程独占执行。
  • 互斥体(Mutex):与临界区类似,互斥体也用于保护共享资源,使其只能由一个线程独占。但是,互斥体比临界区更加灵活,它可以支持多个线程在不同时间段独占资源。
  • 事件(Event):用于通知一个或多个线程发生某种事件。事件可以处于已触发或未触发两种状态。当事件被触发时,等待该事件的线程将被唤醒。事件常用于线程间的通信和协调。
  • 信号量(Semaphore):用于控制对共享资源的访问。信号量维护着一个计数器,该计数器表示资源的可用数量。线程只能在资源可用时才能获取信号量。信号量常用于限制对资源的访问数量,以防止资源过载。

同步机制

        ATL 提供了多种同步机制,用于协调多线程之间的执行。这些机制包括:

  • CComCritSecLock:用于锁定和解锁临界区。
  • CComMutexLock:用于锁定和解锁互斥体。
  • CComEvent:用于触发和等待事件。
  • CComSemaphore:用于获取和释放信号量。

示例

以下代码演示了如何使用 CComAutoThreadModule 类和 ATL 的线程安全机制来编写一个简单的多线程应用程序:

#include <atlbase.h>

class CMyThread : public CThread
{
public:
    CMyThread() : m_dwCount(0) {}

    DWORD Run()
    {
        for (int i = 0; i < 100; ++i)
        {
            // 使用临界区保护共享变量 m_dwCount
            CComCritSecLock lock(m_cs);
            ++m_dwCount;

            // 输出线程 ID 和计数器值
            TRACE("Thread ID: %d, Count: %d\n", GetCurrentThreadId(), m_dwCount);
        }

        return 0;
    }

private:
    CComCriticalSection m_cs;
    DWORD m_dwCount;
};

int main()
{
    // 创建 CComAutoThreadModule 对象
    CComAutoThreadModule module;

    // 创建两个 CMyThread 对象
    CMyThread thread1;
    CMyThread thread2;

    // 启动两个线程
    thread1.StartThread();
    thread2.StartThread();

    // 等待两个线程终止
    thread1.WaitForSingleObject(INFINITE);
    thread2.WaitForSingleObject(INFINITE);

    return 0;
}

多线程 FTP 客户端实例

        下面是一个使用 Winsock 的多线程实现的 FTP 客户端的实例。该客户端允许多个线程同时连接到 FTP 服务器,下载文件。

头文件包含

        在代码中包含 Winsock 的头文件:#include <winsock2.h>

线程函数

        定义线程函数,用于连接到 FTP 服务器并下载文件:

DWORD WINAPI DownloadThread(LPVOID lpParam)
{
    // 获取线程参数
    DWORD dwIndex = *((DWORD*)lpParam);
    CString strFileName = "file" + CString(dwIndex) + ".txt"; // 文件名

    // 创建套接字
    SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (sock == INVALID_SOCKET)
    {
        TRACE("创建套接字失败:%d\n", WSAGetLastError()); // 创建套接字失败
        return 1;
    }

    // 连接到 FTP 服务器
    sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(21); // FTP 服务器端口号
    serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // FTP 服务器 IP 地址
    int nConnectResult = connect(sock, (SOCKADDR*)&serverAddr, sizeof(serverAddr));
    if (nConnectResult == SOCKET_ERROR)
    {
        TRACE("连接到 FTP 服务器失败:%d\n", WSAGetLastError()); // 连接到 FTP 服务器失败
        closesocket(sock);
        return 1;
    }

    // 登录到 FTP 服务器
    sendCommand(sock, "USER anonymous"); // 发送用户名命令
    recvResponse(sock); // 接收响应

    sendCommand(sock, "PASS anonymous@example.com"); // 发送密码命令
    recvResponse(sock); // 接收响应

    // 切换到二进制模式
    sendCommand(sock, "TYPE I"); // 发送切换模式命令
    recvResponse(sock); // 接收响应

    // 进入 FTP 文件传输模式
    sendCommand(sock, "PASV"); // 发送进入被动模式命令
    CString strPASVResponse = recvResponse(sock); // 接收响应

    // 解析 PASV 响应以获取服务器 IP 地址和端口
    int nPortStart = strPASVResponse.Find(',', 4);
    if (nPortStart == -1)
    {
        TRACE("解析 PASV 响应失败\n"); // 解析 PASV 响应失败
        closesocket(sock);
        return 1;
    }

    CString strIP = strPASVResponse.Mid(4, nPortStart - 4); // 提取服务器 IP 地址
    CString strPort = strPASVResponse.Mid(nPortStart + 1); // 提取服务器端口

    // 将 IP 地址和端口转换为数字值
    DWORD dwIP = inet_addr(strIP);
    if (dwIP == INADDR_NONE)
    {
        TRACE("无效的 IP 地址:%s\n", strIP); // 无效的 IP 地址
        closesocket(sock);
        return 1;
    }

    USHORT nPort = (USHORT)_wtoi(strPort); // 提取服务器端口

    // 创建数据套接字
    SOCKET dataSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (dataSock == INVALID_SOCKET)
    {
        TRACE("创建数据套接字失败:%d\n", WSAGetLastError()); // 创建数据套接字失败
        closesocket(sock);
        return 1;
    }

    // 连接到数据套接字
    sockaddr_in dataAddr;
    dataAddr.sin_family = AF_INET;
    dataAddr.sin_port = htons(nPort); // 数据端口
    dataAddr.sin_addr.s_addr = dwIP; // 数据服务器 IP 地址
    int nDataConnectResult = connect(dataSock, (SOCKADDR*)&dataAddr, sizeof(dataAddr));
    if (nDataConnectResult == SOCKET_ERROR)
    {
        TRACE("连接到数据套接字失败:%d\n", WSAGetLastError()); // 连接到数据套接字失败
        closesocket(sock);
        closesocket(dataSock);
        return 1;
    }

    // 打开 FTP 文件
    sendCommand(sock, "RETR " + strFileName); // 发送下载命令
    recvResponse(sock); // 接收响应

    // 下载 FTP 文件
    BYTE buffer[1024]; // 缓冲区
    DWORD dwRead; // 读取字节数
    BOOL bSuccess = TRUE; // 成功标志

主函数

        定义主函数,创建多个线程并启动 FTP 下载:

int main()
{
    // 初始化 Winsock
    WSADATA wsaData;
    WSAStartup(MAKEWORD(2, 2), &wsaData);

    // 创建多个线程
    DWORD dwThreadCount = 5;
    HANDLE hThreads[dwThreadCount];
    for (DWORD i = 0; i < dwThreadCount; i++)
    {
        // 复制索引值到新变量
        DWORD dwIndex = i;
        // 传递新变量的地址给线程
        hThreads[i] = CreateThread(NULL, 0, DownloadThread, &dwIndex, 0, NULL);
    }

    // 等待所有线程完成
    WaitForMultipleObjects(dwThreadCount, hThreads, TRUE, INFINITE);

    // 注销 Winsock
    WSACleanup();

    return 0;
}

总结

        Winsock 的多线程编程允许应用程序在等待网络操作完成的同时执行其他任务,提高了应用程序的响应能力和效率。Winsock 提供了两种 I/O 模式:阻塞模式和非阻塞模式。在非阻塞模式下,应用程序可以同时执行其他任务,避免了应用程序失去响应或效率低下的问题。Win32 系统提供了多进程和多线程机制,支持多线程编程。VC++ 也提供了对多线程网络编程的支持,包括 MFC 和 ATL 中的多线程类。

  • 30
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值