Delphi Win API CreateTimerQueueTimer线程和线程安全FormatDateTime崩溃问题

4

这是一个有点长的问题,但是我们来说一下。有一个被称为线程安全的FormatDateTime版本,您可以使用它来格式化日期和时间。线程安全意味着多个线程可以同时调用该函数而不会导致任何问题。这是通过将每个线程的数据存储在其自己的栈帧中来实现的。

GetLocaleFormatSettings(3081, FormatSettings); 

获取一个值,然后您可以像这样使用它;

FormatDateTime('yyyy', 0, FormatSettings); 

现在想象一下两个计时器,一个使用TTimer(间隔为1000ms),然后另一个计时器创建如下(10ms间隔);
CreateTimerQueueTimer
(
  FQueueTimer, 
  0, 
  TimerCallback, 
  nil, 
  10, 
  10, 
  WT_EXECUTEINTIMERTHREAD
);

现在是比较棘手的部分,如果在回调函数和定时器事件中有以下代码;
for i := 1 to 10000 do
begin
  FormatDateTime('yyyy', 0, FormatSettings);
end;

请注意,此处没有赋值操作。这几乎立即导致访问冲突,有时在20分钟后,在随机位置发生。如果您在C++Builder中编写该代码,则不会崩溃。我们正在使用的头文件转换是JEDI JwaXXXX。即使我们在Delphi版本中在代码周围放置锁定,也只能延迟不可避免的结果。我们查看了原始的C头文件,一切看起来都很好,是否C ++使用Delphi运行时的不同方式?FormatDatTime的线程安全版本似乎是可重入的。任何之前见过这种情况的人有什么想法或思路。
更新:
为了缩小范围,FormatSettings作为常量传递,因此如果它们使用相同的副本是否重要(事实证明,在函数调用中传递本地版本会产生相同的问题)?另外,采用FormatSettings的版本不调用GetThreadLocale,因为它已经在FormatSettings结构中具有区域设置信息(我通过代码步进进行了双重检查)。
我提到没有分配是为了清楚地表明没有访问共享存储,因此不需要锁定。
WT_EXECUTEINTIMERTHREAD用于简化问题。我认为你应该只用它来处理非常短的任务,因为如果运行时间长,它可能会错过下一个时间间隔?
如果使用普通的TThread,则不会出现问题。我想说的是,使用TThread或TTimer可以工作,但是在VCL之外创建的线程无法工作,这就是为什么我问C ++ Builder是否使用了VCL / Delphi RTL的不同方式。
此外,正如之前提到的那样,这段代码也会失败(但需要更长时间):CS:=TCriticalSection.Create;
  CS.Acquire;
  for i := 1 to LoopCount do
  begin
    FormatDateTime('yyyy', 0, FormatSettings);
  end;
  CS.Release;

现在让我来谈一下我真正不理解的部分,我按照建议编写了以下内容:

function ReturnAString: string;
begin
  Result := 'Test';
  UniqueString(Result);
end;

然后在每种计时器内部的代码是:
  for i := 1 to 10000 do
  begin
    ReturnAString;
  end;

这会导致与之前所述的一样的故障,故障从未发生在CPU窗口内相同的位置等。有时它是访问冲突,有时可能是无效指针操作。顺便说一下,我正在使用Delphi 2009。

更新2:

Roddy(下面)指出Ontimer事件(不幸的是还有Winsock,即TClientSocket)使用Windows消息泵(顺便说一句,使用IOCP和重叠IO的一些不错的Winsock2组件将很好),因此要避免使用它。但是,有人知道如何查看CreateQueueTimerQueue上设置了什么样的线程本地存储吗?

感谢您抽出时间思考并回答这个问题。

8个回答

5

我不确定是否应该在自己的问题下发布“答案”,但这似乎是合理的,如果这不好,请告诉我。

我认为我已经找到了问题,线程本地存储的想法让我遵循了一堆线索,发现了这个神奇的代码行:

IsMultiThread := True;

来自帮助文档;

“将IsMultiThread设置为true表示内存管理器应支持多线程。 BeginThread和类工厂会将IsMultiThread设置为true。”

当然,如果您使用单个主VCL线程使用TTimer时,不会设置此值。 但是,当您使用TThread时,它会为您设置。 如果我手动设置它,问题就会消失。

在C++Builder中,我不使用TThread,但是使用以下代码似乎可以达到同样的效果;

if (IsMultiThread) {
  ShowMessage("IsMultiThread is True!");
}

这意味着它在某个地方自动设置好了。

非常感谢人们的反馈,让我找到了这个问题的解决办法,希望它能帮助其他人。


如果你创建的是标准 Delphi 线程,则不需要设置这个值。但有时,比如在一个被调用为多线程 DLL 中,你必须自己设置它。很好的发现。 - mj2008

1

由于DateTimeToString使用GetThreadLocale,而FormatDateTime调用了它,您可能希望尝试为每个线程设置一个本地的FormatSettings变量,甚至可以在循环之前设置FormatSettings的本地变量。

这也可能是WT_EXECUTEINTIMERTHREAD参数导致的。请注意,它只应用于非常短的任务。

如果问题仍然存在,问题实际上可能出现在其他地方,这是我第一次看到时的第一反应,但我没有足够的信息来确定。

关于访问冲突发生的详细信息可能会有所帮助。


1

你确定这与FormatDateTime有关吗?你特别提到那里没有赋值语句;这是你问题的重要方面吗?如果你调用其他返回字符串的函数会发生什么?(确保它不是一个常量字符串。编写自己的函数,在返回之前调用UniqueString(Result)。)

FormatSettings变量是线程特定的吗?这就是为什么FormatDateTime有额外参数的原因,所以每个线程都有自己的私有副本,保证在函数活动期间不会被任何其他线程修改。

计时器队列对这个问题很重要吗?或者当你在Execute方法中使用普通的TThread运行循环时,你得到相同的结果吗?

你确实警告说这是一个长问题,但似乎有一些事情可以做来缩小问题的范围。


0

针对update2:

有一个免费的IOCP套接字组件:http://www.torry.net/authorsmore.php?id=7131(包括源代码)

“由Naberegnyh Sergey N.开发的高性能套接字服务器,基于Windows完成端口并使用Windows套接字扩展。支持IPv6。”

我在寻找更好的组件/库来重新架构我的小型即时通讯服务器时发现了它。虽然我还没有尝试过,但第一印象是它看起来编码良好。


0

我想知道你是否正在进行RTL/VCL调用时,期望访问某些线程本地存储(TLS)变量,但在通过计时器队列调用代码时没有正确设置?

这不是你问题的答案,但你是否意识到TTimer OnTimer事件只是作为主VCL线程中正常消息循环的一部分运行?


0
你找到了答案 - IsMultiThread。这必须在任何时候使用,以恢复使用API并创建线程。来自MSDN:CreateTimerQueueTimer正在创建一个线程池来处理此功能,因此您有一个外部线程与主VCL线程一起工作,没有保护。(注意:除非代码的其他部分尊重此锁定,否则CS.acquire/release不会起作用。)

我不太清楚为什么“CS.acquire/release没有任何作用”,因为我认为它应该在两个地方保护代码FormatDateTime,但我想主VCL可能在其他地方使用内存管理器,这是我无法轻易控制并导致问题的原因。 - Bruce

0
关于您上次关于Winsock和重叠I/O的问题:您应该仔细研究Indy。
Indy使用阻塞I/O,并且是在主线程执行任何操作时都能提供高性能网络I/O的绝佳选择。现在您已经解决了多线程问题,您只需创建另一个线程(或更多)来使用Indy来处理您的I/O即可。

0
Indy 的问题在于如果你需要多个连接,它根本不高效。它需要每个连接一个线程(阻塞 I/O),这完全不可扩展,因此 IOCP 和 Overlapping IO 的好处,它几乎是 Windows 上唯一可伸缩的方式。

当然可以,但在许多情况下,我认为这通常是过早优化,会带来很多麻烦。不过,这个线程可能会给你一些指针。https://dev59.com/43VD5IYBdhLWcg3wQZQg - Roddy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接