C#中是否有一种在给定线程上引发异常的好方法

时间:2020-03-05 18:47:52  来源:igfitidea点击:

我要编写的代码是这样的:

void MethodOnThreadA()
{
    for (;;)
    {
        // Do stuff
        if (ErrorConditionMet)
            ThrowOnThread(threadB, new MyException(...));
    }
}

void MethodOnThreadB()
{
    try
    {
        for (;;)
        {
            // Do stuff
        }
    }
    catch (MyException ex)
    {
        // Do the right thing for this exception.
    }
}

我知道我可以让线程B定期以线程安全的方式检查线程A是否已设置标志,但这会使代码更加复杂。我可以使用更好的机制吗?

这是一个定期检查的充实示例:

Dictionary<Thread, Exception> exceptionDictionary = new Dictionary<Thread, Exception>();

void ThrowOnThread(Thread thread, Exception ex)
{
    // the exception passed in is going to be handed off to another thread,
    // so it needs to be thread safe.
    lock (exceptionDictionary)
    {
        exceptionDictionary[thread] = ex;
    }
}

void ExceptionCheck()
{
    lock (exceptionDictionary)
    {
        Exception ex;
        if (exceptionDictionary.TryGetValue(Thread.CurrentThread, out ex))
            throw ex;
    }
}

void MethodOnThreadA()
{
    for (;;)
    {
        // Do stuff
        if (ErrorConditionMet)
            ThrowOnThread(threadB, new MyException(...));
    }
}

void MethodOnThreadB()
{
    try
    {
        for (;;)
        {
            // Do stuff
            ExceptionCheck();
        }
    }
    catch (MyException ex)
    {
        // Do the right thing for this exception.
    }
}

解决方案

回答

有足够的异常问题可以通过其他机制(例如中止线程之类)引发在线程上抛出,因此我们应该找到另一种方法。

例外情况是一种机制,用于指示一个进程经历了无法处理的异常事件。我们应尽量避免编写代码,以便使用异常来表示其他事物经历了异常。

在代码可能抛出异常的所有情况下,另一个线程很可能不知道如何处理该异常。

简而言之,除了使用异常之外,我们还应该找到其他一些中止线程的机制。

使用事件对象或者类似对象告诉线程中止其处理,这是最好的方法。

回答

我很想知道我们为什么要这样做。这不是一个简单的方法,因为这不是一个好习惯。我们可能应该回到设计中,并找出一种更干净的方法来实现最终目标。

回答

我不认为这是个好主意。
对此问题要另辟crack径尝试使用其他机制(如共享数据)在线程之间发出信号。

回答

像其他人一样,我不确定这是一个好主意,但是如果我们确实想这样做,则可以创建SynchronizationContext的子类,该子类允许将委托发布和发送到目标线程(如果是WinForms线程,则可以完成工作)已为我们完成,因为已经存在此类子类)。但是,目标线程必须实现某种等效的消息泵,才能接收委托。

回答

这不是一个好主意

本文讨论了ruby的超时库。跨线程抛出异常。

它说明了如何从根本上打破这种事情。它不仅在ruby中被破坏,而且在任何会在线程之间引发异常的地方都被破坏。

简而言之,可以(并且确实)发生的事情是这样的:

线程A:

At some random time, throw an exception on thread B:

线程B:

try {
    //do stuff
} finally {
    CloseResourceOne();
    // ThreadA's exception gets thrown NOW, in the middle 
    // of our finally block and resource two NEVER gets closed.
    // Obviously this is BAD, and the only way to stop is to NOT throw
    // exceptions across threads
    CloseResourceTwo();
}

"定期检查"示例很好,因为我们实际上并未跨线程抛出异常。
我们只是设置一个标记,上面写着"下次我们查看该标记时会抛出异常",这很好,因为它不会遭受"可能在捕获的中间抛出或者最终阻塞"的问题。
但是,如果要执行此操作,则最好只设置一个" exitnow"标志,并使用它来避免创建异常对象的麻烦。挥发性的bool就可以了。

回答

@猎户座爱德华兹

我同意观点,即在finally块中引发了异常。

但是,我认为有一种方法可以使用另一种线程来使用这种异常即中断的想法。

线程A:

At some random time, throw an exception on thread C:

线程B:

try {
    Signal thread C that exceptions may be thrown
    //do stuff, without needing to check exit conditions
    Signal thread C that exceptions may no longer be thrown
}
catch {
    // exception/interrupt occurred handle...
}
finally {
    // ...and clean up
    CloseResourceOne();
    CloseResourceTwo();
}

线程C:

while(thread-B-wants-exceptions) {
        try {
            Thread.Sleep(1) 
        }
        catch {
            // exception was thrown...
            if Thread B still wants to handle exceptions
                throw-in-B
        }
    }

还是只是愚蠢?

回答

在研究另一个问题时,我遇到了这篇文章,它使我想起了问题:

使用Rotor探查ThreadAbortException的深度

它显示了.NET实现Thread.Abort()所经历的旋转-大概任何其他跨线程异常都必须是相似的。 (耶!)

回答

Orion Edwards所说的并非完全正确:不是"唯一"的方式。

// Obviously this is BAD, and the only way to stop is to NOT throw
// exceptions across threads

在Callow中使用CER(受约束的执行区域)可以使我们以原子操作的形式释放资源,从而保护代码免遭线程间异常的侵害。 .NET Framework的几类可以与Windows的本机API一起使用此技术,未发布的句柄可能会导致内存泄漏。

请参阅http://msdn.microsoft.com/zh-cn/library/system.runtime.compilerservices.runtimehelpers.prepareconstrainedregions.aspx

以下示例显示了如何通过使用PrepareConstrainedRegions方法来可靠地设置句柄。为了可靠地将句柄设置为指定的预先存在的句柄,必须确保本机句柄的分配以及在" SafeHandle"对象中该句柄的后续记录是原子的。这些操作之间的任何失败(例如线程中止或者内存不足异常)都将导致本机句柄被泄漏。我们可以使用PrepareConstrainedRegions方法来确保句柄没有泄漏。

就像:

public MySafeHandle AllocateHandle()
{
    // Allocate SafeHandle first to avoid failure later.
    MySafeHandle sh = new MySafeHandle();

    RuntimeHelpers.PrepareConstrainedRegions();
    try { }
    finally  // this finally block is atomic an uninterruptible by inter-thread exceptions
    {
        MyStruct myStruct = new MyStruct();
        NativeAllocateHandle(ref myStruct);
        sh.SetHandle(myStruct.m_outputHandle);
    }

    return sh;
}