11

让ThreadA向某个事件的ThreadB发出信号,而不让ThreadB被阻塞等待事件发生的正确技术是什么?

我有一个后台线程将填充一个共享列表<T>。我正在尝试找到一种方法来异步向“主”线程发出信号,表明有数据可供提取。


我考虑使用 EventWaitHandle 对象设置一个事件,但我不能让我的主线程坐在 Event.WaitOne() 上。


我考虑有一个委托回调,但是a)我不希望主线程在委托中工作:线程需要重新开始工作添加更多的东西——我不希望它在委托执行时等待,并且b ) 委托需要编组到主线程上,但我没有运行 UI,我没有控制权来 .Invoke 委托反对。


我认为有一个委托回调,它只是启动一个零间隔 System.Windows.Forms.Timer(线程访问同步的定时器)。这样线程只需要在调用时被卡住

Timer.Enabled = true;

但这似乎是一个黑客行为。

在过去,我的对象会创建一个隐藏窗口,并让线程将消息发布到该隐藏窗口的 HWND。我考虑过创建一个隐藏控件,但我认为你不能在没有创建句柄的控件上调用。另外,我没有 UI:我的对象可能是在 Web 服务器、服务或控制台上创建的,我不希望出现图形控件 - 我也不希望编译对 System.Windows 的依赖项。形式。


我考虑让我的对象公开一个 ISynchronizeInvoke 接口,但是我需要实现 .Invoke(),这就是我的问题。


让线程 A 向某个事件的线程 B 发出信号,而不让线程 B 被阻塞等待事件发生的正确技术是什么?

4

7 回答 7

11

这是 System.ComponentModel.BackgroundWorker 类的代码示例。

    private static BackgroundWorker worker = new BackgroundWorker();
    static void Main(string[] args)
    {
        worker.DoWork += worker_DoWork;
        worker.RunWorkerCompleted += worker_RunWorkerCompleted;
        worker.ProgressChanged += worker_ProgressChanged;
        worker.WorkerReportsProgress = true;

        Console.WriteLine("Starting application.");
        worker.RunWorkerAsync();

        Console.ReadKey();
    }

    static void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        Console.WriteLine("Progress.");
    }

    static void worker_DoWork(object sender, DoWorkEventArgs e)
    {
        Console.WriteLine("Starting doing some work now.");

        for (int i = 0; i < 5; i++)
        {
            Thread.Sleep(1000);
            worker.ReportProgress(i);
        }
    }

    static void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        Console.WriteLine("Done now.");
    }
于 2008-09-23T20:37:57.433 回答
3

我在这里结合了一些回应。

理想情况下使用线程安全标志,例如AutoResetEvent. 您不必在调用时无限期地阻塞WaitOne(),实际上它有一个允许您指定超时的重载。false如果在间隔期间未设置标志,则此重载返回。

AQueue是一种更理想的生产者/消费者关系结构,但如果您的要求迫使您使用List. 主要区别在于您必须确保您的消费者在提取项目时锁定对集合的访问;最安全的可能是使用该CopyTo方法将所有元素复制到数组中,然后释放锁。当然,请确保您的生产者List在锁定期间不会尝试更新。

这是一个简单的 C# 控制台应用程序,它演示了如何实现它。如果你玩弄时间间隔,你可能会导致各种事情发生;在这个特定的配置中,我试图让生产者在消费者检查项目之前生成多个项目。

using System;
using System.Collections.Generic;
using System.Threading;

namespace ConsoleApplication1
{
    class Program
    {
        private static object LockObject = new Object();

        private static AutoResetEvent _flag;
        private static Queue<int> _list;

        static void Main(string[] args)
        {
            _list = new Queue<int>();
            _flag = new AutoResetEvent(false);

            ThreadPool.QueueUserWorkItem(ProducerThread);

            int itemCount = 0;

            while (itemCount < 10)
            {
                if (_flag.WaitOne(0))
                {
                    // there was an item
                    lock (LockObject)
                    {
                        Console.WriteLine("Items in queue:");
                        while (_list.Count > 0)
                        {
                            Console.WriteLine("Found item {0}.", _list.Dequeue());
                            itemCount++;
                        }
                    }
                }
                else
                {
                    Console.WriteLine("No items in queue.");
                    Thread.Sleep(125);
                }
            }
        }

        private static void ProducerThread(object state)
        {
            Random rng = new Random();

            Thread.Sleep(250);

            for (int i = 0; i < 10; i++)
            {
                lock (LockObject)
                {
                    _list.Enqueue(rng.Next(0, 100));
                    _flag.Set();
                    Thread.Sleep(rng.Next(0, 250));
                }
            }
        }
    }
}

如果您根本不想阻止生产者,那就有点棘手了。在这种情况下,我建议让生产者成为自己的类,同时具有私有和公共缓冲区以及公共AutoResetEvent。生产者默认将项目存储在私有缓冲区中,然后尝试将它们写入公共缓冲区。当消费者使用公共缓冲区时,它会重置生产者对象上的标志。在生产者尝试将项目从私有缓冲区移动到公共缓冲区之前,它会检查此标志并仅在消费者不处理项目时复制项目。

于 2008-09-23T20:37:46.420 回答
1

如果您使用后台工作者启动第二个线程并使用 ProgressChanged 事件通知另一个线程数据已准备好。其他事件也可用。 这篇 MSDN 文章应该可以帮助您入门

于 2008-09-23T18:45:03.320 回答
1

有很多方法可以做到这一点,具体取决于您想要做什么。生产者/消费者队列可能是您想要的。要深入了解线程,请参阅优秀书籍C# 3.0 in a Nutshell 中关于线程的章节(可在线获得)。

于 2008-09-23T18:48:18.947 回答
1

您可以使用 AutoResetEvent(或 ManualResetEvent)。如果你使用 AutoResetEvent.WaitOne(0, false),它不会阻塞。例如:

AutoResetEvent ev = new AutoResetEvent(false);
...
if(ev.WaitOne(0, false)) {
  // event happened
}
else {
 // do other stuff
}
于 2008-09-23T19:24:47.877 回答
1

在这种情况下,BackgroundWorker 类是答案。它是唯一能够将消息异步发送到创建 BackgroundWorker 对象的线程的线程构造。通过调用方法在内部BackgroundWorker使用类。AsyncOperationasyncOperation.Post()

this.asyncOperation = AsyncOperationManager.CreateOperation(null);
this.asyncOperation.Post(delegateMethod, arg);

.NET 框架中的一些其他类也使用 AsyncOperation:

  • 后台工作者
  • SoundPlayer.LoadAsync()
  • SmtpClient.SendAsync()
  • Ping.SendAsync()
  • WebClient.DownloadDataAsync()
  • WebClient.DownloadFile()
  • WebClient.DownloadFileAsync()
  • 网络客户端...
  • 图片框.LoadAsync()
于 2008-09-23T21:42:15.423 回答
0

如果您的“主”线程是 Windows 消息泵 (GUI) 线程,那么您可以使用 Forms.Timer 进行轮询 - 根据您需要让 GUI 线程“注意到”来自工作线程的数据的速度调整计时器间隔.

List<>如果要使用,请记住同步对共享的访问foreach,以避免CollectionModified异常。

我将这种技术用于实时交易应用程序中的所有市场数据驱动的 GUI 更新,并且效果很好。

于 2008-09-23T20:14:44.807 回答