You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

单进程多线程中调用WinSock2 accept函数触发访问违例问题

问题:多线程WinSock2监听时第二个线程accept触发访问违例

我正在为公司自研语言通过FFI开发C/C++库,涉及线程库与套接字库。在单进程内创建两个线程分别监听不同端口时,第一个线程可正常运行,第二个线程调用WinSock2的accept函数时触发访问违例,经Application Verifier检测显示“First chance access violation for current stack trace”。预期效果为两个套接字持续监听请求直至程序终止,但实际第二个线程调用accept时程序退出。

相关代码

threads.cc

#include <windows.h>
#include <stdlib.h>
#include <iostream>

DWORD WINAPI thread_call(void* data)
{
    void** datap = (void**)data;
    
    void*(*datafn)(void*) = (void*(*)(void*))datap[0];
    datafn(datap[1]);

    return 0;
}

extern "C" {
void* gc_alloc(int size);

char* create_thread(char *(*worker)(char *), char *data) {
    void** datap = (void**)gc_alloc(sizeof(char*(*)(char*))+sizeof(char*));
    datap[0] = (void*)worker;
    datap[1] = (void*)data;

    HANDLE* handle = (HANDLE*)gc_alloc(sizeof(HANDLE));
    *handle = CreateThread(NULL, 0, thread_call, datap, 0, NULL);
    return (char*)handle;
}

void close_thread(char* th) {
    HANDLE* thandle = (HANDLE*)th;
    CloseHandle(*thandle);
}
}

sockets.cc

#include <winsock2.h>
#include <Ws2tcpip.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <windows.h>
#include <iostream>
#include <string.h>
#include "../websocket.h"
#pragma comment(lib, "ws2_32.lib")

#define BUFFER_SIZE 2048

extern "C"
{
    // 自研语言程序入口处调用
    int windowsStart()
    {
        WSADATA wsaData;
        if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        {
            return 1;
        }

        return 0;
    }

    // 自研语言程序退出时调用
    void windowsCleanup()
    {
        WSACleanup();
    }

    void *gc_alloc(int bytes);
    char* runtime_int_to_string(int number);

    SOCKET create_socket(int port)
    {
        int iResult;

        SOCKET ListenSocket = INVALID_SOCKET;

        struct addrinfo *result = NULL;
        struct addrinfo hints;

        int iSendResult;

        ZeroMemory(&hints, sizeof(hints));
        hints.ai_family = AF_INET;
        hints.ai_socktype = SOCK_STREAM;
        hints.ai_protocol = IPPROTO_TCP;
        hints.ai_flags = AI_PASSIVE;
        

        // 解析服务器地址和端口
        iResult = getaddrinfo(NULL, runtime_int_to_string(port), &hints, &result);
        if (iResult != 0)
        {
            printf("getaddrinfo failed with error: %d\n", iResult);
            WSACleanup();
            return 1;
        }

        // 创建用于监听客户端连接的套接字
        ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
        if (ListenSocket == INVALID_SOCKET)
        {
            printf("socket failed with error: %ld\n", WSAGetLastError());
            freeaddrinfo(result);
            WSACleanup();
            return 1;
        }

        // 设置TCP监听套接字
        iResult = bind(ListenSocket, result->ai_addr, (int)result->ai_addrlen);
        if (iResult == SOCKET_ERROR)
        {
            printf("bind failed with error: %d\n", WSAGetLastError());
            freeaddrinfo(result);
            closesocket(ListenSocket);
            WSACleanup();
            return 1;
        }

        freeaddrinfo(result);

        iResult = listen(ListenSocket, SOMAXCONN);
        if (iResult == SOCKET_ERROR)
        {
            printf("listen failed with error: %d\n", WSAGetLastError());
            closesocket(ListenSocket);
            WSACleanup();
            return 1;
        }

        return ListenSocket;
    }

    int open_socket(int *port, bool *run_socket, char *(*request_handler)(char *, char *), char *data)
    {
        SOCKET ListenSocket = create_socket(*port);
        SOCKET ClientSocket = INVALID_SOCKET;

        int iSendResult;
        int iRecvResult;

        char buffer[BUFFER_SIZE];
        while (*run_socket)
        {
            ClientSocket = accept(ListenSocket, NULL, NULL); // 错误发生位置

            if (ClientSocket == INVALID_SOCKET) {
                printf("accept failed with error: %d\n", WSAGetLastError());
                closesocket(ListenSocket);
                WSACleanup();
                return 1;
            }

            iRecvResult = recv(ClientSocket, buffer, BUFFER_SIZE, 0);

            char *response = request_handler(data, buffer);
            iSendResult = send(ClientSocket, response, strlen(response), 0);
            if (iSendResult == SOCKET_ERROR) {
                closesocket(ClientSocket);
                WSACleanup();
                return 1;
            }

            close_socket(ClientSocket);
        }
    }
}

线程创建示例代码

char* requestHandler(char* data, char* request) {
    ...
}

char* worker(char* data) {
    bool runSocket = true;
    open_socket((int*)data, &runSocket, requestHandler, NULL);
}

int main() {
    int port1 = 1234;
    int port2 = 4321;

    windowsStart();
    create_thread(worker, (char*)(&port1));
    create_thread(worker, (char*)(&port2));
    windowsCleanup();
}

问题原因分析

  1. WSACleanup过早调用:示例main函数中,创建线程后立即调用windowsCleanup(),而WinSock2的WSACleanup是进程级操作,会释放整个进程的Winsock资源。此时第二个线程可能还在初始化套接字,或者已创建的套接字资源被提前释放,导致后续accept调用访问无效内存,触发访问违例。

  2. 错误处理中滥用WSACleanup:create_socket和open_socket函数中,只要出错就调用WSACleanup,这会导致其他线程的Winsock资源被意外销毁。WSACleanup只需在整个进程退出前调用一次,而非每个套接字错误时都调用。

  3. 返回值错误:create_socket函数在出错时返回1,但SOCKET类型本质是句柄,应返回INVALID_SOCKET,这可能导致后续对无效套接字的操作引发异常。

修复方案

1. 调整WSACleanup调用时机

在main函数中,必须等待所有线程完成Winsock操作后再调用windowsCleanup,可通过WaitForMultipleObjects等待线程退出:

int main() {
    int port1 = 1234;
    int port2 = 4321;

    windowsStart();
    char* th1 = create_thread(worker, (char*)(&port1));
    char* th2 = create_thread(worker, (char*)(&port2));

    // 等待两个线程结束
    HANDLE handles[] = {*(HANDLE*)th1, *(HANDLE*)th2};
    WaitForMultipleObjects(2, handles, TRUE, INFINITE);

    close_thread(th1);
    close_thread(th2);
    windowsCleanup();
}

2. 移除错误处理中的WSACleanup调用

仅在进程退出前调用一次WSACleanup,错误处理时只需清理当前线程的套接字和addrinfo资源:

// 修改create_socket中的错误分支示例
if (iResult != 0)
{
    printf("getaddrinfo failed with error: %d\n", iResult);
    // 移除WSACleanup();
    return INVALID_SOCKET;
}

// 其他错误分支同理,仅清理当前资源,不调用WSACleanup

3. 修正套接字返回值

确保create_socket在出错时返回INVALID_SOCKET而非1,避免后续操作无效句柄:

// 替换所有错误返回的1为INVALID_SOCKET
return INVALID_SOCKET;

4. 验证gc_alloc线程安全性

如果gc_alloc未实现线程安全,需添加同步机制(如临界区)保护内存分配操作,避免多线程并发分配导致内存损坏。

内容的提问来源于stack exchange,提问作者JSGuy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 21:44:50