从另一个线程调用方法而不阻塞线程(或为非 UI 线程编写自定义同步上下文)C#
本文关键字:线程 UI 自定义 同步 上下文 方法 调用 另一个 | 更新日期: 2023-09-27 18:35:17
这可能是Stackoverflow中最常见的问题之一,但是我找不到我的问题的确切答案:我想设计一种模式,它允许从线程 A 启动线程 B,并在特定条件下(例如发生异常时)调用线程 A 中的方法。在异常的情况下,正确的线程非常重要,因为异常必须在主线程 A 中调用 catch 方法。如果线程 A 是 UI 线程,那么一切都很简单(调用 .Invoke()
或 .BeginInvoke()
仅此而已)。UI 线程有一些机制是如何完成的,我想获得一些见解,如何为非 UI 线程编写自己的机制。通常建议的方法是使用消息抽
取 http://www.codeproject.com/Articles/32113/Understanding-SynchronizationContext-Part-II
但是while
循环会阻塞线程 A,这不是我需要的,也不是 UI 线程处理此问题的方式。有多种方法可以解决此问题,但我想更深入地了解这个问题,并独立于所选方法编写我自己的通用实用程序,例如使用 System.Threading.Thread
或 System.Threading.Tasks.Task
或 BackgroundWorker
或其他任何东西,并且独立地是否有 UI 线程(例如控制台应用程序)。
下面是示例代码,我尝试使用它来测试异常的捕获(这清楚地表明异常被抛到的错误线程)。我将它用作具有所有锁定功能的实用程序,检查线程是否正在运行等,这就是我创建类实例的原因。
class Program
{
static void Main(string[] args)
{
CustomThreads t = new CustomThreads();
try
{
// finally is called after the first action
t.RunCustomTask(ForceException, ThrowException); // Runs the ForceException and in a catch calls the ThrowException
// finally is never reached due to the unhandled Exception
t.RunCustomThread(ForceException, ThrowException);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
// well, this is a lie but it is just an indication that thread B was called
Console.WriteLine("DONE, press any key");
Console.ReadKey();
}
private static void ThrowException(Exception ex)
{
throw new Exception(ex.Message, ex);
}
static void ForceException()
{
throw new Exception("Exception thrown");
}
}
public class CustomThreads
{
public void RunCustomTask(Action action, Action<Exception> action_on_exception)
{
Task.Factory.StartNew(() => PerformAction(action, action_on_exception));
}
public void RunCustomThread(Action action, Action<Exception> action_on_exception)
{
new Thread(() => PerformAction(action, action_on_exception)).Start();
}
private void PerformAction(Action action, Action<Exception> action_on_exception)
{
try
{
action();
}
catch (Exception ex)
{
action_on_exception.Invoke(ex);
}
finally
{
Console.WriteLine("Finally is called");
}
}
}
我发现的一个更有趣的功能是,new Thread()
抛出未经处理的异常,并且永远不会调用finally
,而new Task()
不会调用,并且调用finally
。也许有人可以评论这种差异的原因。
而不是 UI 线程处理此问题的方式
这是不准确的,这正是 UI 线程处理它的方式。 消息循环是生产者-消费者问题的通用解决方案。 在典型的 Windows 程序中,操作系统以及其他进程生成消息,并且使用唯一的 UI 线程。
此模式是处理从根本上不安全的代码所必需的。 而且周围总是有很多不安全的代码,它越复杂,它被线程安全的几率就越低。 在 .NET 中可以看到,很少有类在设计上是线程安全的。 列表这么简单<>不是线程安全的,您可以使用 lock 关键字来确保它的安全。 GUI代码是非常不安全的,再多的锁定也无法使其安全。
不仅仅是因为很难弄清楚将 lock 语句放在哪里,还涉及一堆不是您编写的代码。 如消息挂钩、UI 自动化、将对象放在剪贴板上的程序(粘贴、拖放)、使用 shell 对话框(如 OpenFileDialog)时运行的 shell 扩展。 所有这些代码都是线程不安全的,主要是因为它的作者不必使其线程安全。 如果您在此类代码中绊倒了线程错误,那么您就没有电话号码可以拨打,并且完全无法解决。
使方法调用在特定线程上运行需要这种帮助。 不可能任意中断线程正在执行的任何操作并强制它调用方法。 这会导致可怕的且完全无法调试的重入问题。 就像由DoEvents()引起的那种问题,但乘以一千。 当代码进入调度程序循环时,它将隐式"空闲",而不是忙于执行自己的代码。 因此可以从消息队列中获取执行请求。 这仍然可能出错,当你不闲着的时候,当你抽水时,你会把你的腿射掉。 这就是为什么DoEvents()如此危险。
所以这里没有快捷方式,你确实需要处理 while() 循环。 可以这样做是你有非常坚实的证据的东西,UI 线程做得很好。 考虑创建自己的。