Winsock recv() 函数阻塞其他线程

Posted

技术标签:

【中文标题】Winsock recv() 函数阻塞其他线程【英文标题】:Winsock recv() function blocking other threads 【发布时间】:2019-01-02 07:25:51 【问题描述】:

我正在编写一个简单的 Windows TCP/IP 服务器应用程序,它一次只需要与一个客户端通信。我的应用程序有四个线程:

    主程序,它还根据需要处理数据传输。 接收传入数据线程。 侦听线程以接受来自客户端的连接请求。 一个 ping 线程,用于监控其他所有内容,并根据需要传输心跳消息。我意识到 TCP/IP 不需要后者,但客户端应用程序(我无法控制)需要它。

我已在任务管理器中确认我的应用程序确实有四个线程在运行。

我正在使用阻塞 TCP/IP 套接字,但我的理解是它们只会阻塞调用线程——其他线程仍然应该被允许执行而不被阻塞。但是,我遇到了以下问题:

    如果 ping 线程认为连接已断开,它会调用 closesocket()。但是,这似乎被接收线程中对 recv() 的调用所阻止。

    当接收线程调用 recv() 时,主应用程序无法传输数据。

正在通过 accept() 函数创建套接字。在这个阶段,我没有设置任何套接字选项。

我现在创建了一个简单的两线程程序来说明问题。如果没有 WSA_FLAG_OVERLAPPED 标志,第二个线程会被第一个线程阻塞,即使这看起来与应该发生的情况相反。如果设置了 WSA_FLAG_OVERLAPPED 标志,那么一切正常。

PROJECT SOURCE FILE:
====================

program Blocking;

uses
  Forms,
  Blocking_Test in 'Blocking_Test.pas' Form1,
  Close_Test in 'Close_Test.pas';

$R *.res

begin
  Application.Initialize;
  Application.CreateForm(TForm1, Form1);
  Application.Run;
end.  Blocking 

UNIT 1 SOURCE FILE:
===================

unit Blocking_Test;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, WinSock2;

type
  TForm1 = class(TForm)
    procedure FormShow(Sender: TObject);
  private
     Private declarations 
  public
     Public declarations 
  end;

var
  Form1: TForm1;
  Test_Socket: TSocket;
  Test_Addr: TSockAddr;
  wsda: TWSADATA;  used to store info returned from WSAStartup 

implementation

$R *.dfm

uses
  Debugger, Close_Test;

procedure TForm1.FormShow(Sender: TObject);
const
  Test_Port: word = 3804;
var
  Buffer: array [0..127] of byte;
  Bytes_Read: integer;
begin  TForm1.FormShow 
  Debug('Main thread started');
  assert(WSAStartup(MAKEWORD(2,2), wsda) = 0);  WinSock load version 2.2 
  Test_Socket := WSASocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP, nil, 0, 0WSA_FLAG_OVERLAPPED);
  assert(Test_Socket <> INVALID_SOCKET);
  with Test_Addr do
  begin
    sin_family := AF_INET;
    sin_port := htons(Test_Port);
    sin_addr.s_addr := 0;  this will be filled in by bind 
  end;  with This_PC_Address 
  assert(bind(Test_Socket, @Test_Addr, SizeOf(Test_Addr)) = 0);
  Close_Thread := TClose_Thread.Create(false);  start thread immediately 
  Debug('B4 Rx');
  Bytes_Read := recv(Test_Socket, Buffer, SizeOf(Buffer), 0);
  Debug('After Rx');
end;  TForm1.FormShow 

end.  Blocking_Test 

UNIT 2 SOURCE FILE:
===================

unit Close_Test;

interface

uses
  Classes;

type
  TClose_Thread = class(TThread)
  protected
    procedure Execute; override;
  end;  TClose_Thread 

var
  Close_Thread: TClose_Thread;

implementation

uses
  Blocking_Test, Debugger, Windows, WinSock2;

type
  TThreadNameInfo = record
    FType: LongWord;     // must be 0x1000
    FName: PChar;        // pointer to name (in user address space)
    FThreadID: LongWord; // thread ID (-1 indicates caller thread)
    FFlags: LongWord;    // reserved for future use, must be zero
  end;  TThreadNameInfo 

var
  ThreadNameInfo: TThreadNameInfo;

procedure TClose_Thread.Execute;

  procedure SetName;
  begin  SetName 
    ThreadNameInfo.FType := $1000;
    ThreadNameInfo.FName := 'Ping_Thread';
    ThreadNameInfo.FThreadID := $FFFFFFFF;
    ThreadNameInfo.FFlags := 0;
    try
      RaiseException( $406D1388, 0, sizeof(ThreadNameInfo) div sizeof(LongWord), @ThreadNameInfo );
    except
    end;  try 
  end;  SetName 

begin  TClose_Thread.Execute 
  Debug('Close thread started');
  SetName;
  sleep(10000);  wait 10 seconds 
  Debug('B4 Close');
  closesocket(Test_Socket);
  Debug('After Close');
end;  TClose_Thread.Execute 

end.  Close_Test 

附:由于设置 WSA_FLAG_OVERLAPPED 属性已解决问题,因此出于学术兴趣,我发布了上述内容。

【问题讨论】:

能把源代码放上来吗? recv 应该只阻塞它所在的线程。否则,一个连接的客户端可能会通过发出缓慢的请求/响应来阻塞另一个线程中的另一个客户端。 好的,但是我需要一些时间来制作一些足够小的内容,只包含相关部分。 我仍在努力创建一个足够小的测试程序以在此处列出,但同时我想到了另一个想法:线程可以“拥有”其他线程,这样做会导致一个线程阻止另一个?我的应用程序是用 Delphi 编写的,我使用的是标准的 TThread 组件。 @MartynA 在 Windows 上,关闭套接字确实会取消其他线程中正在进行的阻塞操作。 @ChrisHubbard 您是否碰巧在阻塞套接字操作时使用了自己的临界区或互斥锁?在没有看到您的实际代码的情况下,这是我能想到的唯一方法。就其本身而言,阻塞套接字的行为方式与您描述的不同。 【参考方案1】:

如果 ping 线程认为连接已经断开,它会调用 closesocket()。但是,这似乎被接收线程中对 recv() 的调用所阻止。

这只是您的代码中的一个错误。您不能在一个线程中释放资源,而另一个线程正在或可能正在使用它。您将不得不安排一些理智的方式来确保您不会围绕访问套接字创建竞争条件。

需要明确的是,您无法知道这种代码可能会做什么。例如,考虑:

    线程实际上还没有调用recv即将调用recv,但是调度程序还没有处理它。 另一个线程调用closesocket。 作为系统库一部分的线程打开了一个新套接字,并碰巧获得了您刚刚关闭的相同套接字描述符。 您的线程现在可以调用recv,只是它在库打开的套接字上接收!

避免此类竞争条件是您的责任,否则您的代码将出现不可预测的行为。您无法知道对随机套接字执行随机操作的后果可能是什么。因此,您必须 在一个线程中释放资源,而另一个线程正在、可能或(最糟糕的)可能即将使用它。

很可能实际发生的情况是 Delphi 有某种内部同步,它试图通过阻塞无法安全向前推进的线程来使您免于灾难。

【讨论】:

感谢您的回答。在 Ping 线程的情况下,我可以消除它关闭套接字的需要(从而消除潜在的竞争条件),只要我能够向套接字添加接收超时。到目前为止,我这样做的尝试没有成功,因此我尝试将此功能添加到 Ping 线程。这就提出了一个新问题:如何将接收超时添加到阻塞套接字?根据我的原始帖子,套接字是由 accept() 函数创建的。 如果您有新问题要问,请提出新问题 @ChrisHubbard 你可以让 ping 线程关闭连接而不关闭套接字。 那么Ping线程调用shutdown()而不是closesocket()就可以了?我的印象是无论如何 closesocket() 调用了 shutdown()。顺便说一句,在这种情况下,不需要有序关机。硬关机没问题。 请注意,Windows 不使用套接字的文件描述符,这与其他一些平台的做法不同。套接字是真正的内核对象,因此新套接字不可能获得与最近关闭的套接字相同的值。这也是为什么你不能在select() 中使用非套接字描述符,以及为什么需要closesocket() 而不是`close()。【参考方案2】:

更新:accept() 创建与用于侦听的套接字具有相同属性的新套接字。由于我没有为侦听套接字设置 WSA_FLAG_OVERLAPPED 属性,因此没有为新套接字设置此属性,接收超时等选项也没有任何作用。

为监听套接字设置 WSA_FLAG_OVERLAPPED 属性似乎已经解决了这个问题。因此我现在可以使用接收超时,如果没有收到数据,Ping 线程不再需要关闭套接字。

为监听套接字设置 WSA_FLAG_OVERLAPPED 属性似乎也解决了阻塞其他线程的问题。

【讨论】:

请注意,WSASocket() 文档指出:“默认情况下,使用 WSASocket() 函数创建的套接字不会设置此重叠属性。相比之下,@987654324 @ 函数创建一个支持重叠 I/O 操作的套接字作为默认行为“因此,如果您使用 socket() 创建侦听套接字,那么 accept() 将继承重叠属性。

以上是关于Winsock recv() 函数阻塞其他线程的主要内容,如果未能解决你的问题,请参考以下文章

如何中止winsock阻塞调用?

Select模型

在winsock中,我将如何阻止接受函数从另一个线程阻塞?

线程中阻塞 MPI_Recv 的 CPU 使用率

奇怪的 Winsock recv() 减速

同时执行 recv() 和 send() winsock