【翻译】深入浅出.NET线程系列之一:.NET线程介绍

原文标题:Beginners Guide To Threading In .NET Part 1 of N --Introduction into threading in .NET

原文作者:Sacha Barber

原文链接:http://www.codeproject.com/KB/threads/ThreadingDotNet.aspx

License:      The Code Project Open License (CPOL)

 

本文所包含如下内容:

1、什么是进程

2、什么是应用程序域

3、线程本地存储(Thread Local Storage)

4、启动线程

5、线程优先级

6、线程回调函数(CallBack)

 

1、什么是进程

当用户启动一个应用程序,操作系统将分配一块内存空间和一些资源给该应用程序。内存空间和资源在物理上的隔绝被称为一个进程。一个应用程序可

会启动一个或一个以上的进程,但应用程序和进程是两个完全不同的概念!

在Windows系统中,可以通过任务管理器来查看系统当前运行的应用程序和进程

应用程序如图所示:

Apps

下图是系统当前运行的进程的列表,可以看出当前有许多进程正在运行。一个应用程序可能和一个或者多个进程关联,但是每个进程都有自己独立的数据、执行代码和系统资源。如图所示:

processes

你可能会发现上图有一个CPU占用数,这是因为每个进程有一个执行序列供CPU使用,这个执行序列就是线程,线程由CPU中的寄存器、线程使用的栈和一个追踪线程状态的容器(线程本地存储)定义。

创建一个进程包括在一个指令点启动进程,这个指令点通常被称为主线程,这个线程的执行序列在很大程度上取决于用户的代码。

时间片

所有的线程都需要使用CPU的时间片,那么是怎么管理进程使用时间片的呢?每个进程都被授予一个使用CPU的时间片段(量子级别的时间),这个时间片从来就不是固定的,它受到操作系统和CPU的影响。

多线程进程

当我们的进程需要做超过一件事的时候会发生什么?比如同时查询一个web service和写数据到数据库。幸运的是我们能够将进程进行分割以便共享操作系统分配进程的时间片,这通过在进程中产生新的线程来实现,这些额外的线程有时候被叫做工作线程(worker thread),这些工作线程能够共享进程独立于系统中其他进程的内存空间。在一个进程中产生新的线程的概念就叫做自由线程(Free Threading)。

在VB6中有单元线程的概念(Apartment Threading),每个线程在它所属的单元中都有自己独立的数据,所以线程与线程之间不能共享数据。

下面两张图能显示了套件线程和自由线程的区别

      单元线程模型

apart

    自由线程模型

freeThreading

自由线程模型运行CPU执行一个额外的线程,但是该线程能够使用进程的数据,这种模型优于单元线程模型,同时还能够从线程共享进程的数据的这个特性中获得额外的益处。

注意:在同一时间内仅有一个线程在CPU上运行

如果我们在任务管理中添加查看线程数这个选项,如下图所示:

tmThreads

上图清晰的表明了一个进程拥有一个以上的线程,那么如果管理这些线程的任务信息和状态信息呢?我们将在下面讨论到。

 

3、线程本地存储(Thread Local Storage)

当一个线程的时间片完成时,它并不停止然后等待重新执行。记住,在同一个时间内CPU只能运行一个线程!所以,当前的线程将被占用CPU时间片的下一个线程代替,在这之前,当前线程的状态信息需要保存起来,以便下次占有CPU时间片时能够正确运行,这就是线程本地存储的作用。其中一个保存在线程本地存储中的寄存器叫做程序计数器,它告诉线程需要执行的下一个指令。

       中断(Interrupts)

进程不需要知道其他进程的执行计划,这是操作系统的工作,甚至操作系统有一个被叫做系统线程的主线程来对所有的线程安排执行计划,它通过中断来实现这个功能。中断是一个机制,它在执行程序不知情的情况下,把正常的执行流转移到内存的另外一块空间。

操作系统决定了线程的执行时间,同时在当前线程的执行序列中放入一个指令。所以通过指令集的中断是软件中断,不是硬件中断。

中断在几乎是所有操作系统都具有的一个特性,但是简单的微处理器允许硬件设备来完成这一过程,当一个中断发生的时候,微处理器会将代码的执行过程临时挂起,并且跳转到一个被称为中断处理程序的特殊的程序。中断处理程序会维持设备需要的状态,然后返回之前执行的代码

在所有的现代操作系统中都有的中断,是由一个计时器来控制的,它的功能就是以一个阶段性的间隔查询任务状态,它的中断处理程序会去读取程序计数器,然后看看它感兴趣的内容是否发生,如果没有则返回。在操作系统的控制下,其中一个“感兴趣”的事会在线程占用的时间片完成后发生,但这种情况发生后,Windows会强迫当前的执行恢复另一个之前被中断的线程。

如果一个中断产生,操作系统还是运行线程继续执行,当线程接收到了中断,操作系统使用一个特别的程序-中断处理程序把线程的状态保存在线程本地存储(TLS),当线程的时间片到期后,根据线程的优先级,它被移动到线程队列的合适的位置,然后等待重新执行。如下图:

sched

当一个线程决定马上需要更多的CPU时间(或者它要等待一个资源),这就导致了中断的发生,并捕获了它的时间片给另外一个线程。 这由程序员或者操作系统来决定,程序员可以捕获线程(通常是通过Sleep方法)并且清除操作系统设置的中断。一个软件中断的激发,将导致线程被存储在TLS中,并且线程被移动到线程队列的末尾。

       线程休眠和时钟中断

我们曾经说过一个线程会使用CPU时间来等待一个资源,但是这可能会是10分钟或者20分钟,所以程序员可以选择让线程进行休眠,这会让线程存储在TLS中,但是线程不是在TLS的运行队列中,而是在休眠队列中。为了让休眠队列中的线程重新运行,我们需要另外一种中断-时钟中断

示意图

clockInterrupt

      线程中止/线程完成

所有的事情都有结束的时候。当一个线程完成了或者在程序中被中断,该线程在TLS中的存储空间被收回,进程中的数据将继续保存,直到进程停止。

TLS是如何来存储线程状态信息的,MSDN里面有如下描述:

"Threads use a local store memory mechanism to store thread-specific data. The common language runtime allocates a multi-slot data store array to each process when it is created. The thread can allocate a data slot in the data store, store and retrieve a data value in the slot, and free the slot for reuse after the thread expires. Data slots are unique per thread. No other thread (not even a child thread) can get that data.

If the named slot does not exist, a new slot is allocated. Named data slots are public and can be manipulated by anyone."

下面我们来看一个MSDN上的例子

 

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
 
namespace Threading
{
    class Program
    {
        static void Main(string[] args)
        {
            Thread[] myThreads = new Thread[4];
            for (int i = 0; i < myThreads.Length; i++)
            {
                myThreads[i] = new Thread(new ThreadStart(Slot.SlotTest));
                myThreads[i].Start();
            }
        }
    }
    class Slot
    {
        static Random randomGenerator = new Random();
        public static void SlotTest()
        {
            //给每个线程设置不同的数据保存在线程数据槽中
            Thread.SetData(Thread.GetNamedDataSlot("Random"), randomGenerator.Next(1, 200));
 
            //从每个线程的有名数据槽中读取数据
            Console.WriteLine("Data in thread_{0}'s data slot: {1,3}",AppDomain.GetCurrentThreadId().ToString(),
                                    Thread.GetData(
                                        Thread.GetNamedDataSlot("Random")));
            //休眠1秒,允许其他线程往数据槽中写数据,以表示数据槽是唯一的
            Thread.Sleep(1000);
 
            Console.WriteLine("Data in thread_{0}'s data slot is still: {1,3}",
                AppDomain.GetCurrentThreadId().ToString(),
                Thread.GetData(
                Thread.GetNamedDataSlot("Random")).ToString());
            //休眠一秒,运行其他线程能够读取数据槽中的数据,这表明线程中的任何代码都
            //能够访问有名数据槽
            Thread.Sleep(1000);
 
            Ohter o = new Ohter();
            o.ShowSlotData();
            Console.ReadKey();
        }
    }
    public class Ohter
    {
        public void ShowSlotData()
        {
            //这个方法无法访问有名数据槽,但是当它被一个线程执行的时候
            //它可以访问线程在有名数据槽的数据
            Console.WriteLine(
               "Other code displays data in thread_{0}'s data slot: {1,3}",
               AppDomain.GetCurrentThreadId().ToString(),
               Thread.GetData(
               Thread.GetNamedDataSlot("Random")).ToString());
 
        }
    }
}
输出如下:
tSlot

两个方法可以注意一下:

  • GetNamedDataSlot:查找一个有名数据槽
  • SetData: 在当前线程中存储数据到一个特定的数据槽中

我们也可以使用ThreadStaticAttribute为每一个线程保存一个特定的值,MSDN上的例子如下:

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
 
namespace Threading
{
    class Program
    {
        static void Main(string[] args)
        {
            Thread[] myThreads = new Thread[3];
            for (int i = 0; i < myThreads.Length; i++)
            {
                myThreads[i] = new Thread(new ThreadStart(ThreadData.ThreadStaticDemo));
                myThreads[i].Start();
            }
        }
    }
    class ThreadData
    {
        [ThreadStatic]
        static int threadSpecificData;
        public static void ThreadStaticDemo()
        {
            threadSpecificData = Thread.CurrentThread.ManagedThreadId;
            Thread.Sleep(1000);
 
            // Display the static data.
            Console.WriteLine("Data for managed thread {0}: {1}",
                Thread.CurrentThread.ManagedThreadId, threadSpecificData);
        }
    }

2.什么是应用程序域(AppDomain)

前面我们谈到进程,进程需要物理上独立的内存空间和资源来维持自身,同时也提到一个进程拥有至少一个线程。微软引进了一个额外的抽象隔离层-APPDOMAIN(应用程序域),应用程序域并不是物理上的隔绝,而是在进程内部的逻辑上的隔离。我们可以从一个进程内部拥有多个应用程序域而得益,例如在没有应用程序域之前,进程之间必须通过代理来互相访问,需要额外的代码和开销。通过使用应用程序域我们可以在一个进程中启动多个应用程序(Applications)。进程中的同类独立资源也能够为应用程序域使用,线程可以跨越应用程序域而不需要跨进程通信的花销。这些所有都封装在应用程序域(AppDomain class)这个类的内部。

        在任何时候,应用程序加载的命名空间都会导入到一个应用程序域中,在默认情况下,这个应用程序域就是当前调用代码所在的应用程序域,也可以自己定义一个新的应用程序域来加载命名空间。

        应用程序域可能有线程,也可能没有线程,在这点上和进程不同

 AppDomain

 

为什么需要应用程序域?

如前所述,AppDomain是对进程内部资源的进一步的隔绝和抽象。那么为什么要使用AppDomain呢?这篇文章的一个读者为这个问题给出了一个非常好的示例。

“我之前需要在单独的AppDomain中来执行代码,调用一个Visual Studio AddIn,它通过反射来查看当前项目中的DLL文件。如果不在一个独立的AppDomain中,当前项目中的开发者所做出的任何的变动都不能通过反射来查看,除非重新启动Visual Studio。Marc指出了这个原因:只要一个AppDomain加载了一个程序集,它就不能卸载该程序集。”

AppDomain forum post, by Daniel Flowers

所以我们可以看到AppDomain能够动态的加载程序集,同时整个AppDomain都能够卸载而不影响进程。我认为这能解释AppDomain的抽象和隔离。

NUint也使用了这种方法,但是这更复杂。

 

设置AppDomain数据

我们来看示例程序是如何设置AppDomain的数据:

using System;
using System.Threading;
 
namespace AppDomainData
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Fetching current Domain");
            //use current AppDomain, and store some data
            AppDomain domain = System.AppDomain.CurrentDomain;
            Console.WriteLine("Setting AppDomain Data");
            string name = "MyData";
            string value = "Some data to store";
            domain.SetData(name, value);
            Console.WriteLine("Fetching Domain Data");
            Console.WriteLine("The data found for key {0} is {1}",
                name, domain.GetData(name));
            Console.ReadLine();
        }
    }
}
输出结果:
image

         同时看看如何在一个特AppDomain中来执行代码

     using System;
     using System.Threading;
 
     namespace PraticalNet.SysThread
     {
        [Serializable]
         public class ExcuteCodeInAppDomain
       {
        public void ExcuteInAppDomain()
        {
       AppDomain domainA = AppDomain.CreateDomain("MyDomainA");
       AppDomain domainB = AppDomain.CreateDomain("MyDomainB");
       domainA.SetData("domainKey", "设置在domainA中的值");
       domainB.SetData("domainKey", "设置在domainB中的值");
       OutputCall();
       domainA.DoCallBack(OutputCall);
       domainB.DoCallBack(OutputCall);
       Console.ReadLine();
       }
       private  void OutputCall()
       {
       AppDomain domain = AppDomain.CurrentDomain;
       Console.WriteLine("在应用程序域{0}中发现了数值{1},当前的线程ID是{2}",
   domain.FriendlyName, domain.GetData("domainKey"),Thread.CurrentThread.ManagedThreadId);
       }
     }
    }
    static void Main(string[] args)
    {
     AppDomainDemo();
    }
    static void AppDomainDemo()
    {
     ExcuteCodeInAppDomain exAppDomain = new ExcuteCodeInAppDomain();
     exAppDomain.ExcuteInAppDomain();
    }

 

    输出结果:

    clip_image002

  
NUnit和AppDomain

  “使用AppDomain来动态重新加载程序集和浅拷贝,这也适用如果你添加或者修改测试用例,程序集会重新加载并且显示会动态更新。浅拷贝我们使用一个可配置目录在执行程序的配置文件中”

    NUnit Release Notes Page

   “Nunit 是由.NET框架专家编写的。如果你查看NUnit的源代码,你就会看见他们知道怎么动态的创建AppDomain并加载程序集到这些AppDomain中。为什么动态AppDomain重要呢?动态的AppDomain使NUnit开放,允许你编译、测试、修改和重新编译,并且重新测试代码而不需要退出NUnit。你能够这样做是因为NUnit对你的程序集进行了浅拷贝,并把它们加载到一个动态的AppDomain中,同时使用一个文件监视器查看你是否改变了这些程序集。如果你确实改变了,这时候NUnit销毁动态的AppDomain,重新复制文件,并创建一个新的AppDomain,然后重复上面的步骤”

    Dr Dobbs Portal

    NUnit所做的关键在于它把测试程序集寄宿在一个单独的AppDomain中,这个AppDomain是独立的,并且能够被卸载而不影响它所属的进程。

 

4、线程优先级

 

就像真实世界中的人类有优先级一样,线程也有优先级。一个程序员可能决定线程的优先级, 但是最终却是由这个线程的接收者(操作系统)来决定哪个运行,哪个等待。

Windows使用了一个优先级系统冲0到31,31是最高优先级。任何优先级超过15的线程都需要有系统管理员的身份来运行。具有16-31之间优先级的线程被认为是实时的并且抢占低优先级线程的时间片。考虑一下类似于驱动程序和输入设备的程序,这些程序会在16-31之间的优先级中运行。

 

clip_image002[9]

 

Windows是一个计划任务系统(典型的循环),每一个优先级有一个线程队列。所有高优先级线程被分配一些CPU时间,低优先级线程同样被分配一些CPU时间。如果一个新的线程有高优先级,那么当前线程被预先抢占,然后高优先级线程开始运行。低优先级线程只有在其他高优先级队列中没有线程了,才能运行。

如果我们再次使用任务管理器,我们可以修改一个进程,让它拥有高优先级,从而使新产生的线程具有一个很高的运行几率。

clip_image002[11]

我们在代码中也可以设置线程的优先级,使用System.Threading.Thread类的Priority属性,根据MSDN的描述,我可以在线程中设置如下几种优先级:

一个线程可以设置下面的任意一个优先级:

  • Highest
  • AboveNormal
  • Normal
  • BelowNormal
  • Lowest

注意:不需要操作系统来授予线程的优先级

例如,一个操作系统可以降低一个高优先级线程的优先级,或者动态调整优先级,使操作系统中的其他线程能受到平等对待,因此一个高优先级的线程有可能被低优先级线程抢占。除此之外,许多操作系统有无限分配的潜在因素:系统中有越多的线程,一个线程的执行在操作系统的计划人亡中所花费的时间越长。其中任何一个因素都能导致一个高优先级线程失去了它的完成时间,甚至在一个非常快的CPU中。

同样的,我们在程序中设置线程高优先级也会有这个问题,所以需要谨慎的设置线程的优先级。

 

5、启动线程

启动线程相当简单,我们仅仅需要使用下面其中的一个线程构造函数:

  • Thread(ThreadStart)
  • Thread(ParameterizedThreadStart)

还有其他的方式,但是这是最普通的启动线程的方式,我们来看看各自的样列程序

没有参数

Thread workerThread = new Thread(StartThread);
Console.WriteLine("Main Thread Id {0}",
    Thread.CurrentThread.ManagedThreadId.ToString());
workerThread.Start();
 
....
....
public static void StartThread()
{
    for (int i = 0; i < 10; i++)
    {
        Console.WriteLine("Thread value {0} running on Thread Id {1}",
            i.ToString(), 
            Thread.CurrentThread.ManagedThreadId.ToString());
    }
}

一个参数

//using parameter
Thread workerThread2 = new Thread(ParameterizedStartThread);
// the answer to life the universe and everything, 42 obviously
workerThread2.Start(42); 
Console.ReadLine();
 
....
....
public static void ParameterizedStartThread(object value)
{
    Console.WriteLine("Thread passed value {0} running on Thread Id {1}",
        value.ToString(),
        Thread.CurrentThread.ManagedThreadId.ToString());
}

把他们放在一起,我们可以看见一个小的程序有两个工作进程

using System;
using System.Threading;
 
namespace StartingThreads
{
    class Program
    {
        static void Main(string[] args)
        {
            //no parameters
            Thread workerThread = new Thread(StartThread);
            Console.WriteLine("Main Thread Id {0}",
                Thread.CurrentThread.ManagedThreadId.ToString());
            workerThread.Start();
 
            //using parameter
            Thread workerThread2 = new Thread(ParameterizedStartThread);
            // the answer to life the universe and everything, 42 obviously
            workerThread2.Start(42); 
            Console.ReadLine();
        }
 
        public static void StartThread()
        {
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine("Thread value {0} running on Thread Id {1}",
                    i.ToString(), 
                    Thread.CurrentThread.ManagedThreadId.ToString());
            }
        }
 
        public static void ParameterizedStartThread(object value)
        {
            Console.WriteLine("Thread passed value {0} running on Thread Id {1}",
                value.ToString(),
                Thread.CurrentThread.ManagedThreadId.ToString());
        }
    }
}

 

输出的结果可能是如下:

clip_image002[13]

 

6、回调函数

 

我们现在来看看一下创建线程的简单方法

到目前为止,我们还没有提的是线程之间同步的方式

线程必须在应用程序代码的执行顺序之外运行,所以你永远不能确定事件的确切指令。这就是说,我们不能保证一个线程使用的共享资源能够在另外一个线程使用它之前被释放

我们将会在接下来的文章中详细的讨论这个问题,但是现在让我们来看看一个使用了Timer的小程序,使用一个Timer我们看以让一个特定的方法在固定的间隔内被调用并且可以在继续之前检测一些数据状态,这是一个非常简单的模型。

我们来看这个小程序,这个程序启动一个工作线程和一个Timer。主线程被放入一个循环体中,等待完成标识符被设置成“true”。Timer一直等待工作线程发送"Completed"消息,接到消息后通过设置完成标识符为"true"让被阻塞的主线程继续运行。

using System;
using System.Threading;
 
 
namespace CallBacks
{
    class Program
    {
        private string message;
        private static Timer timer;
        private static bool complete;
 
 
        static void Main(string[] args)
        {
            Program p = new Program();
            Thread workerThread = new Thread(p.DoSomeWork);
            workerThread.Start();
 
            //create timer with callback
            TimerCallback timerCallBack =
                new TimerCallback(p.GetState);
            timer = new Timer(timerCallBack, null, 
                TimeSpan.Zero, TimeSpan.FromSeconds(2));
 
            //wait for worker to complete
            do
            {
                //simply wait, do nothing
            } while (!complete);
 
            Console.WriteLine("exiting main thread");
            Console.ReadLine();
        }
 
 
        public void GetState(Object state)
        {
            //not done so return
            if (message == string.Empty) return;
            Console.WriteLine("Worker is {0}", message);
            //is other thread completed yet, if so signal main
            //thread to stop waiting
            if (message == "Completed")
            {
                timer.Dispose();
                complete = true;
            }
        }
 
        public void DoSomeWork()
        {
            message = "processing";
            //simulate doing some work
            Thread.Sleep(3000); 
            message = "Completed";
        }
    }
}

可能的输出结果

image

 
END
 
 
注:第一次翻译,粗糙错误之处再所难免,多请大家指正,谢谢
 
PS:Thanks Langlang Ray Especially,i will forget to continue to translate this post without his warning.
 
翻译人:Async Liu
 
posted @ 2008-08-14 17:31  Async Liu  阅读(1121)  评论(0编辑  收藏  举报