`
wsql
  • 浏览: 12102035 次
  • 性别: Icon_minigender_1
  • 来自: 深圳
文章分类
社区版块
存档分类
最新评论

C#的多线程机制初探

阅读更多
注:本文中出现的代码均在.netFrameworkRC3环境中运行通过

一.多线程的概念

Windows是一个多任务的系统,如果你使用的是windows2000及其以上版本,你可以通过任务管理器查看当前系统运行的程序和进程。什么是进程呢?当一个程序开始运行时,它就是一个进程,进程所指包括运行中的程序和程序所使用到的内存和系统资源。而一个进程又是由多个线程所组成的,线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。浏览器就是一个很好的多线程的例子,在浏览器中你可以在下载JAVA小应用程序或图象的同时滚动页面,在访问新页面时,播放动画和声音,打印文件等。

多线程的好处在于可以提高CPU的利用率——任何一个程序员都不希望自己的程序很多时候没事可干,在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率。

然而我们也必须认识到线程本身可能影响系统性能的不利方面,以正确使用线程:

线程也是程序,所以线程需要占用内存,线程越多占用内存也越多
多线程需要协调和管理,所以需要CPU时间跟踪线程
线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题
线程太多会导致控制太复杂,最终可能造成很多Bug

基于以上认识,我们可以一个比喻来加深理解。假设有一个公司,公司里有很多各司其职的职员,那么我们可以认为这个正常运作的公司就是一个进程,而公司里的职员就是线程。一个公司至少得有一个职员吧,同理,一个进程至少包含一个线程。在公司里,你可以一个职员干所有的事,但是效率很显然是高不起来的,一个人的公司也不可能做大;一个程序中也可以只用一个线程去做事,事实上,一些过时的语言如fortune,basic都是如此,但是象一个人的公司一样,效率很低,如果做大程序,效率更低——事实上现在几乎没有单线程的商业软件。公司的职员越多,老板就得发越多的薪水给他们,还得耗费大量精力去管理他们,协调他们之间的矛盾和利益;程序也是如此,线程越多耗费的资源也越多,需要CPU时间去跟踪线程,还得解决诸如死锁,同步等问题。总之,如果你不想你的公司被称为“皮包公司”,你就得多几个员工;如果你不想让你的程序显得稚气,就在你的程序里引入多线程吧!

本文将对C#编程中的多线程机制进行探讨,通过一些实例解决对线程的控制,多线程间通讯等问题。为了省去创建GUI那些繁琐的步骤,更清晰地逼近线程的本质,下面所有的程序都是控制台程序,程序最后的Console.ReadLine()是为了使程序中途停下来,以便看清楚执行过程中的输出。

好了,废话少说,让我们来体验一下多线程的C#吧!
二.操纵一个线程

任何程序在执行时,至少有一个主线程,下面这段小程序可以给读者一个直观的印象:

[CODE]
//SystemThread.cs
usingSystem;
usingSystem.Threading;

namespaceThreadTest
{
  classRunIt
  {
    [STAThread]
    staticvoidMain(string[]args)
    {
      Thread.CurrentThread.Name="SystemThread";//给当前线程起名为"SystemThread"
Console.WriteLine(Thread.CurrentThread.Name+"'Status:"+Thread.CurrentThread.ThreadState);
      Console.ReadLine();
    }
  }
}
[/CODE]

编译执行后你看到了什么?是的,程序将产生如下输出:

SystemThread'sStatus:Running

在这里,我们通过Thread类的静态属性CurrentThread获取了当前执行的线程,对其Name属性赋值“SystemThread”,最后还输出了它的当前状态(ThreadState)。所谓静态属性,就是这个类所有对象所公有的属性,不管你创建了多少个这个类的实例,但是类的静态属性在内存中只有一个。很容易理解CurrentThread为什么是静态的——虽然有多个线程同时存在,但是在某一个时刻,CPU只能执行其中一个。

就像上面程序所演示的,我们通过Thread类来创建和控制线程。注意到程序的头部,我们使用了如下命名空间:
以下内容为程序代码:

usingSystem;
usingSystem.Threading;

在.netframeworkclasslibrary中,所有与多线程机制应用相关的类都是放在System.Threading命名空间中的。其中提供Thread类用于创建线程,ThreadPool类用于管理线程池等等,此外还提供解决了线程执行安排,死锁,线程间通讯等实际问题的机制。如果你想在你的应用程序中使用多线程,就必须包含这个类。Thread类有几个至关重要的方法,描述如下:

Start():启动线程
Sleep(int):静态方法,暂停当前线程指定的毫秒数
Abort():通常使用该方法来终止一个线程
Suspend():该方法并不终止未完成的线程,它仅仅挂起线程,以后还可恢复。
Resume():恢复被Suspend()方法挂起的线程的执行
下面我们就动手来创建一个线程,使用Thread类创建线程时,只需提供线程入口即可。线程入口使程序知道该让这个线程干什么事,在C#中,线程入口是通过ThreadStart代理(delegate)来提供的,你可以把ThreadStart理解为一个函数指针,指向线程要执行的函数,当调用Thread.Start()方法后,线程就开始执行ThreadStart所代表或者说指向的函数。

打开你的VS.net,新建一个控制台应用程序(ConsoleApplication),下面这些代码将让你体味到完全控制一个线程的无穷乐趣!

//ThreadTest.cs

usingSystem;
usingSystem.Threading;

namespaceThreadTest
{
publicclassAlpha
    {
      publicvoidBeta()
      {
        while(true)
        {
          Console.WriteLine("Alpha.Betaisrunninginitsownthread.");
        }
      }
    };

    publicclassSimple
    {
      publicstaticintMain()
      {
        Console.WriteLine("ThreadStart/Stop/JoinSample");

        AlphaoAlpha=newAlpha();
        //这里创建一个线程,使之执行Alpha类的Beta()方法
        ThreadoThread=newThread(newThreadStart(oAlpha.Beta));
        oThread.Start();
        while(!oThread.IsAlive);
        Thread.Sleep(1);
        oThread.Abort();
        oThread.Join();
        Console.WriteLine();
        Console.WriteLine("Alpha.Betahasfinished");
        try
        {
          Console.WriteLine("TrytorestarttheAlpha.Betathread");
          oThread.Start();
        }
        catch(ThreadStateException)
        {
          Console.Write("ThreadStateExceptiontryingtorestartAlpha.Beta.");
          Console.WriteLine("Expectedsinceabortedthreadscannotberestarted.");
          Console.ReadLine();
        }
        return0;
      }
    }
  }

这段程序包含两个类Alpha和Simple,在创建线程oThread时我们用指向Alpha.Beta()方法的初始化了ThreadStart代理(delegate)对象,当我们创建的线程oThread调用oThread.Start()方法启动时,实际上程序运行的是Alpha.Beta()方法:

AlphaoAlpha=newAlpha();
  ThreadoThread=newThread(newThreadStart(oAlpha.Beta));
  oThread.Start();

然后在Main()函数的while循环中,我们使用静态方法Thread.Sleep()让主线程停了1ms,这段时间CPU转向执行线程oThread。然后我们试图用Thread.Abort()方法终止线程oThread,注意后面的oThread.Join(),Thread.Join()方法使主线程等待,直到oThread线程结束。你可以给Thread.Join()方法指定一个int型的参数作为等待的最长时间。之后,我们试图用Thread.Start()方法重新启动线程oThread,但是显然Abort()方法带来的后果是不可恢复的终止线程,所以最后程序会抛出ThreadStateException异常。
在这里我们要注意的是其它线程都是依附于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,它可以把一段代码定义为互斥段(criticalsection),互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待。在C#中,关键字lock定义如下:

lock(expression)statement_block
expression代表你希望跟踪的对象,通常是对象引用。一般地,如果你想保护一个类的实例,你可以使用this;如果你希望保护一个静态变量(如互斥代码段在一个静态方法内部),一般使用类名就可以了。而statement_block就是互斥段的代码,这段代码在一个时刻内只可能被一个线程执行。

  下面是一个使用lock关键字的典型例子,我将在注释里向大家说明lock关键字的用法和用途:

//lock.cs
usingSystem;
usingSystem.Threading;

internalclassAccount
{
intbalance;
Randomr=newRandom();
internalAccount(intinitial)
{
balance=initial;
}

internalintWithdraw(intamount)
{
if(balance<0)
{
//如果balance小于0则抛出异常
thrownewException("NegativeBalance");
}
//下面的代码保证在当前线程修改balance的值完成之前
//不会有其他线程也执行这段代码来修改balance的值
//因此,balance的值是不可能小于0的
lock(this)
{
Console.WriteLine("CurrentThread:"+Thread.CurrentThread.Name);
//如果没有lock关键字的保护,那么可能在执行完if的条件判断之后
//另外一个线程却执行了balance=balance-amount修改了balance的值
//而这个修改对这个线程是不可见的,所以可能导致这时if的条件已经不成立了
//但是,这个线程却继续执行balance=balance-amount,所以导致balance可能小于0
if(balance>=amount)
{
Thread.Sleep(5);
balance=balance-amount;
returnamount;
}
else
{
return0;//transactionrejected
}
}
}
internalvoidDoTransactions()
{
for(inti=0;i<100;i++)
Withdraw(r.Next(-50,100));
}
}

internalclassTest
{
staticinternalThread[]threads=newThread[10];
publicstaticvoidMain()
{
Accountacc=newAccount(0);
for(inti=0;i<10;i++)
{
Threadt=newThread(newThreadStart(acc.DoTransactions));
threads[i]=t;
}
for(inti=0;i<10;i++)
threads[i].Name=i.ToString();
for(inti=0;i<10;i++)
threads[i].Start();
Console.ReadLine();
}
}

而多线程公用一个对象时,也会出现和公用代码类似的问题,这种问题就不应该使用lock关键字了,这里需要用到System.Threading中的一个类Monitor,我们可以称之为监视器,Monitor提供了使线程共享资源的方案。

  Monitor类可以锁定一个对象,一个线程只有得到这把锁才可以对该对象进行操作。对象锁机制保证了在可能引起混乱的情况下一个时刻只有一个线程可以访问这个对象。Monitor必须和一个具体的对象相关联,但是由于它是一个静态的类,所以不能使用它来定义对象,而且它的所有方法都是静态的,不能使用对象来引用。下面代码说明了使用Monitor锁定一个对象的情形:

......
QueueoQueue=newQueue();
......
Monitor.Enter(oQueue);
......//现在oQueue对象只能被当前线程操纵了
Monitor.Exit(oQueue);//释放锁

如上所示,当一个线程调用Monitor.Enter()方法锁定一个对象时,这个对象就归它所有了,其它线程想要访问这个对象,只有等待它使用Monitor.Exit()方法释放锁。为了保证线程最终都能释放锁,你可以把Monitor.Exit()方法写在try-catch-finally结构中的finally代码块里。对于任何一个被Monitor锁定的对象,内存中都保存着与它相关的一些信息,其一是现在持有锁的线程的引用,其二是一个预备队列,队列中保存了已经准备好获取锁的线程,其三是一个等待队列,队列中保存着当前正在等待这个对象状态改变的队列的引用。当拥有对象锁的线程准备释放锁时,它使用Monitor.Pulse()方法通知等待队列中的第一个线程,于是该线程被转移到预备队列中,当对象锁被释放时,在预备队列中的线程可以立即获得对象锁。

下面是一个展示如何使用lock关键字和Monitor类来实现线程的同步和通讯的例子,也是一个典型的生产者与消费者问题。这个例程中,生产者线程和消费者线程是交替进行的,生产者写入一个数,消费者立即读取并且显示,我将在注释中介绍该程序的精要所在。用到的系统命名空间如下:

usingSystem;
usingSystem.Threading;

分享到:
评论

相关推荐

    C#的多线程机制初探(1)C#的多线程机制初探(1)

    C#的多线程机制初探(1)C#的多线程机制初探(1)C#的多线程机制初探(1)C#的多线程机制初探(1)C#的多线程机制初探(1)C#的多线程机制初探(1)C#的多线程机制初探(1)C#的多线程机制初探(1)C#的多线程机制初探(1)

    C#的多线程机制初探(2)

    C#的多线程机制初探(1)C#的多线程机制初探(1)C#的多线程机制初探(1)C#的多线程机制初探(1)C#的多线程机制初探(1)C#的多线程机制初探(1)

    初探c#初探c#初探c#初探c#初探c#初探c#

    虽然也有这样那样的方法,但都不尽人意,但在C#中,要编写多线程应用程序却相当的简单。这篇文章将作简要的介绍,以起到抛砖引玉的作用! .NET将关于多线程的功能定义在System.Threading名字空间中。因此,要使用多...

    C# 入门初探.

    C# 是一种面向对象的编程语言,由微软公司开发,主要用于构建Windows应用程序、Web应用程序以及游戏等。...随着对C#的深入学习,你将掌握更多高级特性和编程技巧,例如面向对象编程、异常处理、集合、多线程等。

    c#写的后台管理程序

    7. **多线程**:对于处理大量并发请求的后台程序,多线程技术能提升性能。C#提供了线程池、异步编程模型(如async/await)等工具,帮助开发者有效管理并发任务。 8. **日志记录**:为了追踪程序运行状态和故障排查...

    C#委托与事件初探

    - **多线程编程**:跨线程通信和同步。 通过理解和熟练运用委托和事件,开发者可以构建更加灵活、响应式和易于维护的C#应用程序。无论是简单的控制台应用还是复杂的UI系统,委托和事件都是不可或缺的重要工具。

    C#车牌识别系统.rar

    此外,该项目的源代码不复杂,这意味着初学者可以在较短时间内掌握其核心思想,并在此基础上进行二次开发,比如增加多线程处理,提高识别效率,或者结合AI模型提升识别准确率。 总的来说,“C#车牌识别系统”是一个...

    C#超酷教程

    例如,“腾讯社区:初探c#--12,13,14.htm”可能包含更复杂的应用示例,如多线程、网络编程或数据库操作。 通过这个系列的教程,初学者可以系统地学习C#编程,从基础语法到高级特性,逐步建立起对C#编程的全面理解。...

    c#做的一个QQ程序,还有不足的地方...适合菜鸟下载下去消化,老鸟不要笑

    总的来说,"MyQQ"项目是一个综合运用C#语言和.NET框架的实践平台,涵盖了面向对象编程、事件驱动、多线程、网络通信、数据库操作等多个核心知识点。对于初学者来说,这是一个很好的学习起点,通过分析和改进这个项目...

    C#基础系列:异步编程初探async和await

    确实,没有异步的多线程是单调的、乏味的,async和await是出现在C#5.0之后,它的出现给了异步并行变成带来了很大的方便。异步编程涉及到的东西还是比较多,本篇还是先介绍下async和await的原理及简单实现。  了解...

    C#之CLR内存原理初探

    总的来说,理解C#中的内存管理机制对于编写高效、无内存泄漏的代码至关重要。通过了解托管堆、栈以及字符串常量池的工作方式,开发者能够更好地控制对象的生命周期,优化程序性能,同时避免因内存问题导致的程序错误...

    IPScan.zip

    《C#实现TCP端口扫描:单线程与多线程初探》 TCP端口扫描是网络管理员和开发者常用的一种技术,用于检测目标主机上哪些端口处于开放状态。在本文中,我们将深入探讨一个基于C#的TCP端口扫描程序的实现,特别是其单...

    C_网络编程初探C_网络编程初探

    ### C网络编程初探:深入理解C#网络编程的核心概念与实践 在探索C#网络编程的世界里,首要任务是理解其与C++等其他语言的显著差异——C#依赖于.NET Framework SDK作为其核心类库,特别是针对网络编程部分,系统提供...

    paint.net.4.2.10_C#_paint.net.4.2.10_反编译后修改_源码(不完整).zip

    此外,源码可能还揭示了Paint.NET如何利用多线程和异步编程来提升性能,尤其是在处理大型图像时。 对于开发者来说,深入理解这些源码有助于提升自己的编程技巧,尤其是对于图像处理和图形用户界面的设计。通过研究...

    网络电台精灵

    这涉及到HTTP协议、流媒体技术和多线程编程,确保视频流畅播放且不影响用户界面的响应速度。 在实现网络电视功能时,开发者可能使用了某种标准的流媒体格式,如MPEG-DASH或HLS(HTTP Live Streaming),这些格式...

    WEEX 初探1

    "Weex 初探" Weex 是一款轻量级的移动端跨平台动态性技术解决方案,前身是WeApp,目标是Write Once, Run Everywhere。它借鉴了React Native的思想,并结合Web Component和Vue.js而诞生。Weex 使用V8引擎,官方支持...

    用.net编写的未完善棋盘

    通过此项目,学习者可以深入理解.NET框架的基础知识,包括面向对象编程、UI设计、事件驱动编程、数据结构的应用以及多线程编程等。同时,也能锻炼解决问题和优化代码的能力,为今后的.NET开发奠定坚实基础。 总结来...

    xfire 初体验

    通过分析Xfire的源码,我们可以学习到如何构建一个跨平台的游戏社交应用,涉及到的技术可能包括网络编程、多线程处理、图形界面设计等。Xfire在设计时可能采用了C++或C#等语言,这为我们提供了研究面向对象编程、...

    亮剑.NET深入体验与实战精要2

    4.11 创建多线程应用程序 191 4.12 WinForm开发常见问题 194 4.12.1 如何设置运行时窗体的起始位置 194 4.12.2 如何使一个窗体在屏幕的最顶端 194 4.12.3 实现窗体渐显效果 194 4.12.4 设置窗口背景为渐变色 195 ...

    亮剑.NET深入体验与实战精要3

    4.11 创建多线程应用程序 191 4.12 WinForm开发常见问题 194 4.12.1 如何设置运行时窗体的起始位置 194 4.12.2 如何使一个窗体在屏幕的最顶端 194 4.12.3 实现窗体渐显效果 194 4.12.4 设置窗口背景为渐变色 195 ...

Global site tag (gtag.js) - Google Analytics