C#学习笔记之线程 - 通知Signal
通知事件等待句柄 Signal With EventWaitHandle
事件等待句柄常用于通知。当一个线程等待直到接收到另外一个线程发出的信号。事件等待句柄是最简单的信号结构,它与C#事件无关。有三种方式:AutoResetEvent,ManualResetEven及CountdownEvent。前2者是基于通用的EventWaitHandle类,它们派生了所有功能。
AutoResetEvent
AutoResetEvent非常像一个验票闸门:插入一张票让一个人通过。名字中的自动意思是当人通过后将自动关闭/复位。一个线程通过调用WaitOne来阻塞等待(等待闸门打开),调用Set函数来插入一张票。如果大量线程调用WaitOne,那么在闸门后建立了一个队列。(根据锁,队列的公平性在不同的操作系统有细微的差别)。一张来自某个线程的票;也就是,任何(非阻塞)访问AutoResetEvent对象的线程能调用Set来释放一个阻塞的线程。
可以用2种方法创建该对象。第一种是:
var auto = new AutoResetEvent(false);
传递true到构造函数,意味着立即调用Set函数。第2种方法是:
var auto = new EventWaitHandle(false, EventResetMode.AutoReset);
在下面的例子中,一个线程等待,得到另外一个线程通知后开始工作:
class BasicWaitHandle
{
static EventWaitHandle _waitHandle = new AutoResetEvent(false);
static void Main()
{
new Thread(Waiter).Start();
Thread.Sleep();
_waitHandle.Set();
} static void Waiter()
{
Console.WriteLine("Waiting...");
_waitHandle.WaitOne(); /// Wait for notification
Console.WriteLine("Notified");
}
}
// Output:
Waiting... (pause)
Notified.
如果Set调用时没有线程在等待,那么句柄尽其所能保持Open直到有线程调用WaitOne。这种行为有助于解决线程竞争条件,一个线程正在走向旋转门,而一个线程正在插入票(插入票快了百万分之1秒,你不得不无限制地等待)。然而,在没有人等待的闸门上重复调用Set当他们到达时不允许全部通过:仅下一个人能够通过并且而外的票将被浪费。
在AutoResetEvent对象上调用Reset关闭旋转门(它应该是开着的)不会等待或阻塞。
WaitOne接收一个可选的超时,如果等待时间到了还没有接收到信号,那么将返回false。
用超时0来测试一个等待句柄是否"open"而不会阻塞调用者。记住,如果它是open这样做将reset对象AutoResetEvent。
释放等待句柄
一旦你完成了等待,你应该调用它的Close来释放操作系统资源。或者,你可以丢弃对这个句柄的所有引用让垃圾回收器在以后某个时间来为你回收资源(等待句柄实现了Dispose,因此finalizer会调用Close)。很少情况是依赖这样的,因为等待句柄会增加OS的负担(异步调用实际上就是依赖这种机制来释放IAsyncResult等待句柄的)。
当应用程序被Unload时,等待句柄被自动释放。
相互通知
比如说,我们想要主线程中通知工作线程3次。如果主线程只是简单快速调用Set几次,那么第2或第三次信号可能丢失,因为工作线程可能正在处理每一个信号。
解决办法是在主线程在通知工作线程之前让工作线程处于Ready。这可以用另外一个AutoResetEvent来实现:
class TwoWaySignaling
{
static EventWaitHandler _ready = new AutoResetEvent(false);
static EventWaitHandler _go = new AutoResetEvent(false);
static readonly object _locker = new object();
static string _message; static void Main()
{
new Thread(Work).Start();
_ready.WaitOne(); /// First wait until worker is ready
lock(_locker) _message="ooo";
_go.Set(); /// Tell worker to go _ready.WaitOne();
lock(_locker) _message="ahhh";
_go.Set(); _ready.WaitOne();
lock(_locker) _message=null;
_go.Set();
}
static void Work()
{
while(true)
{
_ready.Set(); ///Indicate that we're ready
_go.WaitOne(); /// wait to be kicked off...
lock(_locker)
{
if(_message==null)return ;
Console.WriteLine(_message);
}
}
}
}
// Output: ooo ahhh
我们使用一个null来指示工作线程应该结束。对于无限运行的线程,有一个退出策略是非常重要的。
生产/消耗队列
生产/消耗队列是在线程中常见的需求。下面就是它的工作原理:
- 设置一个队列来描述工作项或者执行的数据。
- 当一个任务被执行时,它被押入栈,调用者继续其它事情。
- 一个或多个工作线程在后台努力从队列里挑选并执行任务。
这种模型好处是你可以精确地控制一次让多少工作线程执行。这也能允许你限制不仅是CPU的消耗,而且其它资源也一样。如果任务执行的是密集的磁盘I/O操作,比如,你可以使用一个工作线程去避免饿死操作系统或其它应用程序。另外一种的应用程序可能有20个工作线程。你应该根据队列的生命周期动态增加和移除工作线程。CLR的线程池本身就是一种这样的生产/消耗队列。
一个生产/消耗者队列通常拥有同一个任务执行的数据。如,数据项可能是文件名,而任务是加密这些文件。
下面的例子,我们使用单个AutoResetEvent来通知一个工作线程,等待直到它执行完任务(也就是队列是空的)。我们结束这个工作线程通过使用null的任务:
using System;
using System.Threading;
using System.Collections.Generic; class ProducerConsumerQueue : IDisposable
{
EventWaitHandle _wh = new AutoResetEvent(false);
Thread _worker;
readonly object _locker = new object();
Queue<string> _tasks = new Queue<string>(); public ProducerConsumerQueue()
{
_worker = new Thread(Work);
_worker.Start();
}
public void EnqueueTask(string task)
{
lock(_locker) _tasks.Enqueue(task);
_wh.Set();
}
public void Dispose()
{
EnqueueTask(null); /// Signal the consumer to exit.
_worker.Join(); ///Wait for the consumer's thread to finish.
_wh.Close(); /// Release any OS resources.
}
void Work()
{
while(true)
{
string task = null;
lock(_locker)
if(_tasks.Count>)
{
task=_tasks.Dequeue();
if(task==null)return;
}
if(task!=null)
{
Console.WriteLine("Performing task: "+task);
Thread.Sleep();//simulate work...
}
else _wh.WaitOne(); // No more tasks - wait for a signal }
}
}
为了确保线程安全,我们使用了一个lock来保护队Queue集合的访问。我们也在Dispose函数中显式关闭了等待句柄,因为我们可能在整个应用程序中创建和销毁这个类很多次。
这是测试方法:
static void Main()
{
using(ProducerConsumerQueue q = new ProducerConsumerQueue))
{
q.EnqueueTask("Hello");
for(int i=;i<;i++)q.EnqueueTask("Say "+i);
q.EnqueueTask("Good Bye!");
}
/// Exiting the using statement call q's Dispose method, which
// enqueues a null task and waits until the consumer finishes.
} Performing task:Hello
Performing task:Say
Performing task:Say
。。。
Performing task:Say
Good Bye!
.NET 4.0提供了一个新的类BlockingCollection<T>实现了生产/消耗队列。本手册编写的生产/消耗队列仍然是有价值的--不仅演示了AutoResetEvent和线程安全,而且作为复杂结构的基础。举个例子,如果我们想要一个bounded blocking queue(限制了队列里的任务数量)而且想支持取消,我们的代码提供了一个很好的开端。我们将在等待和触发例子中进一步采用生产/消耗队列。
ManualResetEvent
ManualResetEvent类似普通的门。调用Set打开门,允许任意数量调用WaitOne的线程通过。调用Reset关闭门。在一个关闭的门上调用WaitOne的线程将被阻塞。当门下一次打开时,他们将立刻被释放。除了这些不同以外,ManualResetEvent类似于AutoResetEvent。
与AutoResetEvent一样,你可以通过2种方式来构造ManualResetEvent:
var manual = new ManualResetEvent(false);
var manual = new EventWaitHandle(false,EventResetMode.ManualReset);
从.NET 4.0开始,有另外一个版本的ManualResetEvent是ManualResetEventSlim。后者优化了等待时间--选择了一定数量的迭代。它也有更高效的实现并且允许一个Wait能够通过一个CancellationTok被取消。然而,它不能用于进程间通知。ManualResetEvent没有子类化WaitHandle;然而它提供了WaitHandle属性来返回一个基于WaitHandle的对象。
通知构造和性能
等待或通知一个AutoResetEvent或ManualResetEvent大概需要1微妙(假设不阻塞)。
ManualResetEventSlim和CountdownEvent最多能够提升50倍在一个短等待场景,因为它们不依赖操作系统而是选择了自旋。
在大多数场景中,通过类本身的负载不会产生瓶颈,所以很少需要考虑。有一个例外,就是高并发代码中。
ManualResetEvent是非常有用的,当它运行一个线程去开启(unblock)其它很多线程时。相反场景应该使用CountdownEvent。
CountdownEvent
CountdownEvent让你等待多个线程。这个类是.NET 4.0新加的并且有一个非常高效的实现(如果使用早期版本,那么这个不适用)。
为了使用它,你必须使用线程的数量来实例化或者用你想要的等待的数量(count):var countdown = new CountdownEvent()3; // Initialize with "count" of 3.
调用Signal减少数量(count);调用Wait阻塞线程直到数量为0。举个例子:
static CoundownEvent _countdown = new CountdownEvent(); static void Main()
{
new Thread(SaySomething).Start("I am thread 1");
new Thread(SaySomething).Start("I am thread 2");
new Thread(SaySomething).Start("I am thread 3");
_countdown.Wait(); /// Blocks until Signal has been called 3 times. Console.WriteLine("All threads have finished speaking!");
} static void SaySomething(object thing)
{
Thread.Sleep();
Console.WriteLine(thing);
_countdown.Signal();
}
CountdownEvent有时能使用并发结构更简单地来解决(PLINQ和Parallel类)。
你可以调用AddCount来重新增加CountdownEvent的数量。然而,如果它已经为0,则扔出一个异常:你不能通过调用AddCount“不激发”一个CountdownEvent。为了避免异常,可以使用TryAddCount,如果为0,该函数返回false。
为了不激发一个countdown事件,可以调用Reset:这将不激发该结构并复位count到它原始的值。
类似于ManualResetEventSlim,CountdownEvent也提供WaitHandle属性。
创建跨进程的事件等待句柄
EventWaitHandle的构造函数允许创建一个“命名”的EventWaitHandle,它可以跨进程操作。它的名字可以是任何字符串,只要不与别人的名字冲突即可。如果名字已经存在,那么你可以得到EventWaitHandle的引用;否则,操作系统将为你创建一个新的。EventWaitHandle wh = new EventWaitHandle(false,EventResetMode.AutoReset,"MyCompany.MyApp.YourEventName");
如果2个应用程序运行这份代码,那么它们相互之间能够通知:等待句柄将跨越2个进程中的所有线程。
等待句柄和线程池
如果你有大量的线程花大量的时间在等待句柄上那么你可以调用ThreadPool.RegisterWaitForSingleObject函数来使用线程池减少资源的负担。这个方法能够接受一个委托,当等待句柄被通知时,这个委托将被执行。当它在等待时,它并不占用线程。
static ManualReset _starter = new ManualResetEvent(false);
public static void Main()
{
RegisteredWaitHandle reg = ThreadPool.RegisterWaitForSingleObject
(_starter, Go, "Some Data", -, true);
Thread.Sleep ();
Console.WriteLine ("Signaling worker...");
_starter.Set();
Console.ReadLine();
reg.Unregister (_starter); // Clean up when we’re done.
}
public static void Go (object data, bool timedOut)
{
Console.WriteLine ("Started - " + data);
// Perform task...
}
// Output:
( second delay)
Signaling worker...
Started - Some Data
当等待句柄被通知或超时时,委托将在线程池中的线程上被执行。
除了等待句柄和委托,RegisterWaitForSingleObject还接收一个“黑盒子”对象,它可以传递你的委托函数(而不是类似于ParameterizedThreadStart),和超时的毫秒数(-1意味着永不超时)及一个标记值来指示是一次性还是循环等待。
RegisterWaitForSingleObject对于一个需要并发处理很多请求的服务器应用程序尤其有用。假设你要阻塞在一个ManualResetEvent上,你只需调用WaitOne。
void AppServerMethod()
{
_wh.WaitOne();
//... continue execution
}
如果你有100个客户端调用这个方法,那么100个服务器线程将被阻塞。如果用RegisterWaitForSingleObject来代替WaitOne允许你的函数立即返回,而不浪费线程:
void AppServerMethod()
{
RegisteredWaitHandle reg = ThreadPool.RegisterWaitForSingleObject(_wh,Resume,null,-1,true);
}
static void Resume(object data, bool timeOut)
{ //... continue execution
}
传递给Resume的数据可以是任何临时数据。
WaitAny, WaitAll和SingalAndWait
除了Set,WaitOne和Reset方法外,WaitHandle还有一些静态方法来帮助复杂的同步。WaitAny,WaitAll和SingalAndWait在多个句柄上执行原子的通知和等待。等待句柄可以是不同类型(包括Mutex和Semphore,因为它们派生自WaitHandle类)。ManualResetEventSlim和CountdownEvent通过在WaitHandle属性上调用这些方法来使用这些方法。
WaitAll和SignalAndWait有一个奇怪的连接与传统的COM架构:这些方法要求调用者必须在多线程套间中,至少适合互操作性。WPF或WF的主线程,不能用这个模型与clipboard互动。稍后,简单讨论。
WaitHandle。WaitAny等待句柄数组中的任何一个;WaitHandle。WaitAll等待给定的所有句柄。这意味着如果你有2个AutoResetEvent:
- WaitAny永远不会结束锁定在2个事件上
- WaitAll永远不会结束锁定仅一个事件。
SignalAndWait在一个WaitHandle调用Set,然后在另外一个WaitHandle调用WaitOne。原子性保证了先通知第一个句柄,然后跳到了队列头部等待第2个句柄。你可以把它看作是与另外一个线程交换信号。你能够在一对EventWaitHandl上调用这个方法来设定2个线程的汇合点或在同一个地点碰头。不管是AutoResetEvent还是ManualResetEvent都可以使用这个技巧。第一个线程执行:WaitHandle.SignalAndWait(wh1,wh2);而第2个线程相反:WaitHandle.SignalAndWait(wh2,wh1);
WaitAll和SignalAndWait的替代方法
WaitAll和SignalAndWait不能运行在单线程套间中。幸运的是,这里有一些方法可以这样做。SignalAndWait你很少需要它的原子性:上面的例子中,你可以在第1个句柄中调用Set然后再第2个句柄上调用WaitOne来实现。在Barrier类中,我们将揭示实现该功能的另外一个方法。
WaitAll有时可以使用Parallel类的Invoke方法来替换。其它的情况可以使用底层的方法来解决这些问题:Wait和Pluse。
C#学习笔记之线程 - 通知Signal的更多相关文章
- 操作系统学习笔记----进程/线程模型----Coursera课程笔记
操作系统学习笔记----进程/线程模型----Coursera课程笔记 进程/线程模型 0. 概述 0.1 进程模型 多道程序设计 进程的概念.进程控制块 进程状态及转换.进程队列 进程控制----进 ...
- java学习笔记15--多线程编程基础2
本文地址:http://www.cnblogs.com/archimedes/p/java-study-note15.html,转载请注明源地址. 线程的生命周期 1.线程的生命周期 线程从产生到消亡 ...
- JUC源码学习笔记5——线程池,FutureTask,Executor框架源码解析
JUC源码学习笔记5--线程池,FutureTask,Executor框架源码解析 源码基于JDK8 参考了美团技术博客 https://tech.meituan.com/2020/04/02/jav ...
- java学习笔记14--多线程编程基础1
本文地址:http://www.cnblogs.com/archimedes/p/java-study-note14.html,转载请注明源地址. 多线程编程基础 多进程 一个独立程序的每一次运行称为 ...
- JavaSE中线程与并行API框架学习笔记1——线程是什么?
前言:虽然工作了三年,但是几乎没有使用到多线程之类的内容.这其实是工作与学习的矛盾.我们在公司上班,很多时候都只是在处理业务代码,很少接触底层技术. 可是你不可能一辈子都写业务代码,而且跳槽之后新单位 ...
- Dubbo -- 系统学习 笔记 -- 示例 -- 线程模型
Dubbo -- 系统学习 笔记 -- 目录 示例 想完整的运行起来,请参见:快速启动,这里只列出各种场景的配置方式 线程模型 事件处理线程说明 如果事件处理的逻辑能迅速完成,并且不会发起新的IO请求 ...
- iOS学习笔记22-推送通知
一.推送通知 推送通知就是向用户推送一条信息来通知用户某件事件,可以在应用退到后台后,或者关闭后,能够通过推送一条消息通知用户某件事情,比如版本更新等等. 推送通知的常用应用场景: 一些任务管理APP ...
- java学习笔记之线程(Thread)
刚开始接触java多线程的时候,我觉得,应该像其他章节的内容一样,了解了生命周期.构造方法.方法.属性.使用的条件,就可以结束了,然而随着我的深入学习了解,我发现java的多线程是java的一个特别重 ...
- 0040 Java学习笔记-多线程-线程run()方法中的异常
run()与异常 不管是Threade还是Runnable的run()方法都没有定义抛出异常,也就是说一条线程内部发生的checked异常,必须也只能在内部用try-catch处理掉,不能往外抛,因为 ...
随机推荐
- [Selenium]中使用css选择器进行元素定位
参考:http://www.cnblogs.com/webblog/archive/2009/07/07/1518274.html 常见语法 * 通用元素选择器,匹配任何元素 E 标签选择器,匹配所有 ...
- C语言中用宏来作注释
看了PostgreSQL的代码后,我觉得有不理解的地方,比如: 例如这样的: /* Options that may appear after CATALOG (on the same line) * ...
- 美国VPS推荐1GB 50GB可以win
今天向大家推荐一款vps,1GB内存 50G硬盘 8M带宽 不限制流量,并且可以安装windows,年付才290元. 购买链接:http://www.jinbaoidc.com/page.aspx?c ...
- 【转】GCC使用简介
Linux系统下的gcc(GNU C Compiler)是GNU推出的功能强大.性能优越的多平台编译器,是GNU的代表作品之一.gcc是可以在多种硬体平台上编译出可执行程序的超级编译器,其执行效率与一 ...
- Codeforces Round #Pi (Div. 2) A. Lineland Mail 水题
A. Lineland MailTime Limit: 20 Sec Memory Limit: 256 MB 题目连接 http://codeforces.com/contest/567/probl ...
- Hibernate征途(六)之数量和关系映射
本来如果和关系模型一样,只需要一对一.一对多.多对多映射就够了,但是前面<Hibernate征途(四)之映射 序>中说到,对象模型中关联是有方向的,所以对一对多而言,就会产生一对多还是多对 ...
- libIconv.lib编码库的生成和使用
iconv是将一种编码格式转换为另一种编码格式的开源库,例如可以把Windows环境下通用的ASCii(中文是GB2312)编码转换为国际通用的Unicode编码 iconv最新版本只支持MingW和 ...
- iOS 2D绘图详解(Quartz 2D)之Transform(CTM,Translate,Rotate,Scale)
前言:Quartz默认采用设备无关的user space来进行绘图,当context(画板)建立之后,默认的坐标系原点以及方向也就确认了,可以通过CTM(current transformation ...
- Linux下的lds链接脚本简介
转载:http://hubingforever.blog.163.com/blog/static/171040579201192472552886/ 一. 概论 每一个链接过程都由链接脚本(lin ...
- android 几个小技巧
1.如果打开模拟器,不同程序打开了不同的模拟器.可能是某个某个模拟器的target版本过低,修改一下4.2,应该都可以用了 2.找不到R.id.的错误,不妨删除menu文件夹下的xml文件 3.act ...