这些资料是我学习的时候在网络上寻找到的,发布出来希望对大家有帮助,一起努力前进,嘿嘿......Microsoft C#规范 2.0 版 GFC用户提交

feedsky 抓虾 pageflakes google reader my yahoo bloglines 鲜果 有道 http://wap.feedsky.com/bliplink
显示标签为“数据库,线程,进程”的博文。显示所有博文
显示标签为“数据库,线程,进程”的博文。显示所有博文

.NET 线程池编程技术 - Do-websoftware - 博客园

随笔-49  评论-31  文章-176  trackbacks-2

 首先声明这篇关于线程池的文章不是我的原创,我是转载的

源地址是http://www.cnblogs.com/wildman/archive/2008/08/22/1274170.html

 

摘要

深度探索 Microsoft .NET提供的线程池, 揭示什么情况下你需要用线程池以及 .NET框架下的线程池是如何实现的,并告诉你如何去使用线程池。

 

内容

介绍

.NET中的线程池

线程池中执行的函数

使用定时器

同步对象的执行

异步I/O操作

监视线程池

死锁

有关安全性

结束

 

介绍

         如果你有在任何编程语言下的多线程编程经验的话,你肯定已经非常熟悉一些典型的范例。通常,多线程编程与基于用户界面的应用联系在一起,它们需要在不影响终端用户的情况下,执行一些耗时的操作。取出任何一本参考书,打开有关线程这一章:你能找到一个能在你的用户界面中并行执行数学运算的多线程示例吗?

我的目的不是让你扔掉你的书,不要这样做!多线程编程技术使基于用户界面的应用更完美。实际上, Microsoft .NET框架支持在任何语言编写的窗口下应用多线程编程技术,允许开发人员设计非常丰富的界面,提供给终端用户一个更好的体验。但是,多线程编程技术不仅仅是为了用户界面的应用,在没有任何用户界面的应用中,一样会出现多个执行流的情况。

我们用一个“硬件商店”的客户/服务器应用系统作为例子。客户端是收银机,服务端是运行在仓库里一台独立的机器上的应用系统。你可以想象一下,服务器没有任何的用户界面,如果不用多线程技术你将如何去实现?

服务端通过通道(http, sockets, files 等等)接收来自客户端的请求并处理它们,然后发送一个应答到客户端。图1显示了它是如何运作的。

 

图1: 单线程的服务端应用系统

为了让客户端的请求不会遗漏,服务端应用系统实现了某种队列来存放这些请求。图1显示了三个请求同时到达,但只有其中的一个被服务端处理。当服务端开始执行 "Decrease stock of monkey wrench," 这个请求时,其它两个必须在队列中等待。当第一个执行完成后,接着是第二个,以此类推。这种方法普遍用于许多现有的系统,但是这样做系统的资源利用率很低。假设 “decreasing the stock”请求修改磁盘上的一个文件,而这个文件正在被修改中,CPU将不会被使用,即使这个请求正处在待处理阶段。这类系统的一个普遍特征就是低CPU利用时间导致出现很长的响应时间,甚至是在访问压力很大的环境里也这样。

         另外一个策略就是在当前的系统中为每一个请求创建不同的线程。当一个新的请求到达之后,服务端为进入的请求创建一个新线程,执行结束时,再销毁它。下图说明了这个过程:

 

         图2:多线程服务端应用系统

就像如图2所示的那样。我们有了较高的CPU利用率。即使它已经不再像原来的那样慢了,但创建线和销毁程也不是最恰当的方法。假设线程的执行操作不复杂,由于需要花额外的时间去创建和销毁线程,所以最终会严重影响系统的响应时间。另外一点就是在压力很大的环境下,这三个线程会给系统带来很多的冲击。多个线程同时执行请求处理将导致CPU的利用率达到100%,而且大多数时间会浪费在上下文切换过程中,甚至会超过处理请求的本身。这类系统的典型特征是大量的访问会导致响应时间呈指数级增长和很高的CUP使用时间。

         一个最优的实现是综合前面两种方案而提出的观点----线程池(Thread Pool),当一个请求达到时,应用系统把置入接收队列,一组的线程从队列提取请求并处理之。这个方案如下图所示:

 

 

图3:启用线程池的服务端应用系统

在这个例子中,我们用了一个含有两个线程的线程池。当三个请求到达时,它们立刻安排到队列等待被处理,因为两个线程都是空闲的,所以头两个请求开始执行。当其中任何一个请求处理结束后,空闲的线程就会去提取第三个请求并处理之。在这种场景中,系统不需要为每个请求创建和销毁线程。线程之间能互相利用。而且如果线程池的执行高效的话,它能增加或删除线程以获得最优的性能。例如当线程池在执行两个请求时,而CPU的利用率才达到50%,这表明执行请求正等待某个事件或者正在做某种I/O操作。线程池可以发现这种情况,并增加线程的数量以使系统能在同一时间处理更多的请求。相反的,如果CPU利用率达到100%,线程池可以减少线程的数量以获得更多的CPU时间,而不要浪费在上下文切换上面。

 

.NET中的线程池

         基于上面的例子,在企业级应用系统中有一个高效执行的线程池是至关重要的。Microsoft在.NET框架的开发环境中已经实现了这个,该系统的核心提供了一个现成可用的最优线程池。

这个线程池不仅对应用程序可用,而且还融合到框架中的多数类中。.NET 建立在同一个池上是一个很重要的功能特性。比如 .NET Remoting 用它来处理来自远程对象的请求。

         当一个托管应用程序开始执行时,运行时环境(runtime)提供一个线程池,它将在代码第一次访问时被创建。这个池与应用程序所在运行的物理进程关联在一起,当你用.NET框架下的同一进程中运行多个应用程序的功能特性时(称之为应用程序域),这将是一个很重要的细节。在这种情况下,由于它们都使用同样的线程池,一个坏的应用程序会影响进程中的其它应用程序。

         你可以通过System.Threading 名称空间的Thread Pool 类来使用线程池,如果你查看一下这个类,就会发现所有的成员都是静态的,而且没有公开的构造函数。这是有理由这样做的,因为每个进程只有一个线程池,并且我们不能创建新的。这个限制的目的是为了把所有的异步编程技术都集中到同一个池中。所以我们不能拥有一个通过第三方组建创建的无法管理的线程池。

 

线程池中执行的函数

ThreadPool.QueueUserWorkItem 方法运行我们在系统线程池上启动一个函数,它的声明如下:

public static bool QueueUserWorkItem (WaitCallback callBack, object state)
第一个参数指明我们将在池中执行的函数,它的声明必须与WaitCallback代理(delegate)互相匹配:public delegate void WaitCallback (object state);

State 参数允许任何类型的信息传递到该方法中,它在调用QueueUserWorkItem时传入。

 

让我们结合这些新概念,看看“硬件商店”的另一个实现。

using System;
using System.Threading;
namespace ThreadPoolTest
{
   class MainApp
   {
      static void Main()
      {
         WaitCallback callBack;
         callBack = new WaitCallback(PooledFunc);
         ThreadPool.QueueUserWorkItem(callBack,
            "Is there any screw left?");
         ThreadPool.QueueUserWorkItem(callBack,
            "How much is a 40W bulb?");
         ThreadPool.QueueUserWorkItem(callBack,a
            "Decrease stock of monkey wrench");   
         Console.ReadLine();
      }
 
      static void PooledFunc(object state)
      {
         Console.WriteLine("Processing request '{0}'", (string)state);
         // Simulation of processing time
         Thread.Sleep(2000);
         Console.WriteLine("Request processed");
      }
   }
}

为了简化例子,我们在Main 类中创建一个静态方法用于处理请求。由于代理的灵活性,我们可以指定任何实例方法去处理请求,只要这些方法的声明与代理相同。在这里范例中,通过调用Thread.Sleep,实现延迟两秒以模拟处理时间。

你如果编译和执行这个范例,将会看到下面的输出:

Processing request 'Is there any screw left?'
Processing request 'How much is a 40W bulb?'
Processing request 'Decrease stock of monkey wrench'
Request processed
Request processed
Request processed

 

注意,所有的请求都被不同的线程并行处理了。

我们可以通过在两个方法中加入如下的代码,以此看到更多的信息。

 // Main method
   Console.WriteLine("Main thread. Is pool thread: {0}, Hash: {1}",
            Thread.CurrentThread.IsThreadPoolThread, 
            Thread.CurrentThread.GetHashCode());
   // Pool method
   Console.WriteLine("Processing request '{0}'." + 
      " Is pool thread: {1}, Hash: {2}",
      (string)state, Thread.CurrentThread.IsThreadPoolThread, 
      Thread.CurrentThread.GetHashCode());
 

我们增加了一个Thread.CurrentThread.IsThreadPoolThread的调用。如果目标线程属于线程池,这个属性将返回True。另外,我们还显示了用GetHashCode 方法从当前线程返回的结果。它是唯一标识当前执行线程的值。现在看一看这个输出结果:

Main thread. Is pool thread: False, Hash: 2
Processing request 'Is there any screw left?'. Is pool thread: True, Hash: 4
Processing request 'How much is a 40W bulb?'. Is pool thread: True, Hash: 8
Processing request 'Decrease stock of monkey wrench '. Is pool thread: True, Hash: 9
Request processed
Request processed
Request processed
 

你可以看到所有的请求都被系统线程池中的不同线程执行。再次运行这个例子,注意系统CPU的利用率,如果你没有任何其它应用程序在后台运行的话,它几乎是0%。因为系统唯一正在做的是每执行2秒后就挂起的处理。

         我们来修改一下这个应用,这次我们不挂起处理请求的线程,相反我们会一直让系统忙,为了做到这点,我们用Environment.TickCount. 构建一个每隔两秒就对请求执行一次的循环。

int ticks = Environment.TickCount;
while(Environment.TickCount - ticks < 2000);

现在打开任务管理器,看一看CPU的使用率,你将看到应用程序占有了CPU的100%的使用率。再看一下我们程序的输出结果:

Processing request 'Is there any screw left?'. Is pool thread: True, Hash: 7
Processing request 'How much is a 40W bulb?'. Is pool thread: True, Hash: 8
Request processed
Processing request 'Decrease stock of monkey wrench '. Is pool thread: True, Hash: 7
Request processed
Request processed
 

注意第三个请求是在第一个请求处理结束之后执行的,而且线程的号码仍然用原来的7,这个原因是线程池检测到CPU的使用率已经达到100%,一直等待某个线程空闲。它并不会重新创建一个新的线程,这样就会减少线程间的上下文切换开销,以使总体性能更佳。

 

使用定时器

假如你曾经开发过Microsoft Win32的应用程序,你知道SetTimer函数是API之一,通过这个函数可以指定的一个窗口接收到来自系统时间周期的WM_TIMER消息。用这个方法遇到的第一个问题是你需要一个窗口去接收消息,所以你不能用在控制台应用程序中。另外,基于消息的实现并不是非常精确,假如你的应用程序正在处理其它消息,情况有可能更糟糕。

相对基于Win32的定时器来说, .NET 中一个很重要的改进就是创建不同的线程,该线程阻塞指定的时间,然后通知一个回调函数。这里的定时器不需要Microsoft的消息系统,所以这样就更精确,而且还能用于控制台应用程序中。以下代码显示了这个技术的一种实现:

class MainApp
{
   static void Main()
   {
      MyTimer myTimer = new MyTimer(2000);
      Console.ReadLine();
   }
}
class MyTimer
{
   int m_period;
   public MyTimer(int period)
   {
      Thread thread;
      m_period = period;
      thread = new Thread(new ThreadStart(TimerThread));
      thread.Start();
   }
   void TimerThread()
   {
      Thread.Sleep(m_period);
      OnTimer();
   }
   void OnTimer()
   {
      Console.WriteLine("OnTimer");
   }
}

这个代码一般用于Wn32应用中。每个定时器创建独立的线程,并且等待指定的时间,然后呼叫回调函数。犹如你看到的那样,这个实现的成本会非常高。如果你的应用程序使用了多个定时器,相对的线程数量也会随着使用定时器的数量而增长。

现在我们有.NET 提供的线程池,我们可以从池中改变请求的等待函数,这样就十分有效,而且会提升系统的性能。我们会遇到两个问题:

n          假如线程池已满(所有的线程都在运行中),那么这个请求排到队列中等待,而且定时器不在精确。

n          假如创建了多个定时器,线程池会因为等待它们时间片失效而非常忙。

为了避免这些问题,.NET框架的线程池提供了独立于时间的请求。用了这个函数,我们可以不用任何线程就可以拥有成千上万个定时器,一旦时间片失效,这时,线程池将会处理这些请求。

这些特色出现在两个不同的类中:

         System.Threading.Timer

                   定时器的简单版本,它运行开发人员向线程池中的定期执行的程序指定一个代理(delegate).

System.Timers.Timer

System.Threading.Timer的组件版本,允许开发人员把它拖放到一个窗口表单(form)中,可以把一个事件作为执行的函数。

这非常有助于理解上述两个类与另外一个称为System.Windows.Forms.Timer.的类。这个类只是封装了Win32中消息机制的计数器,如果你不准备开发多线程应用,那么就可以用这个类。

在下面的例子中,我们将用System.Threading.Timer 类,定时器的最简单实现,我们只需要如下定义的构造方法

public Timer(TimerCallback callback,
   object state,
   int dueTime,
   int period);

对于第一个参数(callback),我们可以指定定时执行的函数;第二个参数是传递给函数的通用对象;第三个参数是计时器开始执行前的延时;最后一个参数period,是两个执行之间的毫秒数。

下面的例子创建了两个定时器,timer1和timer2:

class MainApp
{
   static void Main()
   {
      Timer timer1 = new Timer(new TimerCallback(OnTimer), 1, 0, 2000);
      Timer timer2 = new Timer(new TimerCallback(OnTimer), 2, 0, 3000);
      Console.ReadLine();
   }
   static void OnTimer(object obj)
   {
      Console.WriteLine("Timer: {0} Thread: {1} Is pool thread: {2}", 
         (int)obj,
         Thread.CurrentThread.GetHashCode(),
         Thread.CurrentThread.IsThreadPoolThread);
   }
}

输出:

Timer: 1 Thread: 2 Is pool thread: True
Timer: 2 Thread: 2 Is pool thread: True
Timer: 1 Thread: 2 Is pool thread: True
Timer: 2 Thread: 2 Is pool thread: True
Timer: 1 Thread: 2 Is pool thread: True
Timer: 1 Thread: 2 Is pool thread: True
Timer: 2 Thread: 2 Is pool thread: True

犹如你看到的那样,两个定时器中的所有函数调用都在同一个线程中执行(ID = 2),应用程序使用的资源最小化了。

 

同步对象的执行

相对于定时器,.NET线程池允许在执行函数上同步对象,为了在多线程环境中的各线程之间共享资源,我们需要用.NET同步对象。

如果我们没有线程,或者线程必须阻塞直到事件收到信号,就像我前面提到一样,这会增加应用程序中总的线程数量,结果导致系统需要更多的资源和CPU时间。

线程池允许我们把请求进行排队,直到某个特殊的同步对象收到信号后执行。如果这个信号没有收到,请求函数将不需要任何线程,所以可以保证系统性能最优化。ThreadPool类提供了下面的方法:

public static RegisteredWaitHandle RegisterWaitForSingleObject(
   WaitHandle waitObject,
   WaitOrTimerCallback callBack,
   object state,
   int millisecondsTimeOutInterval,
   bool executeOnlyOnce);

 

第一个参数,waitObject 可以是任何继承于WaitHandle的对象:

         Mutex

     ManualResetEvent

     AutoResetEvent

就像你看到的那样,只有系统的同步对象才能用在这里,就是继承自WaitHandle的对象。你不能用其它任何的同步机制,比如moniter 或者 read-write 锁。剩余的参数允许我们指明当一个对象收到信号后执行的函数(callBack);一个传递给函数的状态(state); 线程池等待对象的最大时间 (millisecondsTimeOutInterval) 和一个标识表明对象收到信号时函数只能执行一次, (executeOnlyOnce). 下面的代理声明目的是用在函数的回调:

delegate void WaitOrTimerCallback(
   object state,
   bool timedOut);
 
如果参数 timeout 设置的最大时间已经失效,但是没有同步对象收到信号的花,这个函数就会被调用。
下面的例子用了一个手工事件和一个互斥量来通知线程池中的执行函数:
class MainApp
{
   static void Main(string[] args)
   {
      ManualResetEvent evt = new ManualResetEvent(false);
      Mutex mtx = new Mutex(true);
      ThreadPool.RegisterWaitForSingleObject(evt,
         new WaitOrTimerCallback(PoolFunc),
         null, Timeout.Infinite, true);
      ThreadPool.RegisterWaitForSingleObject(mtx,
         new WaitOrTimerCallback(PoolFunc),
         null, Timeout.Infinite, true);
      for(int i=1;i<=5;i++)
      {
         Console.Write("{0}...", i);
         Thread.Sleep(1000);
      }
      Console.WriteLine();
      evt.Set();
      mtx.ReleaseMutex();
      Console.ReadLine();
   }
   static void PoolFunc(object obj, bool TimedOut)
   {
      Console.WriteLine("Synchronization object signaled, Thread: {0} Is pool: {1}", 
         Thread.CurrentThread.GetHashCode(),
         Thread.CurrentThread.IsThreadPoolThread);
   }
}

结束显示两个函数都在线程池的同一线程中执行:

1...2...3...4...5...
Synchronization object signaled, Thread: 6 Is pool: True
Synchronization object signaled, Thread: 6 Is pool: True

 

异步I/O操作

线程池常见的应用场景就是I/O操作。多数应用系统需要读磁盘,数据发送到Sockets,因特网连接等等。所有的这些操作都有一些特征,直到他们执行操作时,才需要CPU时间。.NET 框架为所有这些可能执行的异步操作提供了I/O类。当这些操作执行完后,线程池中特定的函数会执行。尤其是在服务器应用程序中执行多线程异步操作,性能会更好。

在第一个例子中,我们将把一个文件异步写到硬盘中。看一看FileStream 的构造方法是如何使用的:

public FileStream(
   string path,
   FileMode mode,
   FleAccess access,
   FleShare share,
   int bufferSize,
   bool useAsync);

最后一个参数非常有趣,我们应该对异步执行文件的操作设置useAsync为True。如果我们没有这样做,即使我们用了异步函数,它们的操作仍然会被主叫线程阻塞。

下面的例子说明了用一旦FileStream BeginWrite方法写文件操作结束,线程池中的一个回调函数将会被执行。注意我们可以在任何时候访问IAsyncResult接口,它可以用来了解当前操作的状态。我们可以用CompletedSynchronously 属性指示一个异步操作是否完成,而当一个操作结束时,IsCompleted 属性会设上一个值。IAsyncResult 提供了很多有趣的属性,比如:AsyncWaitHandle ,一旦操作完成,一个异步对象将会被通知。

class MainApp
{
   static void Main()
   {
      const string fileName = "temp.dat";
      FileStream fs;
      byte[] data = new Byte[10000];
      IAsyncResult ar;
 
      fs = new FileStream(fileName, 
         FileMode.Create, 
         FileAccess.Write, 
         FileShare.None, 
         1, 
         true);
      ar = fs.BeginWrite(data, 0, 10000,
         new AsyncCallback(UserCallback), null);
      Console.WriteLine("Main thread:{0}",
         Thread.CurrentThread.GetHashCode());
      Console.WriteLine("Synchronous operation: {0}",
         ar.CompletedSynchronously);
      Console.ReadLine();
   }
   static void UserCallback(IAsyncResult ar)
   {
      Console.Write("Operation finished: {0} on thread ID:{1}, is pool: {2}", 
         ar.IsCompleted, 
         Thread.CurrentThread.GetHashCode(), 
         Thread.CurrentThread.IsThreadPoolThread);
   }
}

输出的结果显示了操作是异步执行的,一旦操作结束后,用户的函数就在线程池中执行。

Main thread:9
Synchronous operation: False
Operation finished: True on thread ID:10, is pool: True

在应用Sockets的场景中,由于I/O操作通常比磁盘操作慢,这时用线程池就显得尤为重要。过程跟前面提到的差不多,Socket 类提供了多个方法用于执行异步操作:

         BeginRecieve

         BeginSend

         BeginConnect

         BeginAccept

假如你的服务器应用使用了Socket来与客户端通讯,一定会用到这些方法。这种方法取代了对每个客户端连接都启用一个线程的做法,所有的操作都在线程池中异步执行。

         下面的例子用另外一个支持异步操作的类,HttpWebRequest。用这个类,我们可以建立一个到Web服务器的连接。这个方法叫BeginGetResponse, 但在这个例子中有一个很重要的区别。在上面最后一个示例中,我们没有用到从操作中返回的结果。但是,我们现在需要当一个操作结束时从Web服务器返回的响应,为了接收到这个信息,.NET中所有提供异步操作的类都提供了成对的方法。在HttpWebRequest这个类中,这个成对的方法就是:BeginGetResponse 和EndGetResponse。用了End版本,我们可以接收操作的结果。在我们的示例中,EndGetResponse 会从Web服务器接收响应。

虽然可以在任何时间调用EndGetResponse 方法,但在我们的例子中是在回调函数中做的。仅仅是因为我们想知道已经做了异步请求。如果我们在之前调用EndGetResponse ,这个调用将一直阻塞到操作完成。

         在下面的例子中,我们发送一个请求到Microsoft Web,然后显示了接收到响应的大小。

class MainApp
{
   static void Main()
   {
      HttpWebRequest request;
      IAsyncResult ar;
 
      request = (HttpWebRequest)WebRequest.CreateDefault(
         new Uri("http://www.microsoft.com"));
      ar = request.BeginGetResponse(new AsyncCallback(PoolFunc), request);
      Console.WriteLine("Synchronous: {0}", ar.CompletedSynchronously);
      Console.ReadLine();
   }
   static void PoolFunc(IAsyncResult ar)
   {
      HttpWebRequest request;
      HttpWebResponse response;
 
      Console.WriteLine("Response received on pool: {0}",
         Thread.CurrentThread.IsThreadPoolThread);
      request = (HttpWebRequest)ar.AsyncState;
      response = (HttpWebResponse)request.EndGetResponse(ar);
      Console.WriteLine(" Response size: {0}",
         response.ContentLength);
   }
}

下面刚开始结果信息表明,异步操作正在执行:

Synchronous: False

过了一会儿,响应接收到了。下面的结果显示:

Response received on pool: True
   Response size: 27331

就像你看到的那样,一旦收到响应,线程池的异步函数就会执行。

 

监视线程池

ThreadPool 类提供了两个方法用来查询线程池的状态。第一个是我们可以从线程池获取当前可用的线程数量:
public static void GetAvailableThreads(
   out int workerThreads,
   out int completionPortThreads);

从方法中你可以看到两种不同的线程:

         WorkerThreads

       工作线程是标准系统池的一部分。它们是被.NET框架托管的标准线程,多数函数是在这里执行的。显式的用户请求(QueueUserWorkItem方法),基于异步对象的方法(RegisterWaitForSingleObject)和定时器(Timer类)

 

CompletionPortThreads

这种线程常常用来I/O操作,Windows NT, Windows 2000 和 Windows XP提供了一个步执行的对象,叫做IOCompletionPort。把API和异步对象关联起来,用少量的资源和有效的方法,我们就可以调用系统线程池的异步I/O操作。但是在Windows 95, Windows 98, 和 Windows Me有一些局限。比如: 在某些设备上,没有提供IOCompletionPorts 功能和一些异步操作,如磁盘和邮件槽。在这里你可以看到.NET框架的最大特色:一次编译,可以在多个系统下运行。根据不同的目标平台,.NET 框架会决定是否使用IOCompletionPorts API,用最少的资源达到最好的性能。

这节包含一个使用Socket 类的例子。在这个示例中,我们将异步建立一个连接到本地的Web服务器,然后发送一个Get请求。通过这个例子,我们可以很容易地鉴别这两种不同的线程。

using System;
using System.Threading;
using System.Net;
using System.Net.Sockets;
using System.Text;
 
namespace ThreadPoolTest
{
   class MainApp
   {
      static void Main()
      {
         Socket s;
         IPHostEntry hostEntry;
         IPAddress ipAddress;
         IPEndPoint ipEndPoint;
         
         hostEntry = Dns.Resolve(Dns.GetHostName());
         ipAddress = hostEntry.AddressList[0];
         ipEndPoint = new IPEndPoint(ipAddress, 80);
         s = new Socket(ipAddress.AddressFamily,
            SocketType.Stream, ProtocolType.Tcp);
         s.BeginConnect(ipEndPoint, new AsyncCallback(ConnectCallback),s);
         
         Console.ReadLine();
      }
      static void ConnectCallback(IAsyncResult ar)
      {
         byte[] data;
         Socket s = (Socket)ar.AsyncState;
         data = Encoding.ASCII.GetBytes("GET /"n");
 
         Console.WriteLine("Connected to localhost:80");
         ShowAvailableThreads();
         s.BeginSend(data, 0,data.Length,SocketFlags.None,
            new AsyncCallback(SendCallback), null);
      }
      static void SendCallback(IAsyncResult ar)
      {
         Console.WriteLine("Request sent to localhost:80");
         ShowAvailableThreads();
      }
      static void ShowAvailableThreads()
      {
         int workerThreads, completionPortThreads;
 
         ThreadPool.GetAvailableThreads(out workerThreads,
            out completionPortThreads);
         Console.WriteLine("WorkerThreads: {0}," + 
            " CompletionPortThreads: {1}",
            workerThreads, completionPortThreads);
      }
   }
}

 

如果你在Microsoft Windows NT, Windows 2000, or Windows XP 下运行这个程序,你将会看到如下结果:

Connected to localhost:80
WorkerThreads: 24, CompletionPortThreads: 25
Request sent to localhost:80
WorkerThreads: 25, CompletionPortThreads: 24

 

如你所看到地那样,连接用了工作线程,而发送数据用了一个完成端口(CompletionPort),接着看下面的顺序:

1.   我们得到一个本地IP地址,然后异步连接到那里。

2.   Socket在工作线程上执行异步连接操作,因为在Socket上,不能用Windows 的IOCompletionPorts来建立连接。

3.   一旦连接建立了,Socket类调用指明的函数ConnectCallback,这个回调函数显示了线程池中可用的线程数量。我们可以看到这些是在工作线程中执行的。

4.   在用ASCII码对Get请求进行编码后,我们用BeginSend方法从同样的函数ConnectCallback 中发送一个异步请求。

5.   Socket上的发送和接收操作可以通过IOCompletionPort 来执行异步操作,所以当请求做完后,回调函数就会在一个CompletionPort类型的线程中执行。因为函数本身显示了可用的线程数量,所以我们可以通过这个来查看,对应的完成端口数已经减少了多少。

如果我们在Windows 95, Windows 98, 或者 Windows Me平台上运行相同的代码,会出现相同的连接结果,请求将被发送到工作线程,而非完成端口。你应该知道的很重要的一点就是,Socket类总是会利用最优的可用机制,所以你在开发应用时,可以不用考虑目标平台是什么。

       你已经看到在上面的例子中每种类型的线程可用的最大数是25。我们可以用GetMaxThreads返回这个值:

public static void GetMaxThreads(
   out int workerThreads,
   out int completionPortThreads);

一旦到了最大的数量,就不会创建新线程,所有的请求都将被排队。假如你看过ThreadPool类的所有方法,你将发现没有一个允许我们更改最大数的方法。就像我们前面提到的那样,线程池是每个处理过程的唯一共享资源。这就是为什么不可能让应用程序域去更改这个配置的原因。想象一下出现这种情况的后果,如果有第三方组件把线程池中线程的最大数改为1,整个应用都会停止工作,甚至在进程中其它的应用程序域都将受到影响。同样的原因,公共语言运行时的宿主也有可能去更改这个配置。比如:ASP.NET允许系统管理员更改这个数字。

 

死锁

在你的应用程序使用线程池之前,还有一个东西你应该知道:死锁。在线程池中执行一个实现不好的异步对象可能导致你的整个应用系统中止运行。

       设想你的代码中有个方法,它需要通过Socket连接到一个Web服务器上。一个可能的实现就是用Socket 类中的BeginConnect方法异步打开一个连接,然后用EndConnect方法等待连接的建立。代码如下:

        class ConnectionSocket
{
   public void Connect()
   {
      IPHostEntry ipHostEntry = Dns.Resolve(Dns.GetHostName());
      IPEndPoint ipEndPoint = new IPEndPoint(ipHostEntry.AddressList[0],
         80);
      Socket s = new Socket(ipEndPoint.AddressFamily, SocketType.Stream,
         ProtocolType.Tcp);
      IAsyncResult ar = s.BeginConnect(ipEndPoint, null, null);
      s.EndConnect(ar);
   }
}

多快,多好。调用BeginConnect使异步操作在线程池中执行,而EndConnect一直阻塞到连接被建立。

       如果线程池中的一个执行函数中用了这个类的方法,将会发生什么事情呢?设想线程池的大小只有两个线程,然后用我们的连接类创建了两个异步对象。当这两个函数同时在池中执行时,线程池已经没有用于其它请求的空间了,除非直到某个函数结束。问题是这些函数调用了我们类中的Connect方法,这个方法在线程池中又发起了一个异步操作。但线程池一直是满的,所以请求就一直等待任何空闲线程的出现。不幸的是,这将永远不会发生,因为使用线程池的函数正等待队列函数的结束。结论就是:我们的应用系统已经阻塞了。

       我们以此推断25个线程的线程池的行为。假如25个函数都等待异步对象操作的结束。结果将是一样的,死锁一样会出现。

       在下面的代码片断中,我们使用了这个类来说明问题:

class MainApp
{
   static void Main()
   {
      for(int i=0;i<30;i++)
      {
         ThreadPool.QueueUserWorkItem(new WaitCallback(PoolFunc));
      }
      Console.ReadLine();
   }
 
   static void PoolFunc(object state)
  {
      int workerThreads,completionPortThreads;
      ThreadPool.GetAvailableThreads(out workerThreads,
         out completionPortThreads);
      Console.WriteLine("WorkerThreads: {0}, CompletionPortThreads: {1}", 
         workerThreads, completionPortThreads);
 
      Thread.Sleep(15000);
      ConnectionSocket connection = new ConnectionSocket();
      connection.Connect();
   }
}

如果你运行这个例子,你将看到池中的线程是如何把线程的可用数量减少到零的,接着应用中止,死锁出现了。

       如果你想在你的应用中避免出现死锁,永远不要阻塞正在等待线程池中的其它函数的线程。这看起来很容易,但记住这个规则意味着有两条:

n          不要创建这样的类,它的同步方法在等待异步函数。因为这种类可能被线程池中的线程调用。

n          不要在任何异步函数中使用这样的类,如果它正等待着这个异步函数。

如果你想检测到应用中的死锁情况,那么就当你的系统挂起时,检查线程池中的线程可用数。线程的可用数量已经没有并且CPU的使用率为0 ,这是很明显的死锁症状。你应该检查你的代码,以确定哪个在线程中执行的函数正在等待异步操作,然后删除它。

      

有关安全性

         如果你再看看ThreadPool类,你会看到有两个方法我们没有用到,UnsafeQueueUserWorkItem 和UnsafeRegisterWaitForSingleObject。 为了完全理解这些方法,首先,我们必须回忆 .NET框架中安全策略是怎么运作的。

          Windows安全机制是关注资源。操作系统本身允许对文件,用户,注册表键值和任何其它的系统资源设定权限。这种方法对应用系统的用户认证非常有效,但当出现用户对他使用的系统产生不信任的情况时,这就会有些局限性。例如这些程序是从Internet下载的。在这种情况下,一旦用户安装了这个程序,它就可以执行用户权限范围内的任何操作。举个例子,假如用户可以删除他公司内的任何共享文件,任何从Internet下载的程序也都可以这样做。

         .NET 提供了应用到程序的安全性策略,而不是用户。这就是说,在用户权限的范围内,我们可以限制任何执行单元(程序集)使用的资源。通过MMC,我们可以根据条件定义一组程序集,然后为每组设置不同的策略,一个典型的例子就是限制从Internet下载的程序访问磁盘的权限。

         为了让这个功能运转起来,.NET 框架必须维护一个不同程序集之间的调用栈。假设一个应用没有权限访问磁盘,但是它调用了一个对整个系统都可以访问的类库,当第二个程序集执行一个磁盘的操作时,设置到这个程序集的权限允许这样做,但是权限不会被应用到主叫程序集,.NET不仅要检查当前程序集的权限,而且会检查整个调用栈的权限。这个栈已经被高度优化了,但是它们给两个不同程序集之间的调用增加了额外的负担。

         UnsafeQueueUserWorkItem , UnsafeRegisterWaitForSingleObject与 QueueUserWorkItem , RegisterWaitForSingleObject两个方法类似。由于是非安全版本不会维护它们执行函数之间的调用栈,所以非安全版本运行的更快些。但是回调函数将只在当前程序集的安全策略下执行,它就不能应用权限到整个调用栈中的程序集。

         我的建议是仅在性能非常重要的、安全已经控制好的极端情况下才用非安全版本。例如,你构建的应用程序不会被其它的程序集调用,或者仅被很明确清楚的程序集使用,那么你可以用非安全版本。如果你开发的类库会被第三方应用程序中使用,那么你就不应该用这些方法,因为它们可能用你的库获取访问系统资源的权限。

         在下面例子中,你可以看到用UnsafeQueueUserWorkItem方法的风险。我们将构建两个单独的程序集,在第一个程序集中我们将在线程池中创建一个文件,然后我们将导出一个类以使这个操作可以被其它的程序集执行。

using System;
using System.Threading;
using System.IO;
namespace ThreadSecurityTest
{
   public class PoolCheck
   {
      public void CheckIt()
      {
         ThreadPool.QueueUserWorkItem(new WaitCallback(UserItem), null);
      }
      private void UserItem(object obj)
      {
         FileStream fs = new FileStream("test.dat", FileMode.Create);
         fs.Close();
         Console.WriteLine("File created");
      }
   }
}

第二个程序集引用了第一个,并且用了CheckIt 方法去创建一个文件:

using System;
namespace ThreadSecurityTest
{
   class MainApp
   {
      static void Main()
      {
         PoolCheck pc = new PoolCheck();
         pc.CheckIt();
         Console.ReadLine();
      }
   }
}

编译这两个程序集,然后运行main应用。默认情况下,你的应用被配置为允许执行磁盘操作,所以系统成功生成文件。

     File created

现在,打开.NET框架的配置。为了简化这个例子,我们仅创建一个代码组关联到main应用。接着展开 运行库安全策略/ 计算机/ 代码组/ All_Code /,增加一个叫ThreadSecurityTest的组。在向导中,选择Hash 条件并导入Hash到我们的应用中,设置为Internet 级别,并选择“该策略级别将只具有与此代码组关联的权限集中的权限”选项。

运行应用程序,看看会发生什么情况:

Unhandled Exception: System.Security.SecurityException: Request for the 
   permission of type System.Security.Permissions.FileIOPermission, 
      mscorlib, Version=1.0.3300.0, Culture=neutral, 
         PublicKeyToken=b77a5c561934e089 failed.

我们的策略开始工作,系统已经不能创建文件了。这是因为.NET框架为我们维护了一个调用栈才使它成为了可能,虽然创建文件的库有权限去访问系统。

现在把库中的QueueUserWorkItem替换为UnsafeQueueUserWorkItem,再次编译程序集,然后运行Main程序。现在的结果是:

File created

即使我们的系统没有足够的权限去访问磁盘,但我们已经创建了一个向整个系统公开它的功能的库,却没有维护它的调用栈。记住一个金牌规则: 仅在你的代码不允许让其它的应用系统调用,或者当你想要严格限制访问很明确清楚的程序集,才使用非安全的函数。

 

结束

         在这篇文章中,我们知道了为什么在我们的服务器应用中需要使用线程池来优化资源和CPU的利用。我们学习了一个线程池是如何实现的,需要考虑多个因素如:CPU使用的百分比,队列请求或者系统的处理器数量。

         .NET提供了丰富的线程池的功能以让我们的应用程序使用, 并且与.NET框架的类紧密地集成在一起。这个线程池是高度优化了的,它只需要最少的CPU时间和资源,而且总能适应目标平台。

         因为与框架集成在一起,所以框架中的大部分类都提供了使用线程池的内在功能,给开发人员提供了集中管理和监视应用中的线程池的功能。鼓励第三方组件使用线程池,这样它们的客户就可以享受.NET所提供的全部功能。允许执行用户函数,定时器,I/O操作和同步对象。

         假如你在开发服务器应用系统,只要有可能就在你的请求处理系统中使用线程池。或者你开发了一个让服务器程序使用的库,那么尽可能提供系统线程池的异步对象处理。

posted on 2008-09-04 18:23 web-software 阅读(252) 评论(1)  编辑 收藏 网摘 所属分类: .NET

评论:
#1楼 [楼主] 2008-09-04 20:24 | To web-software      
关于多线程中的线程同步可以参考http://www.cnblogs.com/topboy/articles/646157.html
 

......

[阅读全文]

C#的内存管理:堆栈、托管堆与指针

 
在32位的Windows操作系统中,每个进程都可以使用4GB的内存,这得益于虚拟寻址技术,在这4GB的内存中存储着可执行代码、代码加载的DLL和程序运行的所有变量,在C#中,虚拟内存中有个两个存储变量的区域,一个称为堆栈,一个称为托管堆,托管堆的出现是.net不同于其他语言的地方,堆栈存储值类型数据,而托管堆存储引用类型如类、对象,并受垃圾收集器的控制和管理。在堆栈中,一旦变量超出使用范围,其使用的内存空间会被其他变量重新使用,这时其空间中存储的值将被其他变量覆盖而不复存在,但有时候我们希望这些值仍然存在,这就需要托管堆来实现。我们用几段代码来说明其工作原理,假设已经定义了一个类class1:

class1 object1;

object1=new class1();

  第一句定义了一个class1的引用,实质上只是在堆栈中分配了一个4个字节的空间,它将用来存府后来实例化对象在托管堆中的地址,在windows中这需要4个字节来表示内存地址。第二句实例化object1对象,实际上是在托管堆中开僻了一个内存空间来存储类class1的一个具体对象,假设这个对象需要36个字节,那么object1指向的实际上是在托管堆一个大小为36个字节的连续内存空间开始的地址。由此也可以看出在C#编译器中为什么不允许使用未实例化的对象,因为这个对象在托管堆中还不存在。当对象不再使用时,这个被存储在堆栈中的引用变量将被删除,但是从上述机制可以看出,在托管堆中这个引用指向的对象仍然存在,其空间何时被释放取决垃圾收集器而不是引用变量失去作用域时。

  在使用电脑的过程中大家可能都有过这种经验:电脑用久了以后程序运行会变得越来越慢,其中一个重要原因就是系统中存在大量内存碎片,就是因为程序反复在堆栈中创建和释入变量,久而久之可用变量在内存中将不再是连续的内存空间,为了寻址这些变量也会增加系统开销。在.net中这种情形将得到很大改善,这是因为有了垃圾收集器的工作,垃圾收集器将会压缩托管堆的内存空间,保证可用变量在一个连续的内存空间内,同时将堆栈中引用变量中的地址改为新的地址,这将会带来额外的系统开销,但是,其带来的好处将会抵消这种影响,而另外一个好处是,程序员将不再花上大量的心思在内在泄露问题上。

  当然,以C#程序中不仅仅只有引用类型的变量,仍然也存在值类型和其他托管堆不能管理的对象,如果文件名柄、网络连接和数据库连接,这些变量的释放仍需要程序员通过析构函数或IDispose接口来做。

  另一方面,在某些时候C#程序也需要追求速度,比如对一个含用大量成员的数组的操作,如果仍使用传统的类来操作,将不会得到很好的性能,因为数组在C#中实际是System.Array的实例,会存储在托管堆中,这将会对运算造成大量的额外的操作,因为除了垃圾收集器除了会压缩托管堆、更新引用地址、还会维护托管堆的信息列表。所幸的是C#中同样能够通过不安全代码使用C++程序员通常喜欢的方式来编码,在标记为unsafe的代码块使用指针,这和在C++中使用指针没有什么不同,变量也是存府在堆栈中,在这种情况下声明一个数组可以使用stackalloc语法,比如声明一个存储有50个double类型的数组:

double* pDouble=stackalloc double[50]

stackalloc会给pDouble数组在堆栈中分配50个double类型大小的内存空间,可以使用pDouble[0]、*(pDouble+1)这种方式操作数组,与在C++中一样,使用指针时必须知道自己在做什么,确保访问的正确的内存空间,否则将会出现无法预料的错误。

  掌握托管堆、堆栈、垃圾收集器和不安全代码的工作原理和方式,将有助于你成为真正的优秀C#程序员。

  进程中每个线程都有自己的堆栈,这是一段线程创建时保留下的地址区域。我们的“栈内存”即在此。至于“堆”内存,我个人认为在未用new定义时,堆应该就是未“保留”未“提交”的自由空间,new的功能是在这些自由空间中保留(并提交?)出一个地址范围

  栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有FIFO的特性,在编译的时候可以指定需要的Stack的大小。在编程中,例如C/C++中,所有的局部变量都是从栈中分配内存空间,实际上也不是什么分配,只是从栈顶向上用就行,在退出函数的时候,只是修改栈指针就可以把栈中的内容销毁,所以速度最快。
   堆(Heap)是应用程序在运行的时候请求操作系统分配给自己内存,一般是申请/给予的过程,C/C++分别用malloc/New请求分配Heap,用free/delete销毁内存。由于从操作系统管理的内存分配所以在分配和销毁时都要占用时间,所以用堆的效率低的多!但是堆的好处是可以做的很大,C/C++对分配的Heap是不初始化的。
   在Java中除了简单类型(int,char等)都是在堆中分配内存,这也是程序慢的一个主要原因。但是跟C/C++不同,Java中分配Heap内存是自动初始化的。在Java中所有的对象(包括int的wrapper Integer)都是在堆中分配的,但是这个对象的引用却是在Stack中分配。也就是说在建立一个对象时从两个地方都分配内存,在Heap中分配的内存实际建立这个对象,而在Stack中分配的内存只是一个指向这个堆对象的指针(引用)而已。

  在.NET的所有技术中,最具争议的恐怕是垃圾收集(Garbage Collection,GC)了。作为.NET框架中一个重要的部分,托管堆和垃圾收集机制对我们中的大部分人来说是陌生的概念。在这篇文章中将要讨论托管堆,和你将从中得到怎样的好处。
  为什么要托管堆?
  .NET框架包含一个托管堆,所有的.NET语言在分配引用类型对象时都要使用它。像值类型这样的轻量级对象始终分配在栈中,但是所有的类实例和数组都被生成在一个内存池中,这个内存池就是托管堆。
  垃圾收集器的基本算法很简单:
  ● 将所有的托管内存标记为垃圾
  ● 寻找正被使用的内存块,并将他们标记为有效
  ● 释放所有没有被使用的内存块
  ● 整理堆以减少碎片
  托管堆优化
  看上去似乎很简单,但是垃圾收集器实际采用的步骤和堆管理系统的其他部分并非微不足道,其中常常涉及为提高性能而作的优化设计。举例来说,垃圾收集遍历整个内存池具有很高的开销。然而,研究表明大部分在托管堆上分配的对象只有很短的生存期,因此堆被分成三个段,称作generations。新分配的对象被放在generation 0中。这个generation是最先被回收的——在这个generation中最有可能找到不再使用的内存,由于它的尺寸很小(小到足以放进处理器的L2 cache中),因此在它里面的回收将是最快和最高效的。
  托管堆的另外一种优化操作与locality of reference规则有关。该规则表明,一起分配的对象经常被一起使用。如果对象们在堆中位置很紧凑的话,高速缓存的性能将会得到提高。由于托管堆的天性,对象们总是被分配在连续的地址上,托管堆总是保持紧凑,结果使得对象们始终彼此靠近,永远不会分得很远。这一点与标准堆提供的非托管代码形成了鲜明的对比,在标准堆中,堆很容易变成碎片,而且一起分配的对象经常分得很远。
  还有一种优化是与大对象有关的。通常,大对象具有很长的生存期。当一个大对象在.NET托管堆中产生时,它被分配在堆的一个特殊部分中,这部分堆永远不会被整理。因为移动大对象所带来的开销超过了整理这部分堆所能提高的性能。
  关于外部资源(External Resources)的问题
  垃圾收集器能够有效地管理从托管堆中释放的资源,但是资源回收操作只有在内存紧张而触发一个回收动作时才执行。那么,类是怎样来管理像数据库连接或者窗口句柄这样有限的资源的呢?等待,直到垃圾回收被触发之后再清理数据库连接或者文件句柄并不是一个好方法,这会严重降低系统的性能。
  所有拥有外部资源的类,在这些资源已经不再用到的时候,都应当执行Close或者Dispose方法。从Beta2(译注:本文中所有的Beta2均是指.NET Framework Beta2,不再特别注明)开始,Dispose模式通过IDisposable接口来实现。这将在本文的后续部分讨论。
  需要清理外部资源的类还应当实现一个终止操作(finalizer)。在C#中,创建终止操作的首选方式是在析构函数中实现,而在Framework层,终止操作的实现则是通过重载System.Object.Finalize 方法。以下两种实现终止操作的方法是等效的:
  ~OverdueBookLocator()
  {
   Dispose(false);
  }
  和:
  public void Finalize()
  {
   base.Finalize();
   Dispose(false);
  }
  在C#中,同时在Finalize方法和析构函数实现终止操作将会导致错误的产生。
  除非你有足够的理由,否则你不应该创建析构函数或者Finalize方法。终止操作会降低系统的性能,并且增加执行期的内存开销。同时,由于终止操作被执行的方式,你并不能保证何时一个终止操作会被执行。
  内存分配和垃圾回收的细节
  对GC有了一个总体印象之后,让我们来讨论关于托管堆中的分配与回收工作的细节。托管堆看起来与我们已经熟悉的C++编程中的传统的堆一点都不像。在传统的堆中,数据结构习惯于使用大块的空闲内存。在其中查找特定大小的内存块是一件很耗时的工作,尤其是当内存中充满碎片的时候。与此不同,在托管堆中,内存被组制成连续的数组,指针总是巡着已经被使用的内存和未被使用的内存之间的边界移动。当内存被分配的时候,指针只是简单地递增——由此而来的一个好处是,分配操作的效率得到了很大的提升。
  当对象被分配的时候,它们一开始被放在generation 0中。当generation 0的大小快要达到它的上限的时候,一个只在generation 0中执行的回收操作被触发。由于generation 0的大小很小,因此这将是一个非常快的GC过程。这个GC过程的结果是将generation 0彻底的刷新了一遍。不再使用的对象被释放,确实正被使用的对象被整理并移入generation 1中。
  当generation 1的大小随着从generation 0中移入的对象数量的增加而接近它的上限的时候,一个回收动作被触发来在generation 0和generation 1中执行GC过程。如同在generation 0中一样,不再使用的对象被释放,正在被使用的对象被整理并移入下一个generation中。大部分GC过程的主要目标是generation 0,因为在generation 0中最有可能存在大量的已不再使用的临时对象。对generation 2的回收过程具有很高的开销,并且此过程只有在generation 0和generation 1的GC过程不能释放足够的内存时才会被触发。如果对generation 2的GC过程仍然不能释放足够的内存,那么系统就会抛出OutOfMemoryException异常
  带有终止操作的对象的垃圾收集过程要稍微复杂一些。当一个带有终止操作的对象被标记为垃圾时,它并不会被立即释放。相反,它会被放置在一个终止队列(finalization queue)中,此队列为这个对象建立一个引用,来避免这个对象被回收。后台线程为队列中的每个对象执行它们各自的终止操作,并且将已经执行过终止操作的对象从终止队列中删除。只有那些已经执行过终止操作的对象才会在下一次垃圾回收过程中被从内存中删除。这样做的一个后果是,等待被终止的对象有可能在它被清除之前,被移入更高一级的generation中,从而增加它被清除的延迟时间。
  需要执行终止操作的对象应当实现IDisposable接口,以便客户程序通过此接口快速执行终止动作。IDisposable接口包含一个方法——Dispose。这个被Beta2引入的接口,采用一种在Beta2之前就已经被广泛使用的模式实现。从本质上讲,一个需要终止操作的对象暴露出Dispose方法。这个方法被用来释放外部资源并抑制终止操作,就象下面这个程序片断所演示的那样:
  public class OverdueBookLocator: IDisposable
  {
   ~OverdueBookLocator()
   {
   InternalDispose(false);
   }
   public void Dispose()
   {
   InternalDispose(true);
   }
   protected void InternalDispose(bool disposing)
   {
   if(disposing)
   {
   GC.SuppressFinalize(this);
   // Dispose of managed objects if disposing.
   }
   // free external resources here
   }
  }

......

[阅读全文]

线程

目录


 

基础篇

WinForm多线程编程篇

线程池

同步

什么时候需要锁定

Web和IIS

 

基础篇

 

 怎样创建一个线程

我只简单列举几种常用的方法,详细可参考.Net多线程总结(一)

一)使用Thread类

ThreadStart threadStart=new ThreadStart(Calculate);//通过ThreadStart委托告诉子线程讲执行什么方法,这里执行一个计算圆周长的方法
Thread thread=new Thread(threadStart);
thread.Start(); 
//
启动新线程

public void Calculate(){
double Diameter=0.5;
Console.Write(
"The perimeter Of Circle with a Diameter of {0} is {1}"Diameter,Diameter*Math.PI);
}

 

二)使用Delegate.BeginInvoke

delegate double CalculateMethod(double Diameter); //申明一个委托,表明需要在子线程上执行的方法的函数签名
static CalculateMethod calcMethod = new CalculateMethod(Calculate);//把委托和具体的方法关联起来
static void Main(string[] args)
{
//此处开始异步执行,并且可以给出一个回调函数(如果不需要执行什么后续操作也可以不使用回调)
calcMethod.BeginInvoke(5, new AsyncCallback(TaskFinished), null);
Console.ReadLine();
}

//线程调用的函数,给出直径作为参数,计算周长
public static double Calculate(double Diameter)
{
    
return Diameter * Math.PI;
}

//线程完成之后回调的函数
public static void TaskFinished(IAsyncResult result)
{
    
double re = 0;
    re = calcMethod.EndInvoke(result);
    Console.WriteLine(re);
}


三)使用ThreadPool.QueueworkItem

WaitCallback w = new WaitCallback(Calculate);
//
下面启动四个线程,计算四个直径下的圆周长
ThreadPool.QueueUserWorkItem(w, 1.0);
ThreadPool.QueueUserWorkItem(w, 
2.0);
ThreadPool.QueueUserWorkItem(w, 
3.0);
ThreadPool.QueueUserWorkItem(w, 
4.0);
public static void Calculate(double Diameter)
{
return Diameter * Math.PI;
}

 
下面两条来自于http://www.cnblogs.com/tonyman/archive/2007/09/13/891912.html

  受托管的线程与 Windows线程

必 须要了解,执行.NET应用的线程实际上仍然是Windows线程。但是,当某个线程被CLR所知时,我们将它称为受托管的线程。具体来说,由受托管的代 码创建出来的线程就是受托管的线程。如果一个线程由非托管的代码所创建,那么它就是非托管的线程。不过,一旦该线程执行了受托管的代码它就变成了受托管的 线程。

一个受托管的线程和非托管的线程的区别在于,CLR将创建一个System.Threading.Thread类的实例来代表并操作前者。在内部实现中,CLR将一个包含了所有受托管线程的列表保存在一个叫做ThreadStore地方。

CLR确保每一个受托管的线程在任意时刻都在一个AppDomain中执行,但是这并不代表一个线程将永远处在一个AppDomain中,它可以随着时间的推移转到其他的AppDomain中。

从安全的角度来看,一个受托管的线程的主用户与底层的非托管线程中的Windows主用户是无关的。
 

  前台线程与后台线程

启 动了多个线程的程序在关闭的时候却出现了问题,如果程序退出的时候不关闭线程,那么线程就会一直的存在,但是大多启动的线程都是局部变量,不能一一的关 闭,如果调用Thread.CurrentThread.Abort()方法关闭主线程的话,就会出现ThreadAbortException 异常,因此这样不行。
后来找到了这个办法: Thread.IsBackground 设置线程为后台线程。
 
msdn对前台线 程和后台线程的解释:托管线程或者是后台线程,或者是前台线程。后台线程不会使托管执行环境处于活动状态,除此之外,后台线程与前台线程是一样的。一旦所 有前台线程在托管进程(其中 .exe 文件是托管程序集)中被停止,系统将停止所有后台线程并关闭。通过设置 Thread.IsBackground 属性,可以将一个线程指定为后台线程或前台线程。例如,通过将 Thread.IsBackground 设置为 true,就可以将线程指定为后台线程。同样,通过将 IsBackground 设置为 false,就可以将线程指定为前台线程。从非托管代码进入托管执行环境的所有线程都被标记为后台线程。通过创建并启动新的 Thread 对象而生成的所有线程都是前台线程。如果要创建希望用来侦听某些活动(如套接字连接)的前台线程,则应将 Thread.IsBackground 设置为 true,以便进程可以终止。
所以解决办法就是在主线程初始化的时候,设置:Thread.CurrentThread.IsBackground = true;

这样,主线程就是后台线程,在关闭主程序的时候就会关闭主线程,从而关闭所有线程。但是这样的话,就会强制关闭所有正在执行的线程,所以在关闭的时候要对线程工作的结果保存。


经常看到名为BeginXXX和EndXXX的方法,他们是做什么用的

这是.net的一个异步方法名称规范
.Net
在设计的时候为异步编程设计了一个异步编程模型(APM),这个模型不仅是使用.NET的开发人员使用,.Net内部也频繁用到,比如所有的Stream 就有BeginRead,EndRead,Socket,WebRequet,SqlCommand都运用到了这个模式,一般来讲,调用 BegionXXX的时候,一般会启动一个异步过程去执行一个操作,EndEnvoke可以接收这个异步操作的返回,当然如果异步操作在 EndEnvoke调用的时候还没有执行完成,EndInvoke会一直等待异步操作完成或者超时

.Net的异步编程模型(APM)一般包含BeginXXX,EndXXX,IAsyncResult这三个元素,BeginXXX方法都要返回一个IAsyncResult,而EndXXX都需要接收一个IAsyncResult作为参数,他们的函数签名模式如下

IAsyncResult BeginXXX(...);

<返回类型> EndXXX(IAsyncResult ar);

BeginXXX 和EndXXX中的XXX,一般都对应一个同步的方法,比如FileStream的Read方法是一个同步方法,相应的BeginRead(), EndRead()就是他的异步版本,HttpRequest有GetResponse来同步接收一个响应,也提供了BeginGetResponse和 EndGetResponse这个异步版本,而IAsynResult是二者联系的纽带,只有把BeginXXX所返回的IAsyncResult传给对 应的EndXXX,EndXXX才知道需要去接收哪个BeginXXX发起的异步操作的返回值。

这个模式在实际使用时稍显繁琐,虽然原则上 我们可以随时调用EndInvoke来获得返回值,并且可以同步多个线程,但是大多数情况下当我们不需要同步很多线程的时候使用回调是更好的选择,在这种 情况下三个元素中的IAsynResult就显得多余,我们一不需要用其中的线程完结标志来判断线程是否成功完成(回调的时候线程应该已经完成了),二不 需要他来传递数据,因为数据可以写在任何变量里,并且回调时应该已经填充,所以可以看到微软在新的.Net Framework中已经加强了对回调事件的支持,这总模型下,典型的回调程序应该这样写

a.DoWork+=new SomeEventHandler(Caculate);
a.CallBack+=new SomeEventHandler(callback);
a.Run();

(注: 我上面讲的是普遍的用法,然而BeginXXX,EndXXX仅仅是一种模式,而对这个模式的实现完全取决于使用他的开发人员,具体实现的时候你可以使用 另外一个线程来实现异步,也可能使用硬件的支持来实现异步,甚至可能根本和异步没有关系(尽管几乎没有人会这样做)-----比如直接在Beginxxx 里直接输出一个"Helloworld",如果是这种极端的情况,那么上面说的一切都是废话,所以上面的探讨并不涉及内部实现,只是告诉大家微软的模式, 和框架中对这个模式的经典实现)



异步和多线程有什么关联

有一句话总结的很好:多线程是实现异步的一种手段和工具

我们通常把多线程和异步等同起来,实际是一种误解,在实际实现的时候,异步有许多种实现方法,我们可以用进程来做异步,或者使用纤程,或者硬件的一些特性,比如在实现异步IO的时候,可以有下面两个方案:

1)可以通过初始化一个子线程,然后在子线程里进行IO,而让主线程顺利往下执行,当子线程执行完毕就回调

2)也可以根本不使用新线程,而使用硬件的支持(现在许多硬件都有自己的处理器),来实现完全的异步,这是我们只需要将IO请求告知硬件驱动程序,然后迅速返回,然后等着硬件IO就绪通知我们就可以了

实 际上DotNet Framework里面就有这样的例子,当我们使用文件流的时候,如果制定文件流属性为同步,则使用BeginRead进行读取时,就是用一个子线程来调 用同步的Read方法,而如果指定其为异步,则同样操作时就使用了需要硬件和操作系统支持的所谓IOCP的机制



WinForm多线程编程篇

 

 

我的多线程WinForm程序老是抛出InvalidOperationException ,怎么解决?

在WinForm中使用多线程时,常常遇到一个问题,当在子线程(非UI线程)中修改一个控件的值:比如修改进度条进度,时会抛出如下错误

Cross-thread operation not valid: Control 'XXX' accessed from a thread other than the thread it was created on.

在VS2005 或者更高版本中,只要不是在控件的创建线程(一般就是指UI主线程)上访问控件的属性就会抛出这个错误,解决方法就是利用控件提供的Invoke和 BeginInvoke把调用封送回UI线程,也就是让控件属性修改在UI线程上执行,下面列出会报错的代码和他的修改版本


ThreadStart threadStart=new ThreadStart(Calculate);//通过ThreadStart委托告诉子线程讲执行什么方法
Thread thread=new Thread(threadStart);
thread.Start();
public void Calculate(){
    double Diameter=0.5;
    double result=Diameter*Math.PI;
    CalcFinished(result);
//
计算完成需要在一个文本框里显示
}
public void CalcFinished(double result){
   
this.TextBox1.Text=result.ToString();//会抛出错误
}

上面加粗的地方在debug的时候会报错,最直接的修改方法是修改Calculate这个方法如下

delegate void changeText(double result);

public void Calculate(){
    double Diameter=0.5;
    double result=Diameter*Math.PI;
   
this.BeginInvoke(new changeText(CalcFinished),t.Result);//计算完成需要在一个文本框里显示
}

这样就ok了,但是最漂亮的方法是不去修改Calculate,而去修改CalcFinished这个方法,因为程序里调用这个方法的地方可能很多,由于加了是否需要封送的判断,这样修改还能提高非跨线程调用时的性能


delegate void changeText(double result);

public void CalcFinished(double result){
    if(this.InvokeRequired){
        this.BeginInvoke(new changeText(CalcFinished),t.Result);
    }
    else{
        this.TextBox1.Text=result.ToString();
    }
}

上面的做法用到了Control的一个属性InvokeRequired(这个属性是可以在其他线程里访问的),这个属性表明调用是否来自另非UI线程,如果是,则使用BeginInvoke来调用这个函数,否则就直接调用,省去线程封送的过程



Invoke,BeginInvoke干什么用的,内部是怎么实现的?

这两个方法主要是让给出的方法在控件创建的线程上执行

Invoke使用了Win32API的SendMessage,

UnsafeNativeMethods.PostMessage(new HandleRef(this, this.Handle), threadCallbackMessage, IntPtr.Zero, IntPtr.Zero);

BeginInvoke使用了Win32API的PostMessage

UnsafeNativeMethods.PostMessage(new HandleRef(this, this.Handle), threadCallbackMessage, IntPtr.Zero, IntPtr.Zero);

这两个方法向UI线程的消息队列中放入一个消息,当UI线程处理这个消息时,就会在自己的上下文中执行传入的方法,换句话说凡是使用BeginInvoke和Invoke调用的线程都是在UI主线程中执行的,所以如果这些方法里涉及一些静态变量,不用考虑加锁的问题

每个线程都有消息队列吗?

不是,只有创建了窗体对象的线程才会有消息队列(下面给出<Windows 核心编程>关于这一段的描述)

当 一个线程第一次被建立时,系统假定线程不会被用于任何与用户相关的任务。这样可以减少线程对系统资源的要求。但是,一旦这个线程调用一个与图形用户界面有 关的函数(例如检查它的消息队列或建立一个窗口),系统就会为该线程分配一些另外的资源,以便它能够执行与用户界面有关的任务。特别是,系统分配一个T H R E A D I N F O结构,并将这个数据结构与线程联系起来。

这 个T H R E A D I N F O结构包含一组成员变量,利用这组成员,线程可以认为它是在自己独占的环境中运行。T H R E A D I N F O是一个内部的、未公开的数据结构,用来指定线程的登记消息队列(posted-message queue)、发送消息队列( send-message queue)、应答消息队列( r e p l y -message queue)、虚拟输入队列(virtualized-input queue)、唤醒标志(wake flag)、以及用来描述线程局部输入状态的若干变量。图2 6 - 1描述了T H R E A D I N F O结构和与之相联系的三个线程。


为什么Winform不允许跨线程修改UI线程控件的值

在vs2003下,使用子线程调用ui线程创建的控件的属性是不会有问题的,但是编译的时候会出现警告,但是vs2005及以上版本就会有这样的问题,下面是msdn上的描述

"当您在 Visual Studio 调试器中运行代码时,如果您从一个线程访问某个 UI 元素,而该线程不是创建该 UI 元素时所在的线程,则会引发 InvalidOperationException。调试器引发该异常以警告您存在危险的编程操作。UI 元素不是线程安全的,所以只应在创建它们的线程上进行访问"

从 上面可以看出,这个异常实际是debugger耍的花招,也就是说,如果你直接运行程序的exe文件,或者利用运行而不调试(Ctrl+F5)来运行你的 程序,是不会抛出这样的异常的.大概ms发现v2003的警告对广大开发者不起作用,所以用了一个比较狠一点的方法.

不过问题依然存在:既然这样设计的原因主要是因为控件的值非线程安全,那么DotNet framework中非线程安全的类千千万万,为什么偏偏跨线程修改Control的属性会有这样严格的限制策略呢?

这个问题我还回答不好,希望博友们能够予以补充  

有没有什么办法可以简化WinForm多线程的开发

使用backgroundworker,使用这个组建可以避免回调时的Invoke和BeginInvoke,并且提供了许多丰富的方法和事件

参见.Net多线程总结(二)-BackgroundWorker,我在这里不再赘诉

线程池 

线程池的作用是什么

作用是减小线程创建和销毁的开销

创建线程涉及用户模式和内核模式的切换,内存分配,dll通知等一系列过程,线程销毁的步骤也是开销很大的,所以如果应用程序使用了完一个线程,我们能把线程暂时存放起来,以备下次使用,就可以减小这些开销

所有进程使用一个共享的线程池,还是每个进程使用独立的线程池?

每 个进程都有一个线程池,一个Process中只能有一个实例,它在各个应用程序域(AppDomain)是共享的,.Net2.0 中默认线程池的大小为工作线程25个,IO线程1000个,有一个比较普遍的误解是线程池中会有1000个线程等着你去取,其实不然, ThreadPool仅仅保留相当少的线程,保留的线程可以用SetMinThread这个方法来设置,当程序的某个地方需要创建一个线程来完成工作时, 而线程池中又没有空闲线程时,线程池就会负责创建这个线程,并且在调用完毕后,不会立刻销毁,而是把他放在池子里,预备下次使用,同时如果线程超过一定时 间没有被使用,线程池将会回收线程,所以线程池里存在的线程数实际是个动态的过程

为什么不要手动线程池设置最大值?

当我首次看到线程池的时候,脑袋里的第一个念头就是给他设定一个最大值,然而当我们查看ThreadPool的SetMaxThreads文档时往往会看到一条警告:不要手动更改线程池的大小,这是为什么呢?

其 实无论FileStream的异步读写,异步发送接受Web请求,甚至使用delegate的beginInvoke都会默认调用 ThreadPool,也就是说不仅你的代码可能使用到线程池,框架内部也可能使用到,更改的后果影响就非常大,特别在iis中,一个应用程序池中的所有 WebApplication会共享一个线程池,对最大值的设定会带来很多意想不到的麻烦

线程池的线程为何要分类?

线 程池有一个方法可以让我们看到线程池中可用的线程数量:GetAvaliableThread(out workerThreadCount,out iocompletedThreadCount),对于我来说,第一次看到这个函数的参数时十分困惑,因为我期望这个函数直接返回一个整形,表明还剩多少 线程,这个函数居然一次返回了两个变量.

原来线程池里的线程按照公用被分成了两大类:工作线程和IO线程,或者IO完成线程,前者用于 执行普通的操作,后者专用于异步IO,比如文件和网络请求,注意,分类并不说明两种线程本身有差别,线程就是线程,是一种执行单元,从本质上来讲都是一样 的,线程池这样分类,举例来说,就好像某施工工地现在有1000把铁锹,规定其中25把给后勤部门用,其他都给施工部门,施工部门需要大量使用铁锹来挖地 基(例子土了点,不过说明问题还是有效的),后勤部门用铁锹也就是铲铲雪,铲铲垃圾,给工人师傅修修临时住房,所以用量不大,显然两个部门的铁锹本身没有 区别,但是这样的划分就为管理两个部门的铁锹提供了方便

线程池中两种线程分别在什么情况下被使用,二者工作原理有什么不同?

下面这个例子直接说明了二者的区别,我们用一个流读出一个很大的文件(大一点操作的时间长,便于观察),然后用另一个输出流把所读出的文件的一部分写到磁盘上

我们用两种方法创建输出流,分别是

创建了一个异步的流(注意构造函数最后那个true)

FileStream outputfs=new FileStream(writepath, FileMode.Create, FileAccess.Write, FileShare.None,256,true);

创建了一个同步的流

FileStream outputfs = File.OpenWrite(writepath);

 然后在写文件期间查看线程池的状况

string readpath = "e:\\RHEL4-U4-i386-AS-disc1.iso";
string writepath = "e:\\kakakak.iso";
byte[] buffer = new byte[90000000];

//FileStream outputfs=new FileStream(writepath, FileMode.Create, FileAccess.Write, FileShare.None,256,true);
//Console.WriteLine("
异步流");
//
创建了一个同步的流

FileStream outputfs = File.OpenWrite(writepath);
Console.WriteLine(
"
同步流");

 
//
然后在写文件期间查看线程池的状况

ShowThreadDetail(
"
初始状态");

FileStream fs = File.OpenRead(readpath);

fs.BeginRead(buffer, 
0, 90000000, delegate(IAsyncResult o)
{

    outputfs.BeginWrite(buffer, 
0, buffer.Length,

    
delegate(IAsyncResult o1)
    {

        Thread.Sleep(
1000);

        ShowThreadDetail(
"BeginWrite
的回调线程");

    }, 
null);

    Thread.Sleep(
500);//this is important cause without this, this Thread and the one used for BeginRead May seem to be same one
},

null);


Console.ReadLine();


public static void ShowThreadDetail(string caller)
{
    
int IO;
    
int Worker;
    ThreadPool.GetAvailableThreads(
out Worker, out IO);
    Console.WriteLine(
"Worker: {0}; IO: {1}", Worker, IO);
}

输出结果
异步流
Worker: 500; IO: 1000
Worker: 500; IO: 999
同步流
Worker: 500; IO: 1000
Worker: 499; IO: 1000
 

这两个构造函数创建的流都可以使用BeginWrite来异步写数据,但是二者行为不同,当使用同步的流进行异步写时,通过回调的输出我们可以看到,他使用的是工作线程,而非IO线程,而异步流使用了IO线程而非工作线程

其 实当没有制定异步属性的时候,.Net实现异步IO是用一个子线程调用fs的同步Write方法来实现的,这时这个子线程会一直阻塞直到调用完成.这个子 线程其实就是线程池的一个工作线程,所以我们可以看到,同步流的异步写回调中输出的工作线程数少了一,而使用异步流,在进行异步写时,采用了 IOCP方法,简单说来,就是当BeginWrite执行时,把信息传给硬件驱动程序,然后立即往下执行(注意这里没有额外的线程),而当硬件准备就绪, 就会通知线程池,使用一个IO线程来读取

.Net线程池有什么不足

没有提供方法控制加入线程池的线程:一旦加入线程池,我们没有办法挂起,终止这些线程,唯一可以做的就是等他自己执行

1)不能为线程设置优先级
2)
一个Process中只能有一个实例,它在各个AppDomain是共享的。ThreadPool只提供了静态方法,不仅我们自己添加进去的WorkItem使用这个Pool,而且.net framework中那些BeginXXX、EndXXX之类的方法都会使用此Pool。
3)
所支持的Callback不能有返回值。WaitCallback只能带一个object类型的参数,没有任何返回值。
4)
不适合用在长期执行某任务的场合。我们常常需要做一个Service来提供不间断的服务(除非服务器down掉),但是使用ThreadPool并不合适。

下面是另外一个网友总结的什么不需要使用线程池,我觉得挺好,引用下来
如果您需要使一个任务具有特定的优先级。
如果您具有可能会长时间运行(并因此阻塞其他任务)的任务。
如果您需要将线程放置到单线程单元中(所有 ThreadPool 线程均处于多线程单元中)。
如果您需要与该线程关联的稳定标识。例如,您应使用一个专用线程来中止该线程、将其挂起或按名称发现它。

锁定与同步

CLR怎样实现lock(obj)锁定?

从原理上讲,lock和Syncronized Attribute都是用Moniter.Enter实现的,比如如下代码

object lockobj=new object();
lock(obj){  
//do things 
}

 在编译时,会被编译为类似

try{
  Moniter.Enter(obj){
   
//do things
  }
}
catch{}
finally{
  Moniter.Exit(obj);
}


而[MethodImpl(MethodImplOptions.Synchronized)]标记为同步的方法会在编译时被lock(this)语句所环绕
所以我们只简单探讨Moniter.Enter的实现

(注:DotNet并非使用Win32API的CriticalSection来实现Moniter.Enter,不过他为托管对象提供了一个类似的结构叫做Syncblk)

每 个对象实例头部都有一个指针,这个指针指向的结构,包含了对象的锁定信息,当第一次使用Moniter.Enter(obj)时,这个obj对象的锁定结 构就会被初时化,第二次调用Moniter.Enter时,会检验这个object的锁定结构,如果锁没有被释放,则调用会阻塞

  WaitHandle是什么,他和他的派生类怎么使用

  WaitHandle是Mutex,Semaphore,EventWaitHandler,AutoResetEvent,ManualResetEvent共同的祖先,他们包装了用于同步的内核对象,也就是说是这些内核对象的托管版本。

  Mutex:类似于一个接力棒,拿到接力棒的线程才可以开始跑,当然接力棒一次只属于一个线程(Thread Affinity),如果这个线程不释放接力棒(Mutex.ReleaseMutex),那么没办法,其他所有需要接力棒运行的线程都知道能等着看热闹

Semaphore: 类似于一个小桶,里面装了几个小球,凡是拿到小球就可以跑,比如指定小桶里最初有四个小球,那么开始的四个线程就可以直接拿着自己的小球开跑,但是第五个 线程一看,小球被拿光了,就只好乖乖的等着有谁放一个小球到小桶里(Semophore.Release),他才能跑,但是这里的游戏规则比较特殊,我们 可以随意向小桶里放入小球,也就是说我可以拿走一个小球,放回去俩,甚至一个都不拿,放回去5个,这样就有五个线程可以拿着这些小球运行了.我们可以规定 小桶里有开始有几个小球(构造函数的第一个参数),也可以规定最多不能超过多少小球(构造函数的第二个参数)  

什么是用双锁实现Singleton,为什么要这样做,双锁检验是不安全的吗?

使用双锁检验技巧来实现单件,来自于Java社区

public static MySingleton Instance{
get{
    if(_instance!=null)}{
        lock(_instance){
            if(s_value==null){
                _instance= new MySingleton();
            }
        }
    }
}
}  

这样做其实是为了提高效率,比起

public static MySingleton Instance{

get{

lock(_instance){

if(s_value==null){

_instance= new MySingleton();

}

}

前一种方法在instance创建的时候不需要用lock同步,从而增进了效率

在java中这种技巧被证明是不安全的详细见http://www.cs.umd.edu/~pugh/java/memoryModel/

但是在.Net下,这样的技巧是成立的,因为.Net使用了改进的内存模型

并且在.Net下,我们可以使用LazyInit来实现单件

private static readonly _instance=new MySingleton()

public static MySingleton Instance{

get{return _instance}

}

当第一此使用_instance时,CLR会生成这个对象,以后再访问这个字段,将会直接返回

互斥对象(Mutex),信号量(Semaphore),事件(Event)对象与lock语句的比较

首先这里所谓的事件对象不是System.Event,而是一种用于同步的内核机制

互斥对象和事件对象属于内核对象,利用内核对象进行线程同步,线程必须要在用户模式和内核模式间切换,所以一般效率很低,但利用互斥对象和事件对象这样的内核对象,可以在多个进程中的各个线程间进行同步。

lock或者Moniter是.net用一个特殊结构实现的,不涉及模式切换,也就是说工作在用户方式下,同步速度较快,但是不能跨进程同步

什么时候需要锁定?

刚刚接触锁定的程序员往往觉得这个世界非常的危险,每个静态变量似乎都有可能产生竞争

首先锁定是解决竞争条件的,也就是多个线程同时访问某个资源,造成意想不到的结果,比如,最简单的情况,一个计数器,如果两个线程同时加一,后果就是损失了一个计数,但是频繁的锁定又可能带来性能上的消耗,还有最可怕的情况,死锁

到底什么情况下我们需要使用锁,什么情况下不用呢?

只有共享资源才需要锁定
首先,只有可以被多线程访问的共享资源才需要考虑锁定,比如静态变量,再比如某些缓存中的值,属于线程内部的变量不需要锁定

把锁定交给数据库
数据库除了存储数据之外,还有一个重要的用途就是同步,数据库本身用了一套复杂的机制来保证数据的可靠和一致性,这就为我们节省了很多的精力.保证了数据源头上的同步,我们多数的精力就可以集中在缓存等其他一些资源的同步访问上了

了解你的程序是怎么运行的
实 际上在web开发中大多数逻辑都是在单个线程中展开的,无论asp.net还是php,一个请求都会在一个单独的线程中处理,其中的大部分变量都是属于这 个线程的,根本没有必要考虑锁定,当然对于asp.net中的application对象中的数据,我们就要小心一些了

WinForm中凡是使用BeginInvoke和Invoke调用的方法也都不需要考虑同步,因为这用这两个方法调用的方法会在UI线程中执行,因此实际是同步的,所以如果调用的方法中存在某些静态变量,不需要考虑锁定

业务逻辑对事务和线程安全的要求
这 条是最根本的东西,开发完全线程安全的程序是件很费时费力的事情,在电子商务等涉及金融系统的案例中,许多逻辑都必须严格的线程安全,所以我们不得不牺牲 一些性能,和很多的开发时间来做这方面的工作,而一般的应用中,许多情况下虽然程序有竞争的危险,我们还是可以不使用锁定,比如有的时候计数器少一多一, 对结果无伤大雅的情况下,我们就可以不用去管他

计算一下冲突的可能性
我 以前曾经谈到过,架构不要过设计,其实在这里也一样,假如你的全局缓存里的某个值每天只有几百或者几千个访问,并且访问时间很短,并且分布均匀(实际上这 是大多数的情况),那么冲突的可能性就非常的少,也许每500天才会出现一次或者更长,从7*24小时安全服务的角度来看,也完全符合要求,那么你还会为 这样万分之一的可能性花80%的精力去设计吗?

请多使用lock,少用Mutex
如 果你一定要使用锁定,请尽量不要使用内核模块的锁定机制,比如.net的Mutex,Semaphore,AutoResetEvent, ManuResetEvent,使用这样的机制涉及到了系统在用户模式和内核模式间的切换,所以性能差很多,但是他们的优点是可以跨进程同步线程,所以应 该清楚的了解到他们的不同和适用范围

Web和IIS

应用程序池,WebApplication,和线程池之间有什么关系

一个应用程序池是一个独立的进程,拥有一个线程池,应用程序池中可以有多个WebApplication,每个运行在一个单独的AppDomain中,这些WebApplication公用一个线程池

不同的AppDomain保证了每个WebApplication的静态变量不会互相干扰,不同的应用程序池保证了一个网站瘫痪,其他不同进程中的站点还能正常运行

 下图说明了他们的关系

 

Web页面怎么调用异步WebService

把Page的Async属性设置为true,就可以调用异步的方法,但是这样调用的效果可能并不如我们的相像,请参考Web中使用多线程来增强用户体验

推荐文章 

http://alang79.blogdriver.com/alang79/456761.html

A low-level Look at the ASP.NET Architecture

参考资料

<Windows 核心编程>这本书里对内核对象的描述比较详尽



......

[阅读全文]