吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 3449|回复: 5
收起左侧

[其他转载] Visual C#中使用线程

[复制链接]
楚轩 发表于 2015-7-25 13:35
简介
  编写多线程 Microsoft® 消息队列 (MSMQ) 触发器应用程序向来是一件让人畏惧的事情。不过,.NET 框架线程和消息类的出现使这项工作变得比以前容易了。这些类允许您使用任何适用于 .NET 框架的语言来编写多线程应用程序。以前,像 Microsoft Visual Basic® 之类的工具对线程的支持十分有限。因此不得不使用 C++ 来编写多线程代码,通过 Visual Basic 构建由多个过程或 ActiveX DLL 组成的解决方案(这种解决方案一点也不理想),或者干脆完全放弃多线程。使用 .NET 框架,您可以构建各种多线程应用程序,而不用考虑选择使用哪种语言。

  本文将逐步介绍构建侦听并处理来自 Microsoft 消息队列的多线程应用程序的过程。本文将着重讨论两个名称空间 System.Threading 和 System.Messaging。示例代码是用 C# 语言编写的,但您可以轻松地将其转换为您所使用的语言。

  线程背景
  在 Win32 环境中,线程有三种基本模式:单线程、单元线程和自由线程。

  单线程
  您最初编写的某些应用程序很可能是单线程应用程序,仅包含与应用程序进程对应的线程。进程可以被定义为应用程序的实例,拥有该应用程序的内存空间。大多数 Windows 应用程序都是单线程的,即用一个线程完成所有工作。

  单元线程
  单元线程是一种稍微复杂的线程模式。标记用于单元线程的代码可以在其自己的线程中执行,并限制在自己的单元中。线程可以被定义为进程所拥有的实体。处理时将调度该进程。在单元线程模式中,所有线程都在主应用程序内存中各自的子段范围内运行。此模式允许多个代码实例同时但独立地运行。例如,在 .NET 之前,Visual Basic 仅限于创建单元线程组件和应用程序。

  自由线程
  自由线程是最复杂的线程模式。在自由线程模式中,多个线程可以同时调用相同的方法和组件。与单元线程不同,自由线程不会被限制在独立的内存空间。当应用程序必须进行大量相似而又独立的数学计算时,您可能需要使用自由线程。在这种情况下,您需要生成多个线程使用相同的代码示例来执行计算。可能 C++ 开发人员是仅有的编写过自由线程应用程序的应用程序开发人员,因为像 Visual Basic 6.0 这样的语言几乎不可能编写自由线程应用程序。

  使用线程模式
  为了使您对线程模式有一定的概念,我们可以将其想象为从一所屋子搬到另一所屋子。如果您采用单线程方法,则需要您自己完成从打包到扛箱子再到拆包的所有工作。如果您使用单元线程模式,则表示您邀请了好朋友来帮忙。每个朋友在一个单独的房间里工作,并且不能帮助在其他房间工作的人。他们各自负责自己的空间和空间内的物品搬运。如果您采用自由线程方法,您仍然邀请相同的朋友来帮忙,但是所有朋友可以随时在任何一个房间工作,共同打包物品。与此类似,您的房子就是运行所有线程的进程,每个朋友都是一个代码实例,搬运的物品为应用程序的资源和变量。

  本示例解释了不同线程模式的优点和缺点。单元线程比单线程要快,因为有多个组件实例在工作。在某些情况下,自由线程比单元线程更快更有效,这是因为所有事情同时发生,并且可以共享所有资源。但是,当多线程更改共享资源时,这可能会出现问题。假设一个人开始使用箱子打包厨房用具,此时另一个朋友进来了,要使用同一个箱子打包浴室的东西。第一个朋友在箱子上贴上了“厨房用具”,另一个朋友用“洗漱用品”标签覆盖了原标签。结果,当您拆包时,就会发生将厨房用品搬到浴室的情况。


示例应用程序
  第一步是要检查示例应用程序的设计。应用程序将生成多个线程,每个线程都侦听来自 MSMQ 队列的消息。本示例使用两个类,主 Form 类和自定义 MQListen 类。Form 类将处理用户界面并创建,管理和破坏辅助线程。MQListen 类包含所有代码,包括辅助线程运行所需的消息队列因素。

  准备应用程序
要启动应用程序,请打开 Visual Studio .NET 并创建一个名为 MultiThreadedMQListener 的新 C# Windows 应用程序。打开窗体的属性,将其命名为 QueueListenerForm。画出初始窗体后,将两个标签、两个按钮、一个状态栏和两个文本框拖放到窗体上。将第一个文本框命名为 Server,第二个文本框命名为 Queue。将第一个按钮命名为 StartListening,第二个按钮命名为 StopListening。可以保留状态栏的默认名称 statusBar1。
下一步,单击 Project(项目)菜单并单击 Add Reference(添加引用),以向 System.Messaging 名称空间添加一个引用。在 .NET 组件列表中找到并选择 System.Messaging.Dll。名称空间包含与 MSMQ 队列通信所使用的类。
下一步,单击 File(文件)菜单,然后单击 Add New Item(添加新项),以在项目中添加一个新类。选择 Class(类)模板并将其命名为 MQListen。在类的顶部添加下列 using 语句:
// C# using System.Threading; using System.Messaging;

System.Threading 名称空间允许您访问所有必要的线程功能,在本例中,您可以访问 Thread 类和 ThreadInterruptException 构造函数。该名称空间还包括许多其他高级功能,本文不作详细讨论。System.Messaging 名称空间允许您访问 MSMQ 功能,包括向队列发送消息和接收队列消息。在本例中,您将使用 MessageQueue 类来接收消息。还必须在主窗体代码中添加 using System.Threading。

  所有引用就位后,您就可以开始编写代码了。

  辅助线程
  首先需要构建封装所有线程工作的 MQListen 类。将下列代码插入 MQListen 中。

// C# public class MQListen { private string m_MachineName; private string m_QueueName; // 构造函数接收必要的队列信息。 public MQListen(string MachineName, string QueueName) { m_MachineName = MachineName; m_QueueName = QueueName; }

// 每个线程用来侦听 MQ 消息的一种唯一方法 public void Listen() { // 创建一个 MessageQueue 对象。 System.Messaging.MessageQueue MQ = new System.Messaging.MessageQueue();

// 设置 MessageQueue 对象的路径属性。 MQ.Path = m_MachineName + "\\private$\\" + m_QueueName; // 创建一个 Message 对象。 System.Messaging.Message Message = new System.Messaging.Message(); // 重复上述步骤,直到收到中断。 while (true) { try { // 休眠以在中断发出时捕捉中断。 System.Threading.Thread.Sleep(100); // 将 Message 对象设置为与接收函数的结果相等。 // 持续时间(天、小时、分钟、秒)。 Message = MQ.Receive(new TimeSpan(0, 0, 0, 1)); // 显示已接收消息的标签 System.Windows.Forms.MessageBox.Show(" Label: " + Message.Label); } catch (ThreadInterruptedException e) { // 从主线程捕捉 ThreadInterrupt 并退出。 Console.WriteLine("Exiting Thread"); Message.Dispose(); MQ.Dispose(); break; } catch (Exception GenericException) { // 捕捉接收过程中抛出的所有异常。 Console.WriteLine(GenericException.Message); } } } }


  代码讨论

  MQListen 类包含一个不同于构造函数的函数。该函数封装每个辅助线程要执行的所有工作。在主线程中,您向线程构造函数传递一个对此函数的引用,以便在启动线程时执行该函数。

  Listen 所做的第一件事情是设置一个消息队列对象。MessageQueue 构造函数通过三种实现进行重载。第一种实现使用两个参数:一个字符串参数,指定侦听队列的位置;一个布尔值参数,指示是否为访问队列的第一个应用程序赋予独占读取队列的权限。第二种实现只使用队列路径参数,第三种实现不使用参数。为了简便起见,您可以使用第三种实现,在下一行分配路径。

  如果您引用了队列,则必须创建一个消息对象。消息构造函数也有三种实现方式。如果您想将消息写入队列,则可以使用前两种实现。这两种实现采用两个对象:一个是位于消息正文中的对象;一个是定义如何将对象序列化到消息正文的 IMessageFormatter 对象。在本例中,您将从队列中读取数据,以初始化空的消息对象。

  初始化对象后,您需要输入执行所有工作的主循环。然后,当主线程调用 Interrupt 终止这些线程时,则只有在线程处于等待、睡眠或连接状态下才会被中断。如果没有处于上述三种状态,则要等到下次进入这三种状态中的一种时才会被中断。要确保辅助线程进入等待、睡眠或连接状态,请调用位于 System.Threading 名称空间的 Sleep 方法。对于使用过 Windows API 睡眠函数的 C++ 和 Visual Basic 开发人员而言,Sleep 方法并不陌生。它只使用一个参数:线程处于睡眠状态的毫秒数。如果您从未调用过 Sleep,辅助线程将永远不会进入可以接收中断请求的状态,而会无限制地继续下去,除非您手动关闭进程。

  MQ Receive 方法有两种实现。第一种实现不使用参数,将一直等待接收消息。第二种实现(本例使用这种实现)使用 TimeSpan 对象指定一个超时值。TimeSpan 构造函数包含四个参数:日、小时、分钟和秒。在本例中,Receive 方法在超时和返回前将等待一秒种。

  收到的消息将被分配给先前创建的消息对象,然后,便可以对其进行处理了。本例打开一个带有标签的消息框,并删除了此消息。如果您想在实际使用中采用此代码,则可以在此处放置任何消息处理代码。

  当辅助线程收到 Interrupt 请求后,将发出一个 ThreadInterruptedException 异常。要捕捉此异常,请在 try-catch 块中包含 Sleep 和 Receive 函数。您应当指定两个捕获:第一个用于捕获中断异常,第二个用于处理捕获到的错误异常。捕获到中断异常时,请首先将其写入线程正在退出的调试窗口。下一步,对队列对象和消息对象调用 Dispose 方法,以保证所有内存都被清空并发送到内存回收器。最后,中断 while 循环。

  函数退出 while 循环后,关联的线程也将立即结束,代码为 0。在调试窗口,您将看到一则消息,例如“The thread '' (0x660) has exited with code 0 (0x0)”(线程 '' (0x660) 已经退出,代码为 0 (0x0))。现在,线程已经退出该环境,并已自动被破坏。主线程和辅助线程都不需要执行专门的清除操作。


下一步是向窗体添加代码以生成辅助线程并针对各辅助线程启动 MQListen 类。首先,请向窗体添加下列函数:
// C# private void StartThreads() { int LoopCounter; // 线程计数 StopListeningFlag = false; // 跟踪辅助线程是否应当 // 终止的标志。
// 将一个包含 5 个线程的数组声明为辅助线程。 Thread[] ThreadArray = new Thread[5];
// 声明包含辅助线程的所有代码的类。 MQListen objMQListen = new MQListen(this.ServerName.Text,this.QueueName.Text);
for (LoopCounter = 0; LoopCounter < NUMBER_THREADS; LoopCounter++) { // 创建一个 Thread 对象。 ThreadArray[LoopCounter] = new Thread(new ThreadStart(objMQListen.Listen)); // 启动线程将调用 ThreadStart 委托。 ThreadArray[LoopCounter].Start(); }
statusBar1.Text = LoopCounter.ToString() + " listener threads started";
while (!StopListeningFlag) { // 等待用户按下停止按钮。 // 在等待过程中,让系统处理其他事件。 System.Windows.Forms.Application.DoEvents(); }
statusBar1.Text = "Stop request received, stopping threads"; // 向每个线程发送一个中断请求。 for (LoopCounter = 0;LoopCounter < NUMBER_THREADS; LoopCounter++) { ThreadArray[LoopCounter].Interrupt(); }
statusBar1.Text = "All Threads have been stopped"; }
  代码讨论
  要启动此函数,请创建一个包含 5 个项目的线程数组。此数组将保持对所有线程的引用,以备将来使用。
  MQListen 类的构造函数使用两个参数:包含消息队列的计算机名以及要侦听的队列的名称。构造函数使用文本框中的值来为这两个参数赋值。
  要创建线程,您需要进入循环以初始化每个线程对象。Thread 构造函数要求您向其传递一个委托,该委托在调用线程的 Start 方法时指向要调用的函数。您希望线程开始使用 MQListen.Listen 函数,但该线程并不是一个委托。为了满足线程构造函数的要求,您必须传递一个 ThreadStart 对象,该对象将创建一个给定函数名称的委托。此时,请向 ThreadStart 对象传递一个对 MQListen.Listen 函数的引用。由于该数组元素已被初始化,请立即调用 Start 来开始线程。
  所有线程开始后,请用相应的消息来更新窗体中的状态栏。随着线程的运行和侦听队列,主线程将等待用户请求应用程序停止侦听。为此,主线程将进入一个 while 循环,直至您单击 StopListening 按钮更改 StopListeningFlag 的值。在此等待循环中,将允许应用程序使用 Forms.Application.DoEvents 方法处理其他需要处理的工作。对于熟悉 Visual Basic 的读者来说,这一点与旧的 DoEvents 方法相同。对于熟悉 C++ 的读者来说,这等于编写一个 MSG 泵。
  当用户单击 StopListening 按钮时,该循环将退出并进入线程关闭代码。要关闭所有线程,代码必须检查线程数组,并向每个线程发送一个中断信号。在此循环内部,请对数组中的每个线程调用 Interrupt 方法。调用此方法之前,MQListen 类中的代码将继续正常执行。因此,您可以对每个辅助线程调用 Interrupt,而不必考虑线程是否正在处理其他事件。完成后,线程类将处理所有线程的清除。最后,请在退出前更新主窗体中的状态栏。
  现在,您需要在按钮后添加代码。请向 StartListening 按钮的 Click 事件添加以下代码:
  // C#
  statusBar1.Text = "Starting Threads";
  StartThreads();
  这将更新状态栏并调用 StartThreads 方法。对于 StopListening 按钮,您只需使用以下代码将 StopListeningFlag 设置为True
  // C#
  StopListeningFlag = true;
   最后一步是为 StopListeningFlag 添加窗体级的变量。请在窗体代码的顶部添加以下行:   // C#
  private bool StopListeningFlag = false;
  要测试应用程序,您可以下载 MQWrite,这是一个写入消息队列的示例应用程序。
  多线程代码问题   您已经完成了示例代码,因此您已经具备编写自己的多线程应用程序所需的工具。线程可以显著提高某些应用程序的性能和可伸缩性。在功能增强的同时,您还必须了解线程有危险的一面。使用线程可能会破坏您的应用程序,这样的情况确实存在。线程可能会阻止运行,造成无法预料的后果,甚至会导致应用程序停止运行。
  如果您有多个线程,请确保它们之间不存在互相等待以到达某一点或完成的情况。如果操作错误,可能会导致死锁状态,两个线程都无法完成,因为它们都在相互等待。
  如果多线程要求访问不能轻易共享的资源(如软盘驱动器、串行端口或红外线端口),您可能需要避免使用线程或需要使用一种更高级的线程工具(如 synclocks 或 mutexes)来管理并发性。如果两个线程试图同时访问这些资源,其中一个线程将无法获得资源,或者会导致数据损坏。
  使用线程的另一个常见问题是竞争状态。如果一个线程正在将数据写入文件,而另一个线程正在从该文件中读取数据,您将无法知道哪个线程先完成。这种情况称为竞争状态,因为两个线程都在竞相到达文件末尾。如果读取线程快于写入线程,则将返回无法预料的结果。
  使用线程时,还应当考虑所有线程是否都能够完全独立地进行工作。如果确实需要来回传递数据,在数据相对简单的情况下,只要小心操作即可。传递复杂对象时,来回移动这些对象的封送代价将十分可观。这将导致操作系统管理的额外开销并且会降低总体性能。
  另一个问题是将代码转交给其他开发人员的传递成本。虽然 .NET 确实使线程变得容易,但请注意,维护您代码的下一位开发人员必须了解要使用的线程。尽管这不是避免使用线程的理由,但是它充分说明了应该提供足够的代码注释。
  这些问题本身并不能打消您使用线程的热情,但您在设计应用程序和决定是否使用线程时应该考虑到这些问题。遗憾的是,本文无法详细论述某些避免这些问题的方法。如果您已决定使用线程但遇到了上述某些问题,请检查 synclocks 或 mutexes 看是否能解决问题或引导您使用其他解决方案。
  总结   有了上述信息,您就可以编写使用线程的应用程序。不过,在编写过程中,请记住上面提到的问题。如果使用得当,那么,与单线程相比,多线程应用程序将具有更好的性能和可伸缩性。但是,如果使用不当,使用线程会适得其反,并且会导致应用程序不稳定。

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| 楚轩 发表于 2015-7-25 13:36
C#的多线程机制探索是另一篇文章了贴在一起方便大家看

注:本文中出现的代码均在.net Framework RC3环境中运行通过   一.多线程的概念 ??Windows是一个多任务的系统,如果你使用的是windows 2000及其以上版本,你可以通过任务管理器查看当前系统运行的程序和进程。什么是进程呢?当一个程序开始运行时,它就是一个进程,进程所指包括运行中的程序和程序所使用到的内存和系统资源。而一个进程又是由多个线程所组成的,线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。浏览器就是一个很好的多线程的例子,在浏览器中你可以在下载JAVA小应用程序或图象的同时滚动页面,在访问新页面时,播放动画和声音,打印文件等。 ??多线程的好处在于可以提高CPU的利用率——任何一个程序员都不希望自己的程序很多时候没事可干,在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率。 ??然而我们也必须认识到线程本身可能影响系统性能的不利方面,以正确使用线程:
线程也是程序,所以线程需要占用内存,线程越多占用内存也越多
多线程需要协调和管理,所以需要CPU时间跟踪线程
线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题
线程太多会导致控制太复杂,最终可能造成很多Bug
??基于以上认识,我们可以一个比喻来加深理解。假设有一个公司,公司里有很多各司其职的职员,那么我们可以认为这个正常运作的公司就是一个进程,而公司里的职员就是线程。一个公司至少得有一个职员吧,同理,一个进程至少包含一个线程。在公司里,你可以一个职员干所有的事,但是效率很显然是高不起来的,一个人的公司也不可能做大;一个程序中也可以只用一个线程去做事,事实上,一些过时的语言如fortune,basic都是如此,但是象一个人的公司一样,效率很低,如果做大程序,效率更低——事实上现在几乎没有单线程的商业软件。公司的职员越多,老板就得发越多的薪水给他们,还得耗费大量精力去管理他们,协调他们之间的矛盾和利益;程序也是如此,线程越多耗费的资源也越多,需要CPU时间去跟踪线程,还得解决诸如死锁,同步等问题。总之,如果你不想你的公司被称为“皮包公司”,你就得多几个员工;如果你不想让你的程序显得稚气,就在你的程序里引入多线程吧! ??本文将对C#编程中的多线程机制进行探讨,通过一些实例解决对线程的控制,多线程间通讯等问题。为了省去创建GUI那些繁琐的步骤,更清晰地逼近线程的本质,下面所有的程序都是控制台程序,程序最后的Console.ReadLine()是为了使程序中途停下来,以便看清楚执行过程中的输出。 ??好了,废话少说,让我们来体验一下多线程的C#吧!   二.操纵一个线程 ??任何程序在执行时,至少有一个主线程,下面这段小程序可以给读者一个直观的印象:
//SystemThread.cs using System; using System.Threading; namespace ThreadTest {   class RunIt   {     [STAThread]     static void Main(string[] args)     {       Thread.CurrentThread.Name="System Thread";//给当前线程起名为"System Thread" Console.WriteLine(Thread.CurrentThread.Name+"'Status:"+Thread.CurrentThread.ThreadState);       Console.ReadLine();     }   } }
??编译执行后你看到了什么?是的,程序将产生如下输出: ??System Thread's Status:Running ??在这里,我们通过Thread类的静态属性CurrentThread获取了当前执行的线程,对其Name属性赋值“System Thread”,最后还输出了它的当前状态(ThreadState)。所谓静态属性,就是这个类所有对象所公有的属性,不管你创建了多少个这个类的实例,但是类的静态属性在内存中只有一个。很容易理解CurrentThread为什么是静态的——虽然有多个线程同时存在,但是在某一个时刻,CPU只能执行其中一个。 ??就像上面程序所演示的,我们通过Thread类来创建和控制线程。注意到程序的头部,我们使用了如下命名空间:
??using System; ??using System.Threading;
??在.net framework class library中,所有与多线程机制应用相关的类都是放在System.Threading命名空间中的。其中提供Thread类用于创建线程,ThreadPool类用于管理线程池等等,此外还提供解决了线程执行安排,死锁,线程间通讯等实际问题的机制。如果你想在你的应用程序中使用多线程,就必须包含这个类。Thread类有几个至关重要的方法,描述如下:
Start():启动线程
Sleep(int):静态方法,暂停当前线程指定的毫秒数
Abort():通常使用该方法来终止一个线程
Suspend():该方法并不终止未完成的线程,它仅仅挂起线程,以后还可恢复。
Resume():恢复被Suspend()方法挂起的线程的执行
在这里我们要注意的是其它线程都是依附于Main()函数所在的线程的,Main()函数是C#程序的入口,起始线程可以称之为主线程,如果所有的前台线程都停止了,那么主线程可以终止,而所有的后台线程都将无条件终止。而所有的线程虽然在微观上是串行执行的,但是在宏观上你完全可以认为它们在并行执行。 ??读者一定注意到了Thread.ThreadState这个属性,这个属性代表了线程运行时状态,在不同的情况下有不同的值,于是我们有时候可以通过对该值的判断来设计程序流程。ThreadState在各种情况下的可能取值如下:
Aborted:线程已停止
AbortRequested:线程的Thread.Abort()方法已被调用,但是线程还未停止
Background:线程在后台执行,与属性Thread.IsBackground有关
Running:线程正在正常运行
Stopped:线程已经被停止
StopRequested:线程正在被要求停止
Suspended:线程已经被挂起(此状态下,可以通过调用Resume()方法重新运行)
SuspendRequested:线程正在要求被挂起,但是未来得及响应
Unstarted:未调用Thread.Start()开始线程的运行
WaitSleepJoin:线程因为调用了Wait(),Sleep()或Join()等方法处于封锁状态
??上面提到了Background状态表示该线程在后台运行,那么后台运行的线程有什么特别的地方呢?其实后台线程跟前台线程只有一个区别,那就是后台线程不妨碍程序的终止。一旦一个进程所有的前台线程都终止后,CLR(通用语言运行环境)将通过调用任意一个存活中的后台进程的Abort()方法来彻底终止进程。 ??当线程之间争夺CPU时间时,CPU按照是线程的优先级给予服务的。在C#应用程序中,用户可以设定5个不同的优先级,由高到低分别是Highest,AboveNormal,Normal,BelowNormal,Lowest,在创建线程时如果不指定优先级,那么系统默认为ThreadPriority.Normal。给一个线程指定优先级,我们可以使用如下代码:
??//设定优先级为最低 ??myThread.Priority=ThreadPriority.Lowest;
??通过设定线程的优先级,我们可以安排一些相对重要的线程优先执行,例如对用户的响应等等。 ??现在我们对怎样创建和控制一个线程已经有了一个初步的了解,下面我们将深入研究线程实现中比较典型的的问题,并且探讨其解决方法。   三.线程的同步和通讯——生产者和消费者 ??假设这样一种情况,两个线程同时维护一个队列,如果一个线程对队列中添加元素,而另外一个线程从队列中取用元素,那么我们称添加元素的线程为生产者,称取用元素的线程为消费者。生产者与消费者问题看起来很简单,但是却是多线程应用中一个必须解决的问题,它涉及到线程之间的同步和通讯问题。 ??前面说过,每个线程都有自己的资源,但是代码区是共享的,即每个线程都可以执行相同的函数。但是多线程环境下,可能带来的问题就是几个线程同时执行一个函数,导致数据的混乱,产生不可预料的结果,因此我们必须避免这种情况的发生。C#提供了一个关键字lock,它可以把一段代码定义为互斥段(critical section),互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待。在C#中,关键字lock定义如下:
??lock(expression) statement_block

 楼主| 楚轩 发表于 2015-7-25 13:37
expression代表你希望跟踪的对象,通常是对象引用。一般地,如果你想保护一个类的实例,你可以使用this;如果你希望保护一个静态变量(如互斥代码段在一个静态方法内部),一般使用类名就可以了。而statement_block就是互斥段的代码,这段代码在一个时刻内只可能被一个线程执行。   下面是一个使用lock关键字的典型例子,我将在注释里向大家说明lock关键字的用法和用途:
??//lock.cs ??using System; ??using System.Threading; ??internal class Account ??{   int balance;   Random r = new Random();   internal Account(int initial)   {   ??balance = initial;   }   internal int Withdraw(int amount)   {   ??if (balance < 0)   ??{     file://如果balance小于0则抛出异常    throw new Exception("Negative Balance");   ??}   ??//下面的代码保证在当前线程修改balance的值完成之前  ??//不会有其他线程也执行这段代码来修改balance的值  ??//因此,balance的值是不可能小于0的  ??lock (this)   ??{     Console.WriteLine("Current Thread:"+Thread.CurrentThread.Name);     file://如果没有lock关键字的保护,那么可能在执行完if的条件判断之后    file://另外一个线程却执行了balance=balance-amount修改了balance的值    file://而这个修改对这个线程是不可见的,所以可能导致这时if的条件已经不成立了    file://但是,这个线程却继续执行balance=balance-amount,所以导致balance可能小于0     if (balance >= amount)     {     ??Thread.Sleep(5);     ??balance = balance - amount;     ??return amount;     }     else     {     ??return 0; // transaction rejected     }   ??}   }   internal void DoTransactions()   {   ??for (int i = 0; i < 100; i++)     Withdraw(r.Next(-50, 100));   } ??} ??internal class Test ??{   static internal Thread[] threads = new Thread[10];   public static void Main()   {   ??Account acc = new Account (0);   ??for (int i = 0; i < 10; i++)   ??{     Thread t = new Thread(new ThreadStart(acc.DoTransactions));     threads = t;   ??}   ??for (int i = 0; i < 10; i++)     threads.Name=i.ToString();   ??for (int i = 0; i < 10; i++)     threads.Start();   ??Console.ReadLine();   } ??}
??而多线程公用一个对象时,也会出现和公用代码类似的问题,这种问题就不应该使用lock关键字了,这里需要用到System.Threading中的一个类Monitor,我们可以称之为监视器,Monitor提供了使线程共享资源的方案。   Monitor类可以锁定一个对象,一个线程只有得到这把锁才可以对该对象进行操作。对象锁机制保证了在可能引起混乱的情况下一个时刻只有一个线程可以访问这个对象。Monitor必须和一个具体的对象相关联,但是由于它是一个静态的类,所以不能使用它来定义对象,而且它的所有方法都是静态的,不能使用对象来引用。下面代码说明了使用Monitor锁定一个对象的情形:
??...... ??Queue oQueue=new Queue(); ??...... ??Monitor.Enter(oQueue); ??......//现在oQueue对象只能被当前线程操纵了 ??Monitor.Exit(oQueue);//释放锁
??如上所示,当一个线程调用Monitor.Enter()方法锁定一个对象时,这个对象就归它所有了,其它线程想要访问这个对象,只有等待它使用Monitor.Exit()方法释放锁。为了保证线程最终都能释放锁,你可以把Monitor.Exit()方法写在try-catch-finally结构中的finally代码块里。对于任何一个被Monitor锁定的对象,内存中都保存着与它相关的一些信息,其一是现在持有锁的线程的引用,其二是一个预备队列,队列中保存了已经准备好获取锁的线程,其三是一个等待队列,队列中保存着当前正在等待这个对象状态改变的队列的引用。当拥有对象锁的线程准备释放锁时,它使用Monitor.Pulse()方法通知等待队列中的第一个线程,于是该线程被转移到预备队列中,当对象锁被释放时,在预备队列中的线程可以立即获得对象锁。 ??下面是一个展示如何使用lock关键字和Monitor类来实现线程的同步和通讯的例子,也是一个典型的生产者与消费者问题。这个例程中,生产者线程和消费者线程是交替进行的,生产者写入一个数,消费者立即读取并且显示,我将在注释中介绍该程序的精要所在。用到的系统命名空间如下:
??using System; ??using System.Threading;
 楼主| 楚轩 发表于 2015-7-25 13:38
首先,我们定义一个被操作的对象的类Cell,在这个类里,有两个方法:ReadFromCell()和WriteToCell。消费者线程将调用ReadFromCell()读取cellContents的内容并且显示出来,生产者进程将调用WriteToCell()方法向cellContents写入数据。
??public class Cell ??{   int cellContents; // Cell对象里边的内容  bool readerFlag = false; // 状态标志,为true时可以读取,为false则正在写入  public int ReadFromCell( )   {   ??lock(this) // Lock关键字保证了什么,请大家看前面对lock的介绍  ??{     if (!readerFlag)//如果现在不可读取    {     ??try     ??{       file://等待WriteToCell方法中调用Monitor.Pulse()方法      Monitor.Wait(this);     ??}     ??catch (SynchronizationLockException e)     ??{       Console.WriteLine(e);     ??}     ??catch (ThreadInterruptedException e)     ??{       Console.WriteLine(e);     ??}     }     Console.WriteLine("Consume: {0}",cellContents);     readerFlag = false; file://重置readerFlag标志,表示消费行为已经完成    Monitor.Pulse(this); file://通知WriteToCell()方法(该方法在另外一个线程中执行,等待中)  ??}   ??return cellContents;   }   public void WriteToCell(int n)   {   ??lock(this)   ??{     if (readerFlag)     {     ??try     ??{       Monitor.Wait(this);     ??}     ??catch (SynchronizationLockException e)     ??{       file://当同步方法(指Monitor类除Enter之外的方法)在非同步的代码区被调用      Console.WriteLine(e);     ??}     ??catch (ThreadInterruptedException e)     ??{       file://当线程在等待状态的时候中止       Console.WriteLine(e);     ??}     }     cellContents = n;     Console.WriteLine("Produce: {0}",cellContents);     readerFlag = true;     Monitor.Pulse(this); file://通知另外一个线程中正在等待的ReadFromCell()方法  ??}   } ??}
??下面定义生产者CellProd和消费者类CellCons,它们都只有一个方法ThreadRun(),以便在Main()函数中提供给线程的ThreadStart代{过}{滤}理对象,作为线程的入口。
??public class CellProd ??{   Cell cell; // 被操作的Cell对象  int quantity = 1; // 生产者生产次数,初始化为1   public CellProd(Cell box, int request)   {   ??//构造函数  ??cell = box;   ??quantity = request;   }   public void ThreadRun( )   {   ??for(int looper=1; looper<=quantity; looper++)     cell.WriteToCell(looper); file://生产者向操作对象写入信息  } ??} ??public class CellCons ??{   Cell cell;   int quantity = 1;   public CellCons(Cell box, int request)   {   ??cell = box;   ??quantity = request;   }   public void ThreadRun( )   {   ??int valReturned;   ??for(int looper=1; looper<=quantity; looper++)     valReturned=cell.ReadFromCell( );//消费者从操作对象中读取信息  } ??}然后在下面这个类MonitorSample的Main()函数中我们要做的就是创建两个线程分别作为生产者和消费者,使用CellProd.ThreadRun()方法和CellCons.ThreadRun()方法对同一个Cell对象进行操作。
??public class MonitorSample ??{   public static void Main(String[] args)   {   ??int result = 0; file://一个标志位,如果是0表示程序没有出错,如果是1表明有错误发生  ??Cell cell = new Cell( );   ??//下面使用cell初始化CellProd和CellCons两个类,生产和消费次数均为20次  ??CellProd prod = new CellProd(cell, 20);   ??CellCons cons = new CellCons(cell, 20);   ??Thread producer = new Thread(new ThreadStart(prod.ThreadRun));   ??Thread consumer = new Thread(new ThreadStart(cons.ThreadRun));   ??//生产者线程和消费者线程都已经被创建,但是没有开始执行   ??try   ??{     producer.Start( );     consumer.Start( );     producer.Join( );     consumer.Join( );     Console.ReadLine();   ??}   ??catch (ThreadStateException e)   ??{     file://当线程因为所处状态的原因而不能执行被请求的操作    Console.WriteLine(e);     result = 1;   ??}   ??catch (ThreadInterruptedException e)   ??{     file://当线程在等待状态的时候中止    Console.WriteLine(e);     result = 1;   ??}   ??//尽管Main()函数没有返回值,但下面这条语句可以向父进程返回执行结果  ??Environment.ExitCode = result;   } ??}
 楼主| 楚轩 发表于 2015-7-25 13:39
大家可以看到,在上面的例程中,同步是通过等待Monitor.Pulse()来完成的。首先生产者生产了一个值,而同一时刻消费者处于等待状态,直到收到生产者的“脉冲(Pulse)”通知它生产已经完成,此后消费者进入消费状态,而生产者开始等待消费者完成操作后将调用Monitor.Pulese()发出的“脉冲”。它的执行结果很简单:
??Produce: 1 ??Consume: 1 ??Produce: 2 ??Consume: 2 ??Produce: 3 ??Consume: 3 ??... ??... ??Produce: 20 ??Consume: 20
??事实上,这个简单的例子已经帮助我们解决了多线程应用程序中可能出现的大问题,只要领悟了解决线程间冲突的基本方法,很容易把它应用到比较复杂的程序中去。   四、线程池和定时器——多线程的自动管理 ??在多线程的程序中,经常会出现两种情况。一种情况下,应用程序中的线程把大部分的时间花费在等待状态,等待某个事件发生,然后才能给予响应;而另外一种情况则是线程平常都处于休眠状态,只是周期性地被唤醒。在.net framework里边,我们使用ThreadPool来对付第一种情况,使用Timer来对付第二种情况。 ??ThreadPool类提供一个由系统维护的线程池——可以看作一个线程的容器,该容器需要Windows 2000以上版本的系统支持,因为其中某些方法调用了只有高版本的Windows才有的API函数。你可以使用ThreadPool.QueueUserWorkItem()方法将线程安放在线程池里,该方法的原型如下: ??//将一个线程放进线程池,该线程的Start()方法将调用WaitCallback代{过}{滤}理对象代表的函数 ??public static bool QueueUserWorkItem(WaitCallback); ??//重载的方法如下,参数object将传递给WaitCallback所代表的方法 ??public static bool QueueUserWorkItem(WaitCallback, object); ??要注意的是,ThreadPool类也是一个静态类,你不能也不必要生成它的对象,而且一旦使用该方法在线程池中添加了一个项目,那么该项目将是没有办法取消的。在这里你无需自己建立线程,只需把你要做的工作写成函数,然后作为参数传递给ThreadPool.QueueUserWorkItem()方法就行了,传递的方法就是依靠WaitCallback代{过}{滤}理对象,而线程的建立、管理、运行等等工作都是由系统自动完成的,你无须考虑那些复杂的细节问题,线程池的优点也就在这里体现出来了,就好像你是公司老板——只需要安排工作,而不必亲自动手。
 楼主| 楚轩 发表于 2015-7-25 13:39
[C#学习]在多线程中如何调用Winform 趁热打贴,哈哈,再一篇

问题的产生:   我的WinForm程序中有一个用于更新主窗口的工作线程(worker thread),但文档中却提示我不能在多线程中调用这个form(为什么?),而事实上我在调用时程序常常会崩掉。请问如何从多线程中调用form中的方法呢?   解答:   每一个从Control类中派生出来的WinForm类(包括Control类)都是依靠底层Windows消息和一个消息泵循环(message pump loop)来执行的。消息循环都必须有一个相对应的线程,因为发送到一个window的消息实际上只会被发送到创建该window的线程中去。其结果是,即使提供了同步(synchronization),你也无法从多线程中调用这些处理消息的方法。大多数plumbing是掩藏起来的,因为WinForm是用代{过}{滤}理(delegate)将消息绑定到事件处理方法中的。WinForm将Windows消息转换为一个基于代{过}{滤}理的事件,但你还是必须注意,由于最初消息循环的缘故,只有创建该form的线程才能调用其事件处理方法。如果你在你自己的线程中调用这些方法,则它们会在该线程中处理事件,而不是在指定的线程中进行处理。你可以从任何线程中调用任何不属于消息处理的方法。   Control类(及其派生类)实现了一个定义在System.ComponentModel命名空间下的接口 -- ISynchronizeInvoke,并以此来处理多线程中调用消息处理方法的问题:
public interface ISynchronizeInvoke {  object Invoke(Delegate method,object[] args);  IAsyncResult BeginInvoke(Delegate method,object[] args);  object EndInvoke(IAsyncResult result);  bool InvokeRequired {get;} }
  ISynchronizeInvoke提供了一个普通的标准机制用于在其他线程的对象中进行方法调用。例如,如果一个对象实现了ISynchronizeInvoke,那么在线程T1上的客户端可以在该对象中调用ISynchronizeInvoke的Invoke()方法。Invoke()方法的实现会阻塞(block)该线程的调用,它将调用打包发送(marshal)到 T2,并在T2中执行调用,再将返回值发送会T1,然后返回到T1的客户端。Invoke()方法以一个代{过}{滤}理来定位该方法在T2中的调用,并以一个普通的对象数组做为其参数。   调用者还可以检查InvokeRequired属性,因为你既可以在同一线程中调用ISynchronizeInvoke也可以将它重新定位(redirect)到其他线程中去。如果InvokeRequired的返回值是false的话,则调用者可以直接调用该对象的方法。   比如,假设你想要从另一个线程中调用某个form中的Close方法,那么你可以使用预先定义好的的MethodInvoker代{过}{滤}理,并调用Invoke方法:
Form form; /* obtain a reference to the form, then: */ ISynchronizeInvoke synchronizer; synchronizer = form; if(synchronizer.InvokeRequired) { MethodInvoker invoker = new MethodInvoker(form.Close); synchronizer.Invoke(invoker,null); } else form.Close();
  ISynchronizeInvoke不仅仅用于WinForm中。例如,一个Calculator类提供了将两个数字相加的Add()方法,它就是通过ISynchronizeInvoke来实现的。用户必须确定ISynchronizeInvoke.Invoke()方法的调用是执行在正确的线程中的。   C# 在正确的线程中写入调用   列表A. Calculator类的Add()方法用于将两个数字相加。如果用户直接调用Add()方法,它会在该用户的线程中执行调用,而用户可以通过ISynchronizeInvoke.Invoke()将调用写入正确的线程中。   列表A:
public class Calculator : ISynchronizeInvoke {  public int Add(int arg1,int arg2)  {   int threadID = Thread.CurrentThread.GetHashCode();   Trace.WriteLine( "Calculator thread ID is " + threadID.ToString());   return arg1 + arg2;  }  //ISynchronizeInvoke implementation  public object Invoke(Delegate method,object[] args)  {   public IAsyncResult BeginInvoke(Delegate method,object[] args)   {    public object EndInvoke(IAsyncResult result)    {     public bool InvokeRequired     {     }    }    //Client-side code    public delegate int AddDelegate(int arg1,int arg2);
    int threadID = Thread.CurrentThread.GetHashCode();     Trace.WriteLine("Client thread ID is " + threadID.ToString());
    Calculator calc;     /* Some code to initialize calc */
    AddDelegate addDelegate = new AddDelegate(calc.Add);
    object[] arr = new object[2];     arr[0] = 3;     arr[1] = 4;
    int sum = 0;     sum = (int) calc.Invoke(addDelegate,arr);     Debug.Assert(sum ==7);
    /* Possible output:     Calculator thread ID is 29     Client thread ID is 30     */
  或许你并不想进行同步调用,因为它被打包发送到另一个线程中去了。你可以通过BeginInvoke()和EndInvoke()方法来实现它。你可以依照通用的.NET非同步编程模式(asynchronous programming model)来使用这些方法:用BeginInvoke()来发送调用,用EndInvoke()来实现等待或用于在完成时进行提示以及收集返回结果。   还值得一提的是ISynchronizeInvoke方法并非安全类型。 类型不符会导致在执行时被抛出异常,而不是编译错误。所以在使用ISynchronizeInvoke时要格外注意,因为编辑器无法检查出执行错误。   实现ISynchronizeInvoke要求你使用一个代{过}{滤}理来在后期绑定(late binding)中动态地调用方法。每一种代{过}{滤}理类型均提供DynamicInvoke()方法: public object DynamicInvoke(object[] args);   理论上来说,你必须将一个方法代{过}{滤}理放到一个需要提供对象运行的真实的线程中去,并使Invoke() 和BeginInvoke()方法中的代{过}{滤}理中调用DynamicInvoke()方法。ISynchronizeInvoke的实现是一个非同一般的编程技巧,本文附带的源文件中包含了一个名为Synchronizer的帮助类(helper class)和一个测试程序,这个测试程序是用来论证列表A中的Calculator类是如何用Synchronizer类来实现ISynchronizeInvoke的。Synchronizer是ISynchronizeInvoke的一个普通实现,你可以使用它的派生类或者将其本身作为一个对象来使用,并将ISynchronizeInvoke实现指派给它。   用来实现Synchronizer的一个重要元素是使用一个名为WorkerThread的嵌套类(nested class)。WorkerThread中有一个工作项目(work item)查询。WorkItem类中包含方法代{过}{滤}理和参数。Invoke()和BeginInvoke()用来将一个工作项目实例加入到查询里。WorkerThread新建一个.NET worker线程,它负责监测工作项目的查询任务。查询到项目之后,worker会读取它们,然后调用DynamicInvoke()方法
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-14 18:57

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表