C#学习笔记之线程 - 高级主题:等待和触发信号

等待信号和触发信号 - Signaling with Wait and Puls



前面讨论事件等待句柄--一个简单的信号机制,一个线程一直阻塞直到接收到另外一个线程的通知。

一个更强大的信号机制被Monitor类所经由静态函数Wait和Pluse(及PulseAll)提供。你自己编写通知逻辑,使用自定义标记和字段(加上锁),然后引入Wait和Pluse命令来阻止自旋。就lock语句和这些函数,你就可以完成AutoResetEvent,ManualResetEven和Semaplore的功能,同样你也可以完成WaitHandle静态函数WaitAll和WaitAny的功能。因此,Wait和Pluse能够在等待句柄使用的地方可以使用。

Wait和Pluse信号比起等待事件句柄也有不足的地方:

  • Wait/Pluse不能跨越应用程序域或应用程序。
  • 你比较保护与信号逻辑相关的所有变量
  • Wait/Pluse编程可能会依赖Microsoft文档的开发者混淆。

在文档中没有很明显地告诉你如何使用Wait和Pluse,即使你读完了它们是如何工作。Wait和Pluse对于业余的人来说特别讨厌。幸运的是,这里有一个很简单的模式来驯服Wait和Pluse。

Pluse大概耗费100ns,是调用Set函数的三分之一。在不竞争信号上等等的时间完全由你决定--因为你自己实现了逻辑。

How to Use Wait and Pluse

  1. 定义一个熟悉用于同步对象,如:readonly object _locker = new object();
  2. 定义一些字段用于自定义阻塞条件,如: bool _go; or int _semaphoreCount;
  3. 在你想阻塞时使用以下代码:lock(_locker) while(<blocking-condition>) Monitor.Wait(_locker);
  4. 使用下面的代码来改变阻塞条件:lock(_locker){ < alter the field(s) or data that might impact the blocking condition(s) >; Monitor.Pluse(_locker);/*or: Monitor.PluseAll(_locker);*/}

这种方式允许你让任何线程为任何条件等待任何时间。下面是一个简单的例子,工作线程等待直到_go字段为true:

class SimpleWaitPluse
{
    static object _locker = new object();
    static bool _go;

    static void Main()
    {
        new Thread(Work).Start();

        Console.ReadLine();

        lock(_locker)
        {
            _go=true;
            Monitor.Pluse(_locker);
        }
    }
    static void Work()
    {
        lock(_locker)
        {
            while(!go)Monitor.Wait(_locker);
        }
        Console.WriteLine("Woken!");
  }
}

为了线程安全,必须确保所有共享字段都加锁。因此,围绕着读/更新_go标记添加一行lock语句。这很关键(除非你想使用非阻塞同步原理)。

Work函数一直阻塞,直到_go为true。Monitor.Wait做了以下的事情,按顺序:

  1. 在_locker对象上释放锁。
  2. 阻塞直到_lcoker被触发。
  3. 在_locker对象上重新获取锁。如果所被竞争,那么它一直阻塞直到所可以使用。

Monitor.Wait要求在lock语句内使用,否则将仍处一个异常。对于Monitor.Pluse也一样。

在Main中,通过设置_go为true且调用Pluse来通知工作线程。只要一释放锁,工作线程立马执行。

Pluse和PluseAll释放阻塞在Wait上的线程。Pluse最多释放一个线程;Pluse释放所有。这个例子中只有一个线程阻塞。如果多个线程使用我们的建议调用PluseAll更加安全。

为了让Wait和Pluse或PluseAll通讯,同步对象必须是相同的(这个例子是_locker)。

在我们的模式中,触发(Pluse)表示有些东西已经改变,等待线程必须重新检查阻塞条件。Work工作线程中通过while循环来检查。然后,等待者决定是否继续。

移除while循环就得到一个皮包骨头的例子:

class SimpleWaitPluse
{
    static object _locker = new object();
    static bool _go;

    static void Main()
    {
        new Thread(Work).Start();

        Console.ReadLine();

        lock(_locker)
        {
            _go=true;
            Monitor.Pluse(_locker);
        }
    }
    static void Work()
    {
        lock(_locker)
        {
            Monitor.Wait(_locker);
        }
        Console.WriteLine("Woken!");
  }
}

它的输出是不确定的。如果Wait先执行,那么工作正常。如果Pluse线执行,那么这个通知将丢失,工作线程永远卡住。这一点与AutoResetEvent是不同的,Set语句将有一块内存并锁住影响,所以即使在WaitOne之前调用它也是有效的。

Pluse不会锁住自己,所以必须用go标记。这就是Wait和Pluse的才艺:我们使用一个bool标记,可以使他像AutoResetEvent一样工作;使用一个整数字段,可以编写一个CountdownEvent Seamphore。使用更多的数据结构,可以编写一个生产/消耗者队列。

生产/消耗者队列

前面已经描述了生产/消耗者队列的概念,及如何使用AutoResetEvent来编写。现在使用Wait和Pluse来编写更强大的生产/消耗者队列。这次允许任意数量的工作项,每一个拥有自己的线程。使用数组来跟踪线程,这使得可以在关闭对列时使用Join选项。

每个工作线程执行一个Consume函数。创建和启动这些线程在一个循环中。将使用更灵活的方法而不是字符串来描述一个任务。使用System.Action委托,它可以匹配任何参数方法,不像ThreadStart委托。仍然调用带有参数的方法来表示任务,通过封装在一个匿名函数或lambda表达式中。使用Queue<T>集合来表示任务的队列。

下面是完整的代码:

using System;
using System.Threading;
using System.Collections.Generic;
public class PCQueue
{
  readonly object _locker = new object();
  Thread[] _workers;
  Queue<Action> _itemQ = new Queue<Action>();
  public PCQueue (int workerCount)
  {
    _workers = new Thread [workerCount];
    // Create and start a separate thread for each worker
    for (int i = 0; i < workerCount; i++)
      (_workers [i] = new Thread (Consume)).Start();
  }
  public void Shutdown (bool waitForWorkers)
  {
    // Enqueue one null item per worker to make each exit.
    foreach (Thread worker in _workers)
      EnqueueItem (null);
    // Wait for workers to finish
  if (waitForWorkers)
      foreach (Thread worker in _workers)
        worker.Join();
  }
  public void EnqueueItem (Action item)
  {
    lock (_locker)
    {
      _itemQ.Enqueue (item);           // We must pulse because we‘re
Monitor.Pulse (_locker);         // changing a blocking condition.
    }
  }
  void Consume()
  {
    while (true)                        // Keep consuming until
    {                                   // told otherwise.
      Action item;
      lock (_locker)
      {
        while (_itemQ.Count == 0) Monitor.Wait (_locker);
        item = _itemQ.Dequeue();
      }
      if (item == null) return;         // This signals our exit.
      item();                           // Execute item.
    }
  }
} 
时间: 2024-08-18 19:19:33

C#学习笔记之线程 - 高级主题:等待和触发信号的相关文章

Linux System Programming 学习笔记(七) 线程

1. Threading is the creation and management of multiple units of execution within a single process 二进制文件是驻留在存储介质上,已被编译成操作系统可以使用,准备执行但没有正运行的休眠程序 进程是操作系统对 正在执行中的二进制文件的抽象:已加载的二进制.虚拟内存.内核资源 线程是进程内的执行单元 processes are running binaries, threads are the smal

操作系统学习笔记----进程/线程模型----Coursera课程笔记

操作系统学习笔记----进程/线程模型----Coursera课程笔记 进程/线程模型 0. 概述 0.1 进程模型 多道程序设计 进程的概念.进程控制块 进程状态及转换.进程队列 进程控制----进程创建.撤销.阻塞.唤醒.... 0.2 线程模型 为什么引入线程 线程的组成 线程机制的实现 用户级线程.核心级线程.混合方式 1. 进程的基本概念 1.1 多道程序设计 允许多个程序同时进入内存运行,目的是为了提高CPU系统效率 1.2 并发环境与并发程序 并发环境: 一段时间间隔内,单处理器上

IOS开发学习笔记-(3) 进度条、等待动画开始停止

一.创建对应空间视图  ,如下图: 二.编写对应的 .h 代码,如下 : #import <UIKit/UIKit.h> @interface ViewController : UIViewController @property (weak, nonatomic) IBOutlet UIActivityIndicatorView *activWaitNetWork; @property (weak, nonatomic) IBOutlet UIProgressView *pgrsDownLo

java学习笔记14--多线程编程基础1

本文地址:http://www.cnblogs.com/archimedes/p/java-study-note14.html,转载请注明源地址. 多线程编程基础 多进程 一个独立程序的每一次运行称为一个进程,例如:用字处理软件编辑文稿时,同时打开mp3播放程序听音乐,这两个独立的程序在同时运行,称为两个进程 进程要占用相当一部分处理器时间和内存资源 进程具有独立的内存空间 通信很不方便,编程模型比较复杂 多线程 一个程序中多段代码同时并发执行,称为多线程,线程比进程开销小,协作和数据交换容易

JavaSE中线程与并行API框架学习笔记1——线程是什么?

前言:虽然工作了三年,但是几乎没有使用到多线程之类的内容.这其实是工作与学习的矛盾.我们在公司上班,很多时候都只是在处理业务代码,很少接触底层技术. 可是你不可能一辈子都写业务代码,而且跳槽之后新单位很可能有更高的技术要求.除了干巴巴地翻书,我们可以通过两个方式来解决这个问题:一是做业余项目,例如在github上传自己的demo,可以实际使用:二是把自己的学习心得写成博客,跟同行们互相交流. 3.1 线程的初窥门径 我们在之前的文章里提到的程序其实都是单线程程序,也就说启动的程序从main()程

Dubbo -- 系统学习 笔记 -- 示例 -- 线程模型

Dubbo -- 系统学习 笔记 -- 目录 示例 想完整的运行起来,请参见:快速启动,这里只列出各种场景的配置方式 线程模型 事件处理线程说明 如果事件处理的逻辑能迅速完成,并且不会发起新的IO请求,比如只是在内存中记个标识,则直接在IO线程上处理更快,因为减少了线程池调度. 但如果事件处理逻辑较慢,或者需要发起新的IO请求,比如需要查询数据库,则必须派发到线程池,否则IO线程阻塞,将导致不能接收其它请求. 如果用IO线程处理事件,又在事件处理过程中发起新的IO请求,比如在连接事件中发起登录请

java学习笔记15--多线程编程基础2

本文地址:http://www.cnblogs.com/archimedes/p/java-study-note15.html,转载请注明源地址. 线程的生命周期 1.线程的生命周期 线程从产生到消亡的过程 一个线程在任何时刻都处于某种线程状态(thread state) 线程生命周期状态图 诞生状态 线程刚刚被创建 就绪状态 线程的 start 方法已被执行 线程已准备好运行 运行状态 处理机分配给了线程,线程正在运行 阻塞状态(Blocked) 在线程发出输入/输出请求且必须等待其返回 遇到

Java基础_学习笔记_16_线程

1.进程与线程 进程,在多任务操作系统中,每个独立执行的程序称为进程,也就是“正在进行的程序”.进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配的最小单元. 线程,是进程中的一部分,是一个程序内部的一条执行线索.在网络或多用户环境下,一个服务器需要接受大量且不确定用户数量的并发请求,为每一个请求创建一个进程显然是行不通的,因此引入了线程.线程是最小的调度单元.通常在一程序中实现多段代码同时交替运行时,需要产生多个线程,并制定每个线程上所要运行的程序代码块,这就

python学习笔记-Day11 (线程、进程、queue队列、生产消费模型、携程)

线程使用 ###方式一 import threading def f1(arg): print(arg) t = threading.Thread(target=f1, args=(123,)) t.start() # start会调用run方法执行 # t是threading.Thread类的一个对象 # t.start()就会以线程的方式执行函数,可以使用pycharm ctrl选择start方法 # 找到Thread类的start方法,在start方法的注释中就已经写明,会去调用run()