单进程多线程中调用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(); }
问题原因分析
WSACleanup过早调用:示例
main函数中,创建线程后立即调用windowsCleanup(),而WinSock2的WSACleanup是进程级操作,会释放整个进程的Winsock资源。此时第二个线程可能还在初始化套接字,或者已创建的套接字资源被提前释放,导致后续accept调用访问无效内存,触发访问违例。错误处理中滥用WSACleanup:
create_socket和open_socket函数中,只要出错就调用WSACleanup,这会导致其他线程的Winsock资源被意外销毁。WSACleanup只需在整个进程退出前调用一次,而非每个套接字错误时都调用。返回值错误:
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

