在另一个线程中引发异常

5
如何在Delphi的另一个线程中引发异常? 我有���程1和线程2,我想在线程1中引发异常,并在线程2中捕获。
编辑: 我现在可以看到我的初始解释很混乱。我的意图是从线程1开始引发线程2中的异常。因此,在线程2中引发并捕获异常,但该过程受到线程1的控制。 假设我有一个创建工作线程的主线程。我需要一种机制来从主线程优雅地停止工作线程,但由于某些原因(这里不相关),我不能使用TThread.Terminate/Terminated模式。因此,我想如果我可以从主线程中注入一个异常引发到工作线程中,那么它就可以用作停止信号。

1
你问这个问题让我毛骨悚然。 ;-)一个线程应该只做一件事情,完全自包含,不干扰其他线程。 - Nick Hodges
@Nick - 但是考虑到当您的线程在意外情况下崩溃并且用户需要了解该情况时,您需要通知主线程。在这种情况下,您需要在线程中处理异常,并且当发生某些事情时,通过发布有关错误的消息来简单地通知主线程,就像此示例所示的那样。我的看法是这是一个好方法。 - user532231
@daemon_X,平均线程将通过线程的“FatalException”属性收到通知。 - Johan
@Johan - 更准确地说,您的线程已终止,您需要检查线程的 OnTerminate 事件是否已分配其 FatalException。但是,如果您仍然想要通知主线程,则需要使用一些同步机制。 - user532231
@Max - 所以你只想从主线程向工作线程传递一些变量(带有异常数据)。 - user532231
@daemon_x 其实我只想传递一个没有任何数据的信号。 - Max
6个回答

5

实际上我想要实现的有点不同。 - Max

3
你可以通过设置布尔标志并让线程检查其状态来发出取消线程的信号,并做出相应的响应。控制线程设置标志,然后工作线程执行必要的操作以中止线程。你必须定期检查标志的状态。
这样的解决方案将是内置的 Terminated 方法的重新实现,但你表示无法使用 Terminated。我认为这会让你陷入困境。线程不能被强制安全可靠地终止,因此你需要一种协作式的方法。
我强烈建议你重新设计架构,使 Terminated 的使用成为可能。

实际上我想要做的是相反的,我想要从主线程向工作线程发送一个信号。 - Max
1
@Max,线程不是事件驱动的,你不能“发送信号”并让线程对此做出反应(除非线程实现了消息泵,但线程通常不会这样做)。所有同步对象都需要线程本身的合作,包括事件和信号量。由于任何你想做的事情都需要向现有线程添加代码,因此最好添加一些if Terminated then Exit;语句,并使你的线程支持通常的终止-终止概念。 - Cosmin Prund
@Cosmin Prund,为什么您认为发送信号是不可能的?在 .Net 中,即使没有线程代码的配合,也可以通过 Thread.Abort 实现。我认为在 Delphi 中也应该是可能的。 - Max
@Max - 你可能想要使用TThread.Suspend这个方法,但是要避免使用它,因为它现在已经被弃用了。无论如何,它会立即停止你的线程运行。 - user532231
我知道很多人反对使用TThread.Suspend方法(包括Embarcadero文档 :),但我仍然认为,如果您只控制一个工作线程,则不会发生任何事情,该线程不会受到另一个线程的影响(除了TThread.Resume),并且当您确保在应用程序终止时恢复它时。简而言之,就像调试器一样。 - user532231
@Max,看我的回答。我实现了类似于.NET的东西,使用了你停止线程、改变指令指针并重新启动线程的建议。我的回答包含完整的示例代码。享受吧。 - Cosmin Prund

3
下面是一段引发异常进入其他线程的示例代码,它使用SuspendThread停止线程,GetThreadContext读取线程寄存器,改变EIP(指令指针),使用SetThreadContext然后ResumeThread重新启动线程。 它可以正常工作!
UKilThread单元
一个精美打包的可重用单元,提供AbortThread()例程:
unit UKillThread;

interface

uses Classes, Windows, SysUtils;

procedure AbortThread(const Th: TThread);

implementation

// Exception to be raized on thread abort.
type EThreadAbort = class(EAbort);

// Procedure to raize the exception. Needs to be a simple, parameterless procedure
// to simplify pointing the thread to this routine.
procedure RaizeThreadAbort;
begin
  raise EThreadAbort.Create('Thread was aborted using AbortThread()');
end;

procedure AbortThread(const Th: TThread);
const AlignAt = SizeOf(DWORD); // Undocumented; Apparently the memory used for _CONTEXT needs to be aligned on DWORD boundary
var Block:array[0..SizeOf(_CONTEXT)+512] of Byte; // The _CONTEXT structure is probably larger then what Delphi thinks it should be. Unless I provide enough padding space, GetThreadContext fails
    ThContext: PContext;
begin
  SuspendThread(Th.Handle);
  ZeroMemory(@Block, SizeOf(Block));
  ThContext := PContext(((Integer(@Block) + AlignAt - 1) div AlignAt) * AlignAt);
  ThContext.ContextFlags := CONTEXT_FULL;
  if not GetThreadContext(Th.Handle, ThContext^) then
    RaiseLastOSError;
  ThContext.Eip := Cardinal(@RaizeThreadAbort); // Change EIP so we can redirect the thread to our error-raizing routine
  SetThreadContext(Th.Handle, ThContext^);
  ResumeThread(Th.Handle);
end;

end.

演示项目

以下是如何使用AbortThread

program Project23;

{$APPTYPE CONSOLE}

uses
  SysUtils,
  Classes,
  Windows,
  UKillThread;

var Th: TThread;

type
  TTestThread = class(TThread)
  public
    procedure Execute;override;
  end;

{ TTestTrehad }

procedure TTestThread.Execute;
var N: Integer;
begin
  try
    N := 1;
    while not Terminated do
    begin
      WriteLn(N);
      Inc(N);
      Sleep(1000);
    end;
  except on E:Exception do
    WriteLn(E.ClassName + ' / ' + E.Message);
  end;
end;

begin
  Th := TTestThread.Create(False);
  WriteLn('Press ENTER to raize exception in Thread');
  ReadLn;
  AbortThread(Th);
  WriteLn('Press ENTER to exit');
  ReadLn;
end.

免责声明

在您使用此代码之前,请确保您了解其功能。该代码并不是适当的 Terminate - Terminated 逻辑(即协作式线程关闭)的替代品,但它比使用 TerminateThread() 更好。此代码是基于.NET Thread.Abort() 方法建模而成。我不知道实际的 .NET 方法是如何实现的,但请仍然阅读一下,因为使用此代码可能会遇到类似的潜在问题:

  • 该方法实际上并没有终止线程,而是在线程上下文中引发一个 EAbort 派生异常。线程的代码可能会捕获该异常。这很不可能发生,因为 EAbort 异常不应被处理。
  • 该方法可能在任何时间停止线程。它可能会在线程处理 finally 部分或设置新异常帧时停止线程。即使您的线程使用适当的 try-finally 块,如果在分配资源给变量之前分配资源后引发异常,可能会导致内存或资源泄漏。
  • 如果在线程进入临界区之后立即中断线程,而在通常情况下遵循的 try-finally 之前,则可能会导致死锁。 EnterCriticalSection 的 MSDN 页面 提到:“如果一个线程在拥有临界区所有权时终止,则临界区的状态是未定义的。” 这让我感到意外,我的直觉认为当拥有线程终止时,关键部分应该被“释放”,但显然不是这样。

非常聪明,但相当不切实际。这是死锁的诱饵。您应该在回答中说明,并建议没有人真正使用此代码。 - David Heffernan
1
@Cosmin 不,那样做不对。你的异常可能会插入在调用EnterCriticalSection和紧随其后的Try/Finally之间。因此,是的,它会导致死锁。 - David Heffernan
@David,你在我编辑的时候进行了修改,我添加了一个长免责声明。 - Cosmin Prund
@Cosmin,虽然你应该提到这段代码可能会导致死锁。我的修改是将Abrot更改为Abort。您想做出更正吗? - David Heffernan
我已经实现了类似的东西,尽管Get/SetThreadContext完全可以工作:var ctx: CONTEXT; SetThreadContext(aThread.Handle, ctx)。我在想也许有可能用更少的hackish方法实现它。 - Max
显示剩余6条评论

2

这是不可能的,Delphi并不重要。异常信息存储在堆栈中,而堆栈属于线程(每个线程都有自己的堆栈)。因此,您必须在同一线程中引发和处理异常。


@Max:如果您在不同的线程中执行代码(使用Synchronize或Queue方法),则代码引发的异常只能在相同(不同)的线程中捕获。

可能会出现线程A引发并捕获异常,将异常对象传递给线程B,然后线程B重新引发异常的情况,但线程B绝对无法捕获由线程A引发的异常,因为每个线程都有自己的堆栈。


可以在另一个线程上下文中执行任意代码。所以我想执行引发异常的代码是可能的。你为什么认为这是不可能的? - Max
Max,在你的评论中所描述的并不是你在问题中所询问的。你的评论描述了如何在线程1中引发一个异常,并从线程1中引发线程2,而你的问题则描述了如何在线程1中引发一个异常,并以某种方式将控制权转移到线程2,这是不可能的,除非线程1首先捕获异常并通过通常的线程通信机制通知线程2。但如果你知道如何将代码注入到另一个线程中,请随意自行回答此问题。 - Rob Kennedy
@Max,请解释如何在另一个线程上下文中执行任意代码。 - David Heffernan
@David Heffernan 我可以考虑挂起线程,然后更改线程上下文,使其指向我们想要执行的代码,然后恢复线程。 - Max
1
这是死锁的配方。 - David Heffernan

0
扩展并可能简化@David的答案:我在我的线程类中添加了公共错误消息和errorState属性。如果线程中发生异常,我会处理或吃掉它(取决于何时适用),并使用异常信息等设置错误属性。
主线程在thread.onTerminate事件(在主线程中运行)中检查线程类错误属性,并在必要时通知前端/用户,显示从线程返回的异常信息。
希望对你有所帮助。

但是你必须将线程的 OnTerminate 事件绑定到主线程的自己的 TNotifyEvent 上才能实现它(正如 Rob 在这里指出的那样:https://dev59.com/IHA65IYBdhLWcg3wyh97#3627964)。而且你不需要添加异常属性,你可以通过 FatalException 属性获取一个(Exception 类)。事件不能运行,它们只是发生 :) - user532231
@daemon_x 4 - 看起来是对的,但这仅适用于确实是致命异常的情况。至于事件,我理解 :-) 。当然,它们实际上只是像任何其他函数一样运行的函数 - 只是您不会显式调用它们。让我们同意它们既不会“运行”也不会“发生” - 它们会“触发”... - Vector
在我看来,任何未处理的异常都会导致线程终止,无论它有多严重。让我们来看看文档(除了描述中的第一句话,它属于FreeOnTerminate属性 :))。 - user532231

0
将所有的线程都设置为“消息”处理线程,并且当消息处理失败时,生成类似异常的消息传递给需要知道的任何其他线程/主线程。我在我的分布式多线程应用框架 这里 中使用了这种架构。

为什么不建议OP使用同步对象?消息处理会引入巨大的开销。 - mg30rg

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