.NET面试题系列[17] - 多线程概念(2)

线程概念

线程和进程的区别

  1. 进程是应用程序的一个实例要使用的资源的一个集合。进程经过虚拟内存地址空间进行隔离,确保各个进程之间不会相互影响。同一个进程中的各个线程之间共享进程拥有的全部资源。
  2. 线程是系统调度的基本单位。时间片和线程相关,和进程无关。
  3. 一个进程至少要拥有一个前台线程。

线程开销

当咱们建立了一个线程后,线程里面主要包括线程内核对象、线程环境块、1M大小的用户模式栈和内核模式栈。程序员

  1. 线程内核对象:若是是内核模式构造的线程,则存在一个线程内核对象,包含一组对线程进行描述的属性,以及线程上下文(包含了CPU寄存器中的数据,用于上下文切换)。
  2. 线程环境块:用户模式中分配和初始化的一个内存块。
  3. 用户模式栈:对于用户模式构造的线程,应用程序能够直接和用户模式栈沟通。
  4. 内核模式栈:若是是内核模式构造的线程进行上下文切换和其余操做时,须要调用操做系统的函数。此时须要使用内核模式栈向操做系统的函数传递参数。应用程序代码没法直接访问内核模式栈,它须要借助用户模式的代码。

线程有本身的线程栈,大小为1M,因此它能够维护本身的变量。线程是一个新的对象,它会增长系统上下文切换的次数,因此过多的线程将致使系统开销很大。例如outlook会建立38个线程,但大部分时候他什么都不作。因此咱们白白浪费了38M的内存。cookie

单核CPU一次只能作一件事,因此系统必须不停的进行上下文切换,且全部的线程(逻辑CPU)之间共享物理CPU。在某一时刻,系统只将一个线程分配给一个CPU。而后,该线程能够运行一个时间片(大约30毫秒),过了这段时间,就发生上下文切换到另外一个线程。多线程

假设某个应用程序的线程进入无限循环,系统会按期抢占他(不让他再次运行)而容许新线程运行一会。若是新线程刚好是任务管理器的线程(此时将会发现任务管理器能够响应,而任务管理器以外屏幕其余地方则仍然无响应),则用户能够利用任务管理器杀死包含了其余已经冻结的线程的进程。经过这种作法,上下文切换开销并不会带来任何性能增益,但换来了好得多的用户体验(很难死机,用户能够用任务管理器杀死其余的进程)。闭包

当某个线程一直空闲(例如一个开启的记事本但长时间无输入)时,他能够提早终止属于他的时间片。线程也能够进入挂起状态,此时以后任什么时候间片,都不会分配到这个线程,除非发生了某个事件(例如用户进行了输入)。节省出来的时间可让CPU调度其余线程,加强系统性能。app

线程的状态

能够用下图表示:异步

线程的主要状态有四种:就绪(Unstarted),运行(Running),阻塞(WaitSleepJoin)和中止(Stopped),还有一种Aborted就是被杀死了。一般,强制得到线程执行任务的结果,或者经过锁等同步工具,会令线程进入阻塞状态。当获得结果以后,线程就解除阻塞,回到就绪状态。ide

当创建一个线程时,它的状态为就绪。使用Start方法令线程进入运行状态。此时线程就开始执行方法。若是没有遇到任何问题,则线程执行完方法以后,就进入中止状态。函数

阻塞(WaitSleepJoin),顾名思义,是使线程进入阻塞状态。当一个线程被阻塞以后,它马上用尽它的时间片(即便还有时间),而后CPU将永远不会调度时间片给它直到它解除阻塞为止(在将来的多少毫秒内我不参与CPU竞争)。主要方式有:Thread.Join(其余线程都运行完了以后就解除阻塞),Thread.Sleep(时间到了就解除阻塞),Task.Result(获得结果了就解除阻塞),遭遇锁而拿不到锁的控制权(等到其余线程释放锁,本身拿到锁,就解除阻塞)等。固然,自旋也是阻塞的一种。工具

Thread类中的方法对线程状态的影响

Start:使线程从就绪状态进入运行状态性能

Sleep:使线程从运行状态进入阻塞状态,持续若干时间,而后阻塞自动解除回到运行状态

Join:使线程从运行状态进入阻塞状态,当其余线程都结束时阻塞解除

Interrupt:当线程被阻塞时,即便阻塞解除的要求尚未达到,可使用Interrupt方法强行唤醒线程使线程进入运行状态。这将会引起一个异常。(例如休息10000秒的线程能够被马上唤醒)

Abort:使用Abort方法能够强行杀死一个处于任何状态的线程

时间片

当咱们讨论多任务时,咱们指出操做系统为每一个程序分配必定时间,而后中断当前运行程序并容许另一个程序执行。这并不彻底准确。处理器实际上为进程分配时间。进程能够执行的时间被称做“时间片”或者“限量”。时间片的间隔对程序员和任何非操做系统内核的程序来讲都是变化莫测的。程序员不该该在他们的程序中将时间片的值假定为一个常量。每一个操做系统和每一个处理器均可能设定一个不一样的时间。

进程和线程优先级

Windows是一个抢占式的操做系统。在抢占式操做系统中,较高优先级的进程老是抢占(preempt较低优先级的进程(即便时间片没有用完)。用户不能保证本身的线程一直运行,也不能阻止其余线程的运行。 

每个进程有一个优先级类,每个线程有一个优先级(0-31)。较高优先级的进程中的较高优先级的线程得到优先分配时间片的权利。

只要存在能够调度的高优先级的线程,系统就永远不会将低优先级的现场分配给CPU,这种状况称为饥饿。饥饿应该尽可能避免,可使用不一样的调度方式,而不是仅仅看优先级的高低。在多处理器机器上饥饿发生的可能性较小些,由于这种机器上,高优先级的线程和低优先级的线程能够同时运行。

Thread类中的Priority容许用户改变线程的优先级(但不是直接指定1-31之间的数字,而是指定几个层级,每一个层级最终mapping到数字,例如层级normal会映射到4)

前台和后台线程

一个进程能够有任意个前台和后台线程。前台线程使得整个进程得以继续下去。一个进程的全部前台线程都结束了,进程也就结束了。当该进程的全部前台线程终止时,CLR将强制终止该进程的全部后台线程,这将会致使finally可能没来得及执行(从而致使一些垃圾回收的问题)。解决的方法是使用join等待。例如你在main函数中设置了一个后台线程,而后让其运行,假设它将运行较长的时间,而此后main函数就没有代码了,那么程序将马上终止,由于main函数是后台线程。

使用thread类建立的线程默认都是前台线程。Thread的IsBackground类容许用户将一个线程置为后台线程。

多线程有什么好处和坏处?

好处:

  1. 更大限度的利用CPU和其余计算机资源。
  2. 当一条线程冻结时,其余线程仍然能够运行。
  3. 在后台执行长任务时,保持用户界面良好的响应。
  4. 并行计算(仅当这么作的好处大于对资源的损耗时)

坏处:

  1. 线程的建立和维护须要消耗计算机资源。(使用线程池,任务来抵消一部分损失)。一条线程至少须要耗费1M内存。
  2. 多个线程之间若是不一样步,结果将会难以预料。(使用锁和互斥)
  3. 线程的启动和运行时间是不肯定的,由系统进行调度,因此可能会形成资源争用,一样形成难以预料的结果。(使用锁和互斥,或者进行原子操做)

为了不2和3,须要开发者更精细的测试代码,增长了开发时间。

System.Threading类的基本使用

建立线程

可使用Thread的构造函数建立线程。咱们要传递一个方法做为构造函数的参数。一般咱们能够传递ThreadStart委托或者ParameterizedThreadStart委托。后者是一个能够传递输入参数的委托。两个委托都没有返回值。ThreadStart委托的签名是:public delegate void ThreadStart();

1 基本例子:经过Thread构造函数创建一个线程。传递的方法WriteY没有返回值,也没有输入。以后使用Start方法使线程开始执行任务WriteY。

class ThreadTest
{
  static void Main()
  {
    Thread t = new Thread (WriteY);          
    t.Start();                            
 
    for (int i = 0; i < 1000; i++) Console.Write ("x");
  }
 
  static void WriteY()
  {
    for (int i = 0; i < 1000; i++) Console.Write ("y");
  }
}
View Code

这个例子中,主线程和次线程同时访问一个静态方法(静态方法是类级别的)。此时系统调度使得主线程和次线程轮流运行(但运行的顺序是随机的)。因此结果多是

xxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyy
yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
yyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

2 主线程和次线程分别维护各自的局部变量

static void Main()
{
  new Thread (Go).Start();    
  Go();                         
}
 
static void Go()
{
  // Declare and use a local variable - 'cycles'
  for (int cycles = 0; cycles < 5; cycles++) Console.Write ('?');
}
View Code

次线程有本身的线程栈(大小1兆),因此主线程和次线程分别拥有各自的局部变量cycles。结果将是十个问号。这十个问号出自主线程和次线程,顺序不定。

3 主线程和次线程分享全局变量

class ThreadTest
{
  bool done;
 
  static void Main()
  {
    ThreadTest tt = new ThreadTest();   // Create a common instance
    new Thread (tt.Go).Start();
    tt.Go();
  }
 
  // Note that Go is now an instance method
  void Go() 
  {
     if (!done) { done = true; Console.WriteLine ("Done"); }
  }
}
View Code

变量done是全局的,被全部线程共享。此时,次线程开始任务,并在Go方法中将done设为真。最后只会打印一个done。

什么时候考虑建立一个线程?

  1. 当建立线程的代价比线程池要小(例如只打算建立一个线程时)
  2. 当但愿本身管理线程的优先级时(线程池自动管理)
  3. 须要一个前台线程(线程池建立的线程都是后台的)

向次线程传递数据

1. 使用Lambda表达式。此时仍然使用的是ThreadStart委托。

static void Main()
{
  Thread t = new Thread ( () => Print ("Hello from t!") );
  t.Start();
}
 
static void Print (string message) 
{
  Console.WriteLine (message);
}
View Code

 

2. 使用Thread的另外一个构造函数传入一个ParameterizedThreadStart委托

ParameterizedThreadStart委托的签名是:public delegate void ParameterizedThreadStart (object obj);

因此它只能传递object类型的数据而且不能有返回值。

static void Main()
{
  Thread t = new Thread (Print);
  t.Start ("Hello from t!");
}
 
static void Print (object messageObj)
{
  string message = (string) messageObj;   // We need to cast here
  Console.WriteLine (message);
}
View Code

捕获变量问题

因为lambda表达式造成闭包,致使有机会出现捕获变量。

for (int i = 0; i < 10; i++)
  new Thread (() => Console.Write (i)).Start();
View Code

上例的捕获变量:全世界只有一个i,因此被十条线程共用。

上面的代码造成了闭包,致使i成为捕获变量被十个匿名函数共享。出来的结果将是没法预料的。解决方法是在表达式内部声明变量,这将是匿名函数本身的变量。(此时循环增长一次就有一个temp因此每一个线程有本身的变量)

for (int i = 0; i < 10; i++)
{
  int temp = i;
  new Thread (() => Console.Write (temp)). Start();
}
View Code

Join:阻塞的是呼叫的线程

封锁呼叫的线程,直到其余线程结束为止。定义十分费解,看看例子。

例子1:Join阻塞的是呼叫的线程,在这个例子中呼叫的线程就是主线程。此时主线程将不会运行最后一行,直到次线程打印完了1000个y为止。

若是没有Join,则程序将马上退出。

static void Main()
{
  Thread t = new Thread (Go);
  t.Start();
  t.Join();
  Console.WriteLine ("Thread t has ended!");
}
 
static void Go()
{
  for (int i = 0; i < 1000; i++) Console.Write ("y");
}
View Code

例子2:等待

static void Main(string[] args)
        {
            Thread t1 = new Thread(PrintOne);
            Thread t2 = new Thread(PrintTwo);
            Thread t3 = new Thread(PrintThree);
            t1.Start();
            t2.Start();
            t2.Join(); //等待其余线程运行完毕(这里只有t1须要等待)
            t1.Join();
            t3.Start();
            Console.ReadKey();
        }

        static void PrintOne()
        {
            Console.WriteLine("One");
        }
        static void PrintTwo()
        {
            Console.WriteLine("Two");
        }
        static void PrintThree()
        {
            Console.WriteLine("Three");
        }
View Code

将按顺序打印One, Two, Three。t2.Join()阻塞呼叫的线程t2,因而等待t1运行完毕。T1.Join()则没有要等待的线程。

Join能够设置一个timeout时间。

Sleep

让线程中止一段时间。呼叫Sleep或Join将阻塞线程,系统将不会为其分配时间片,因此不会耗费系统性能。特别的,Sleep(0)会将线程如今的时间片马上用尽(即便还有剩余的时间)。

线程池

线程池是由CLR自动管理的,包含若干线程的集合。CLR利用线程池自动进行多线程中线程的建立,执行任务和销毁。利用任务或委托,能够隐式的和线程池发生关联。

线程池是如何管理线程的?

线程池的工做方法和普通的线程有所不一样。他维护一个队列QueueUserWorkItem,当程序想执行一个异步操做时,线程池将这个操做追加到队列中,并派遣给一个线程池线程。线程池建立伊始是没有线程的。若是线程池中没有线程,就建立一个新线程。

相对于普通的使用Threading类建立线程,线程池的好处有:

  1. 线程池中建立的线程不会在执行任务以后销毁,而是返回线程池等待下一个响应,这样咱们能够最大限度的重用线程。
  2. 线程池会尽可能用最少的线程处理队列中的全部请求,只有在队列增长的速度超过了请求处理的速度以后,线程池才会考虑建立线程。
  3. 若是线程池中的线程空闲了一段时间,它会本身醒来终止本身以释放资源。
  4. 当同时运行的线程超过阈值时,线程池将不会继续开新的线程,而是等待现有的线程运行完毕。

线程池的缺点:

  1. 你不能为线程命名
  2. 线程池建立的线程必定是后台线程

C#运用了线程池的类和操做有:

  1. 任务并行库
  2. 委托
  3. BackgroundWorker

等等。

使用线程池:经过任务

咱们能够经过建立一个任务来隐式的使用线程池:

static void Main()    // The Task class is in System.Threading.Tasks
{
  Task.Factory.StartNew (Go);
}
 
static void Go()
{
  Console.WriteLine ("Hello from the thread pool!");
}
View Code

任务方法能够有返回值,咱们能够经过访问Task.Result(会阻塞)来获得这个返回值。当访问时,若是任务执行中出现了异常,则咱们能够将访问Task.Result写入try块来捕捉异常。

使用线程池:显式操做

咱们能够经过显式操做ThreadPool.QueueUserWorkItem队列来操纵线程池,为它添加任务。咱们还可使用其的重载为任务指派输入变量。

static void Main()
{
  ThreadPool.QueueUserWorkItem (Go);
  ThreadPool.QueueUserWorkItem (Go, 123);
  Console.ReadLine();
}
 
static void Go (object data)   
{
  Console.WriteLine ("Hello from the thread pool! " + data);
}
View Code

和任务有所不一样,ThreadPool.QueueUserWorkItem的方法没法有返回值。并且,必须在方法的内部进行异常处理,不然将会出现执行时异常。

使用线程池:异步委托

异步委托是一种解决ThreadPool.QueueUserWorkItem没有返回值的方法。

static void Main()
{
  Func<string, int> method = Work;
  IAsyncResult cookie = method.BeginInvoke ("test", null, null);
  //
  // ... here's where we can do other work in parallel...
  //
  int ret = method.EndInvoke (cookie);
  Console.WriteLine ("String length is: " + ret);
}
 
static int Work (string s) { return s.Length; }
View Code

异步调用一个方法也至关于给线程池派了一个新的任务。咱们能够经过访问method.EndInvoke来得到访问结果。

相关文章
相关标签/搜索