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

时间:2019-01-02 07:25:52

标签: multithreading delphi winsock

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

  1. 主程序,该程序还根据需要处理数据传输。
  2. 接收传入的数据线程。
  3. 监听线程以接受来自客户端的连接请求。
  4. 一个ping线程,该线程监视其他所有事物,并根据需要传输心跳消息。我意识到,TCP / IP并不需要后者,但是客户端应用程序(我无法控制)需要这样做。

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

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

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

  2. 当接收线程正在进行对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, 0{WSA_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 }

P.S。由于设置WSA_FLAG_OVERLAPPED属性已解决了该问题,因此出于学术兴趣,我发布了以上内容。

2 个答案:

答案 0 :(得分:2)

  

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

那只是代码中的错误。当一个线程正在使用或可能正在使用另一个线程时,您无法在一个线程中释放资源。您将必须安排一些明智的方法,以确保不会在访问套接字的过程中产生竞争条件。

要清楚,您无法知道这种代码可以做什么。例如,考虑:

  1. 该线程实际上尚未调用recv,调用recv大约是,但是调度程序尚未解决。
  2. 另一个线程调用closesocket
  3. 作为系统库一部分的线程会打开一个新的套接字,并且碰巧会得到与您刚刚关闭的套接字描述符相同的内容。
  4. 您的线程现在可以调用recv,只有它在库打开的套接字上接收!

您有责任避免此类竞争情况,否则您的代码将无法正常运行。您无法知道在随机套接字上执行随机操作的后果。因此,您必须 在一个线程中释放资源,而另一个线程正在使用,可能正在使用(或者最糟糕)可能正在使用该资源。

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

答案 1 :(得分:0)

更新:accept()创建具有与用于侦听的套接字相同属性的新套接字。由于我尚未为监听套接字设置WSA_FLAG_OVERLAPPED属性,因此未为新套接字设置此属性,并且诸如接收超时之类的选项也无济于事。

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

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