C# 多线程——无控件调用

发布于 2024-08-11 21:32:54 字数 721 浏览 2 评论 0原文

我对多线程只是有点熟悉,因为我读过它,但从未在实践中使用过它。

我有一个项目使用第三方库,通过引发事件来共享输入设备的状态。问题是,库的编写方式是从不同的线程引发这些事件。

我的应用程序不需要是多线程的,而且我遇到了很多经典的线程问题(UI 控件抱怨与不同线程进行交互,当一段代码迭代时集合被修改,等等) .)。

我只想将第 3 方库的事件返回给我的 UI 线程。具体来说,我认为应该发生的是:

我的类接收事件,并且处理程序在与 UI 不同的线程上运行。我想检测这种情况(就像使用 InvokeRequired 一样),然后执行相当于 BeginInvoke 的操作,将控制权交还给 UI 线程。然后,可以在类层次结构上发送适当的通知,并且我的所有数据仅由一个线程触及。

问题是,接收这些输入事件的类不是从 Control 派生的,因此没有 InvokeRequired 或 BeginInvoke。这样做的原因是我试图将 UI 和底层逻辑彻底分开。该类仍在 UI 线程上运行,只是类本身内部没有任何 UI。

现在我通过破坏这种分离来解决这个问题。我传入对将显示我的类中的数据并使用 Invoke 方法的控件的引用。这似乎违背了分离它们的全部目的,因为现在底层类直接依赖于我的特定 UI 类。

也许有一种方法可以保存对运行构造函数的线程的引用,然后 Threading 命名空间中有一些东西可以执行 Invoke 命令?

有办法解决这个问题吗?我的做法完全错误吗?

I am only somewhat familiar with multi-threading in that I've read about it but have never used it in practice.

I have a project that uses a third party library that shares the status of an input device by raising events. The problem is, the way the library is written these events are raised from a different thread.

My application does not need to be multi-threaded and I've run into a lot of classic threading issues (UI controls complaining about being interacted with from a different thread, collections that get modified as one piece of code is iterating over it, etc.).

I just want the 3rd party library's event to be given back to my UI thread. Specifically what I think should happen is:

My class receives the event and the handler is being run on a different thread than the UI. I want to detect this condition (like with InvokeRequired), and then perform the equivalent of BeginInvoke to give control back to the UI thread. Then the proper notifications can be sent on up the class hierarchy and all of my data is only touched by the one thread.

The problem is, the class that is receiving these input events is not derived from Control and therefore doesn't have InvokeRequired or BeginInvoke. The reason for this is that I tried to cleanly separate UI and the underlying logic. The class is still being run on the UI thread, it just doesn't have any UI inside the class itself.

Right now I fixed the issue by ruining that separation. I pass in a reference to the control that will be displaying data from my class and using its Invoke methods. That seems like it defeats the whole purpose of separating them because now the underlying class has a direct dependence on my specific UI class.

Perhaps there's a way to save a reference to the thread that ran the constructor and then there's something in the Threading namespace that will perform the Invoke commands?

Is there a way around this? Is my approach completely wrong?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

风追烟花雨 2024-08-18 21:32:54

查看 AsyncOperation班级。您可以在要使用 AsyncOperationManager.CreateOperation 方法。我用于 Create 的参数通常为 null,但您可以将其设置为任何值。要调用该线程上的方法,请使用 AsyncOperation.Post 方法。

Look into the AsyncOperation class. You create an instance of AsyncOperation on the thread you want to call the handler on using the AsyncOperationManager.CreateOperation method. The argument I use for Create is usually null, but you can set it to anything. To call a method on that thread, use the AsyncOperation.Post method.

岁月打碎记忆 2024-08-18 21:32:54

使用 SynchronizationContext.Current,它将指向您可以与之同步的东西。

根据应用程序的类型,这将做正确的事情™。对于 WinForms 应用程序,它将在主 UI 线程上运行。

具体来说,使用 SynchronizationContext.Send 方法,像这样:

SynchronizationContext context =
    SynchronizationContext.Current ?? new SynchronizationContext();

context.Send(s =>
    {
        // your code here
    }, null);

Use SynchronizationContext.Current, which will point to something that you can synchronize with.

This will do the right thing™ depending on the type of application. For a WinForms application, it will run this on the main UI thread.

Specifically, use the SynchronizationContext.Send method, like this:

SynchronizationContext context =
    SynchronizationContext.Current ?? new SynchronizationContext();

context.Send(s =>
    {
        // your code here
    }, null);
被翻牌 2024-08-18 21:32:54

处理方法可以简单地将数据存储到类的成员变量中。当您想要将线程更新为不是在该线程上下文中创建的控件时,就会出现跨线程的唯一问题。因此,您的泛型类可以侦听该事件,然后使用委托函数调用您想要更新的实际控件。

同样,只有您想要更新的 UI 控件才需要被调用,以确保它们是线程安全的。不久前,我在“C# 中非法跨线程调用的简单解决方案"

这篇文章介绍了更多细节,但一个非常简单(但有限)方法的关键是在您的 UI 控件上使用匿名委托函数想要更新:

if (label1.InvokeRequired) {
  label1.Invoke(
    new ThreadStart(delegate {
      label1.Text = "some text changed from some thread";
    }));
} else {
  label1.Text = "some text changed from the form's thread";
}

我希望这有帮助。 InvokeRequired 在技术上是可选的,但调用控件的成本相当高,因此检查确保在不需要时不会通过调用更新 label1.Text。

The handling method could simply store the data into a member variable of the class. The only issue with cross-threading occurs when you want to update threads to controls not created in that thread context. So, your generic class could listen to the event, and then invoke the actual control you want to update with a delegate function.

Again, only the UI controls that you want to update need to be invoked to make them thread safe. A while ago I wrote a blog entry on a "Simple Solution to Illegal Cross-thread Calls in C#"

The post goes into more detail, but the crux of a very simple (but limited) approach is by using an anonymous delegate function on the UI control you want to update:

if (label1.InvokeRequired) {
  label1.Invoke(
    new ThreadStart(delegate {
      label1.Text = "some text changed from some thread";
    }));
} else {
  label1.Text = "some text changed from the form's thread";
}

I hope this helps. The InvokeRequired is technically optional, but Invoking controls is quite costly, so that check ensure it doesn't update label1.Text through the invoke if its not needed.

强辩 2024-08-18 21:32:54

您不需要特定的控件,任何控件(包括表单)都可以。所以你可以将它从 UI 中抽象出来。

You don't need the specific control, any control (including the Form) will do. So you could abstract it away from the UI somewhat.

莫言歌 2024-08-18 21:32:54

如果您使用的是 WPF:

您需要对管理 UI 线程的 Dispatcher 对象的引用。然后,您可以使用调度程序对象上的 Invoke 或 BeginInvoke 方法来安排在 UI 线程中发生的操作。

获取调度程序的最简单方法是使用 Application.Current.Dispatcher。这是负责主(也可能是唯一)UI 线程的调度程序。

把它们放在一起:

class MyClass
{
    // Can be called on any thread
    public ReceiveLibraryEvent(RoutedEventArgs e)
    {
        if (Application.Current.CheckAccess())
        {
            this.ReceiveLibraryEventInternal(e);
        }
        else
        {
            Application.Current.Dispatcher.Invoke(
                new Action<RoutedEventArgs>(this.ReceiveLibraryEventInternal));
        }
    }

    // Must be called on the UI thread
    private ReceiveLibraryEventInternal(RoutedEventArgs e)
    {
         // Handle event
    }
}

If you are using WPF:

You need a reference to the Dispatcher object which manages the UI thread. Then you can use the Invoke or BeginInvoke method on the dispatcher object to schedule an operation which takes place in the UI thread.

The simplest way to get the dispatcher is using Application.Current.Dispatcher. This is the dispatcher responsible for the main (and probably the only) UI thread.

Putting it all together:

class MyClass
{
    // Can be called on any thread
    public ReceiveLibraryEvent(RoutedEventArgs e)
    {
        if (Application.Current.CheckAccess())
        {
            this.ReceiveLibraryEventInternal(e);
        }
        else
        {
            Application.Current.Dispatcher.Invoke(
                new Action<RoutedEventArgs>(this.ReceiveLibraryEventInternal));
        }
    }

    // Must be called on the UI thread
    private ReceiveLibraryEventInternal(RoutedEventArgs e)
    {
         // Handle event
    }
}
七度光 2024-08-18 21:32:54

我刚刚遇到了同样的情况。但是,就我而言,我无法使用 SynchronizationContext.Current,因为我无权访问任何 UI 组件,也没有回调来捕获当前同步上下文。事实证明,如果代码当前未在 Windows 窗体消息泵中运行,SynchronizationContext.Current 将设置为标准 SynchronizationContext,它将仅在当前线程上运行 Send 调用,并在 ThreadPool 上运行 Post 调用。

我发现这个答案解释了不同类型的同步上下文。就我而言,解决方案是在线程上创建一个新的 WindowsFormsSynchronizationContext 对象,该对象稍后将使用 Application.Run() 启动消息泵。然后,其他线程可以使用此同步上下文在 UI 线程上运行代码,而无需接触任何 UI 组件。

I just ran into the same situation. However, in my case I couldn't use SynchronizationContext.Current, because I did not have access to any UI components and no callback to capture the current synchronization context. It turns out that if the code is not currently running in a Windows Forms messge pump, SynchronizationContext.Current will be set to a standard SynchronizationContext, which will just run Send calls on the current thread and Post calls on the ThreadPool.

I found this answer explaining the different types of synchronization contexts. In my case the solution was to create a new WindowsFormsSynchronizationContext object on the thread that would later start the message pump using Application.Run(). This synchronization context can then be used by other threads to run code on UI thread, without ever touching any UI components.

樱娆 2024-08-18 21:32:54

有办法解决这个问题吗?

是的,解决方法是创建一个线程安全队列。

  • 您的事件处理程序由第 3 方线程调用
  • 您的事件处理程序将某些内容(事件数据)排队到您拥有的集合(例如列表)中
  • 您的事件处理程序执行一些操作来向您自己的主题发出信号,表明集合中有数据出队并处理:
    • 你的线程可能正在等待某些东西(互斥锁或其他东西);当事件处理程序向其互斥体发出信号时,它会醒来并检查队列。
    • 或者,它可以定期唤醒(例如每秒一次或其他方式)并轮询队列,而不是收到信号。

在任何一种情况下,因为您的队列是由两个不同的线程写入(第 3 方线程正在排队,而您的线程正在出队),所以它需要是一个线程安全的受保护队列。

Is there a way around this?

Yes, the work-around would be for you to create a thread-safe queue.

  • Your event handler is invoked by the 3rd-party thread
  • Your event handler enqueues something (event data) onto a collection (e.g. a List) which you own
  • Your event handler does something to signal your own thead, that there's data in the collection for it to dequeue and process:
    • Your thread could be waiting on something (a mutex or whatever); when its mutex is signalled by the event handler, it wakes up and checks the queue.
    • Alternatively, instead of being signalled, it could wake up periodically (e.g. once per second or whatever) and poll the queue.

In either case, because your queue is being written by two different threads (the 3rd-party thread is enqueueing, and your thread is dequeueing), it needs to be a thread-safe, protected queue.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文