Winform服务总线消息接收器中如何避免跨线程操作无效异常

How to avoid cross-thread operation not valid exception in Winform service bus message receiver

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

控制台应用代码如下:

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 中将服务总线消息显示为字符串。
我用控制台应用程序代码创建了一个新的 Class (Azure),并在主窗体中调用该方法。

Class蔚蓝

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);
    }
}

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

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

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

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

当然你不能从另一个线程引用在UI线程中创建的控件;正如您所注意到的,当您尝试执行以下操作时会引发 Invalid Cross-thread operation 异常:Windows 表单应用程序必须是单线程的,原因在 STAThreadAttribute Class 文档中有很好的解释。

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

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

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

表格面:

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

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

蔚蓝class方:

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(): 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 属性.
当然,见Parallel Computing - It's All About the SynchronizationContext.

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

表格面:

// 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);
// [...]

蔚蓝class方:

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() 方法。
这就是为什么在这种情况下,此方法的优先级较低(但您仍然可以使用它)。

BeginInvoke()不需要检查Control.InvokeRequired:这个方法可以从任何线程调用,包括UI线程。调用 Invoke() 需要检查,因为如果从 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);

蔚蓝class方:

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()方法。

我没有在此处实现它,因为 Dispatcher 需要引用 WindowsBase.dll(通常是 WPF),这可能会在 WinForms 应用程序中导致不良影响那不是 DpiAware。
你可以在这里阅读:
DPI Awareness - Unaware in one Release, System Aware in the Other

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