.NET:如何使后台线程信号主线程数据可用?
使ThreadA发出某个事件的信号ThreadB而又不阻止ThreadB坐等事件发生的正确技术是什么?
我有一个后台线程,它将填充共享的List <T>。我正在尝试找到一种方法来异步发信号通知"主"线程有可用的数据。
我考虑过使用EventWaitHandle对象设置事件,但是我无法将主线程放在Event.WaitOne()上。
我考虑过一个委托回调,但是
a)我不希望主线程在委托中工作:该线程需要重新开始工作,添加更多东西,我不希望它在委托执行时等待,并且
b)代表需要编组到主线程上,但是我没有运行UI,我没有控件来调用代表。
我考虑过有一个委托回调,该回调仅启动零间隔System.Windows.Forms.Timer(对计时器的线程访问已同步)。这样,线程只需要在调用时被卡住
Timer.Enabled = true;
但这似乎是一个hack。
在过去,我的对象会创建一个隐藏窗口,并让线程将消息发布到该隐藏窗口的HWND。我考虑过创建一个隐藏的控件,但我认为我们无法在没有创建句柄的控件上进行调用。另外,我没有UI:我的对象可能是在Web服务器,服务或者控制台上创建的,我既不想显示图形控件,也不想编译对System.Windows.Forms的依赖关系。
我考虑过让我的对象公开一个ISynchronizeInvoke接口,但是然后我需要实现.Invoke(),这就是我的问题。
使线程A发出某个事件的信号B而不使线程B处于阻塞状态以等待事件发生的正确技术是什么?
解决方案
如果我们使用后台工作程序启动第二个线程,并使用ProgressChanged事件通知另一个线程数据已准备好。其他事件也可用。此MSDN文章应该使我们入门。
有多种方法可以执行此操作,具体取决于我们要执行的操作。生产者/消费者队列可能是我们想要的。要深入了解线程,请参阅出色的《 Cut in Nutshell》一书中有关线程的章节(在线提供)。
我们可以使用AutoResetEvent(或者ManualResetEvent)。如果使用AutoResetEvent.WaitOne(0,false),它将不会阻塞。例如:
AutoResetEvent ev = new AutoResetEvent(false); ... if(ev.WaitOne(0, false)) { // event happened } else { // do other stuff }
如果"主"线程是Windows消息泵(GUI)线程,则可以使用Forms.Timer进行轮询,根据需要让GUI线程"通知"工作线程中的数据的速度来调整计时器间隔。
如果要使用foreach
,请记住要同步对共享List <>
的访问,以避免CollectionModified
异常。
我将这种技术用于实时交易应用程序中所有由市场数据驱动的GUI更新,并且效果很好。
我在这里合并一些回应。
理想情况是使用线程安全标志,例如" AutoResetEvent"。当我们调用WaitOne()
时,我们不必无限期地阻塞,实际上,它具有允许我们指定超时的重载。如果在间隔期间未设置标志,则此重载将返回" false"。
对于生产者/消费者关系,"队列"是更理想的结构,但是如果要求迫使我们使用"列表",则可以模仿它。主要区别在于我们将必须确保消费者在提取项目时锁定对集合的访问权限;最安全的事情可能是使用" CopyTo"方法将所有元素复制到数组,然后释放锁。当然,请确保生产者在持有锁时不会尝试更新List
。
这是一个简单的Cconsole应用程序,演示了如何实现。如果我们按时间间隔玩耍,可能会导致各种事情发生。在这种特定配置中,我试图让生产者在消费者检查项目之前生成多个项目。
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"使生产者成为自己的类。生产者默认将项目存储在专用缓冲区中,然后尝试将它们写入公共缓冲区。使用者使用公共缓冲区时,它将重置生产者对象上的标志。在生产者尝试将项目从专用缓冲区移动到公共缓冲区之前,它会检查此标志并仅在使用者不使用它时才复制项目。
这是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."); }
在这种情况下,BackgroundWorker类是答案。它是唯一能够将消息异步发送到创建BackgroundWorker对象的线程的线程构造。内部BackgroundWorker通过调用asyncOperation.Post()方法来使用AsyncOperation类。
this.asyncOperation = AsyncOperationManager.CreateOperation(null); this.asyncOperation.Post(delegateMethod, arg);
.NET框架中的其他一些类也使用AsyncOperation:
- 后台工作者
- SoundPlayer.LoadAsync()
- SmtpClient.SendAsync()
- Ping.SendAsync()
- WebClient.DownloadDataAsync()
- WebClient.DownloadFile()
- WebClient.DownloadFileAsync()
- WebClient ...
- PictureBox.LoadAsync()