Winform服务总线消息接收器如何避免跨线程操作无效异常 [英] How to avoid cross-thread operation not valid exception in Winform service bus message receiver

查看:40
本文介绍了Winform服务总线消息接收器如何避免跨线程操作无效异常的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

开发了一个运行良好的 Azure 服务总线消息接收器控制台应用程序控制台应用程序.

Have developed an Azure service bus message receiver console app which is working fine console app.

控制台应用程序代码如下:

Code for console app as follows:

using System.IO;
using Microsoft.ServiceBus.Messaging;

class Program
{
    static void Main(string[] args)
    {
        const string connectionString = "Endpoint=sb://sbusnsXXXX.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=bkjk3Qo5QFoILlnay44ptlukJqncoRUaAfR+KtZp6Vo=";
        const string queueName = "bewtstest1";
        var queueClient = QueueClient.CreateFromConnectionString(connectionString, queueName);

        try
        {
            queueClient.OnMessage(message => {
                string body = new StreamReader(message.GetBody<Stream>(), Encoding.UTF8).ReadToEnd();                                        
                Console.WriteLine(body);
                message.Complete();                    
            });
            Console.ReadLine();
        }
        catch (Exception ex)
        {
            queueClient.OnMessage(message => {
                Console.WriteLine(ex.ToString());
                message.Abandon();                    
            });
            Console.ReadLine();
        }            
    }
}

已尝试转换为 WinForms 应用程序,因此我可以将服务总线消息显示为 ListBox 中的字符串.
我使用控制台应用程序代码创建了一个新类 (Azure),并在主窗体中调用该方法.

Have tried to convert to a WinForms app, so I can show the service bus message as a string in a ListBox.
I have created a new Class (Azure) with the console app code, and call the method in the main form.

Azure 类:

using System.IO;
using Microsoft.ServiceBus.Messaging;

public class Azure
{
    public static void GetQueue(Form1 form)
    {
        const string connectionString = "Endpoint=sb://sbusnsXXXX.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=bkjk3Qo5QFoILlnay44ptlukJqncoRUaAfR+KtZp6Vo=";
        const string queueName = "bewtstest1";
        var queueClient = QueueClient.CreateFromConnectionString(connectionString, queueName);

        try
        {
            queueClient.OnMessage(message => {
                string body = new StreamReader(message.GetBody<Stream>(), Encoding.UTF8).ReadToEnd();
                //Form1 f = new Form1();                
                form.listBox1.Items.Add(body);
                Console.WriteLine(body);
                message.Complete();
            });
            Console.ReadLine();
        }
        catch (Exception ex)
        {
            queueClient.OnMessage(message => {
                Console.WriteLine(ex.ToString());
                message.Abandon();
            });
            Console.ReadLine();
        }
    }
}

主表单:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        Azure.GetQueue(this);
    }
}

代码编译,但是当收到新的服务总线消息时,我收到以下异常:

The code compiles, however when a new service bus message is received I get the following exception:

System.InvalidOperationException: '跨线程操作无效:控制从它所在线程以外的线程访问的listBox1"创建于.'

System.InvalidOperationException: 'Cross-thread operation not valid: Control 'listBox1' accessed from a thread other than the thread it was created on.'

关于如何避免此异常的任何想法(请注意,我已尝试使用 InvokeRequired 但无法编译代码)?

Any thoughts on how I can avoid this exception (note I have tried using InvokeRequired but can't get the code to compile)?

(感觉就像当我停止并重新运行程序时,我已经接近了,表单加载了 ListBox 中的消息,如下所示:带有消息的列表框!)

(Feel like I'm close as when I stop and re-run the program, the form loads with the message in the ListBox as shown here: listbox with message!)

推荐答案

当然你不能从另一个线程引用在 UI 线程中创建的控件;如您所见,当您尝试执行以下操作时会引发 Invalid Cross-thread operation 异常:Windows 窗体应用程序必须是单线程的,原因在 STAThreadAttribute Class 文档.

Of course you cannot reference a Control created in the UI Thread from another thread; as you have noticed, a Invalid Cross-thread operation exception is raised when you try to: a Windows Forms app must be single-threaded, the reasons are well explained in the STAThreadAttribute Class documentation.

注意:删除所有 Console.ReadLine(),你不能在 WinForms 中使用它(没有控制台).

Note: Remove all Console.ReadLine(), you cannot use that in WinForms (there's no Console).

这里有一些可能适合您的实现,按照与您的上下文相关的顺序排列(好吧,至少我是这么认为的.您选择自己喜欢的).

Here some implementations that may work for you, in order of relevance in your context (well, that's what I think, at least. You pick what you prefer).

Progress:这个类使用起来非常简单.你只需要定义它的返回类型(T 类型,它可以是任何东西,一个简单的string,一个类对象等等).您可以就地定义它(在其中调用线程方法)并传递其引用.就这些.
接收引用的方法调用其Report() 方法,传递由 T 定义的值.
此方法在创建 Progress 对象的线程中执行.
如您所见,您不需要将 Control 引用传递给 GetQueue():

Progress<T>: this class is really simple to use. You just need to define its return type (the T type, it can be anything, a simple string, a class object etc.). You can define it in-place (where you call your threaded method(s)) and pass its reference. That's all.
The method that receives the reference calls its Report() method, passing the value(s) defined by T.
This method is executed in the Thread that created the Progress<T> object.
As you can see, you don't need to pass a Control reference to GetQueue():

表格侧:

// [...]
var progress = new Progress<string>(msg => listBox1.Items.Add(msg));

Azure.GetQueue(progress);
// [...]

Azure 类方面:

public static void GetQueue(IProgress<string> update)
{    
    // [...]
    try {
        queueClient.OnMessage(message => {
            string body = new StreamReader(message.GetBody<Stream>(), Encoding.UTF8).ReadToEnd();
            update.Report(body);
            message.Complete();
         });
    }
    // [...]
}


SynchronizationContext (WindowsFormsSynchronizationContext) Post():这个类用于同步线程上下文,它的Post()方法将异步消息分派到生成类对象的同步上下文,由当前 属性.
当然,参见 并行计算 - 都是关于 SynchronizationContext.


▶ SynchronizationContext (WindowsFormsSynchronizationContext) Post(): this class is used to sync threading contexts, its Post() method dispatches an asynchronous message to the synchronization context where the class object is generated, referenced by the Current property.
Of course, see Parallel Computing - It's All About the SynchronizationContext.

实现与之前的没有太大区别:您可以使用 Lambda 作为 Post() 方法的 SendOrPostCallback 委托.
Action 委托用于发布到 UI 线程,而无需将 Control 引用传递给 Azure.GetQueue() 方法:

The implementation is no much different than the previous: you can use a Lambda as the SendOrPostCallback delegate of the Post() method.
An Action<string> delegate is used to post to the UI Thread without the need to pass a Control reference to the Azure.GetQueue() method:

表格侧:

// Add static Field for the SynchronizationContext object
static SynchronizationContext sync = null;

// Add a method that will receive the Post() using an Action delegate
private void Updater(string message) => listBox1.Items.Add(message);

// Call the method from somewhere, passing the current sync context
sync = SynchronizationContext.Current;
Azure.GetQueue(sync, Updater);
// [...]

Azure 类方面:

public static void GetQueue(SynchronizationContext sync, Action<string> updater)
{    
    // [...]
    try {
        queueClient.OnMessage(message => {
            string body = new StreamReader(message.GetBody<Stream>(), Encoding.UTF8).ReadToEnd();
            sync.Post((spcb) => { updater(body); }, null);
            message.Complete();
         });
    }
    // [...]
}


Control.BeginInvoke():您可以使用 BeginInvoke() 在创建控件句柄的线程上异步执行委托(通常作为 Lambda).
当然,您必须将 Control 引用传递给 Azure.GetQueue() 方法.
这就是为什么在这种情况下,此方法具有较低的偏好(但无论如何您都可以使用它).


▶ Control.BeginInvoke(): you can use BeginInvoke() to execute a delegate (usually as a Lambda) asynchronously on the thread that created a Control's handle.
Of course, you have to pass a Control reference to the Azure.GetQueue() method.
That's why, in this case, this method has a lower preference (but you can use it anyway).

BeginInvoke() 不需要检查 Control.InvokeRequired:该方法可以从任何线程调用,包括 UI 线程.调用 Invoke() 需要进行该检查,因为如果从 UI 线程使用它可能会导致死锁

BeginInvoke() doesn't require to check Control.InvokeRequired: this method can be called from any thread, including the UI Thread. Calling Invoke() instead requires that check, since it could cause a dead-lock if used from the UI Thread

表格侧:

Azure.GetQueue(this, Updater);
// [...]

// Add a method that will act as the Action delegate
private void Updater(string message) => listBox1.Items.Add(message);

Azure 类方面:

public static void GetQueue(Control control, Action<string> action)
{    
    // [...]
    try {
        queueClient.OnMessage(message => {
            string body = new StreamReader(message.GetBody<Stream>(), Encoding.UTF8).ReadToEnd();
            control.BeginInvoke(new Action(()=> action(body));
            message.Complete();
         });
    }
    // [...]
}


您也可以使用 System.Windows.Threading.Dispatcher 来管理线程的排队工作项,调用其 BeginInvoke()(首选)或 Invoke() 方法.
它的实现与 SynchronizationContext 类似,它的方法被称为已经提到的 Control.BeginInvoke() 方法.


You can also use System.Windows.Threading.Dispatcher to manage a Thread's queued work items, calling its BeginInvoke() (preferable) or Invoke() methods.
Its implementation is similar to the SynchronizationContext one and its methods are called as the Control.BeginInvoke() method already mentioned.

我没有在这里实现它,因为 Dispatcher 需要对 WindowsBase.dll(WPF,通常)的引用,这会在 WinForms 应用程序中导致不良影响这不是 DpiAware.
你可以在这里阅读:
DPI 意识 - 一个版本不知道,另一个版本有系统意识

I'm not implementing it here, since the Dispatcher requires a reference to WindowsBase.dll (WPF, usually) and this can cause undesired effects in a WinForms application that is not DpiAware.
You can read about this here:
DPI Awareness - Unaware in one Release, System Aware in the Other

无论如何,如果您有兴趣,请告诉我.

Anyway, in case you're interested, let me know.

这篇关于Winform服务总线消息接收器如何避免跨线程操作无效异常的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆