现在的位置: 首页 > 综合 > 正文

.NET 线程

2012年05月11日 ⁄ 综合 ⁄ 共 8481字 ⁄ 字号 评论关闭

一概论

多线程在构建大型系统的时候是需要重点关注的一个重要方面,特别是在效率(系统跑得多快?)和性能(系统工作正常?)之间做一个权衡的时候。恰当的使用多线程可以极大的提高系统性能。

 什么是线程?
  每个正在系统上运行的程序都是一个进程。每个进程包含一到多个线程。进程也可能是整个程序或者是部分程序的动态执行。线程是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行。也可以把它理解为代码运行的上下文。所以线程基本上是轻量级的进程,它负责在单个程序里执行多任务。通常由操作系统负责多个线程的调度和执行。



多线程的好处:在于可以提高CPU的利用率

不利方面:

  1. 线程也是程序,所以线程需要占用内存,线程越多占用内存也越多

  2. 多线程需要协调和管理,所以需要CPU时间跟踪线程

  3.  线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题

  4.  线程太多会导致控制太复杂,最终可能造成很多Bug


以一个公司来比喻线程:

有利方面:

    在公司里,你可以一个职员干所有的事,但是效率很显然是高不起来的,一个人的公司也不可能做大,然而各司其职的员工无疑使公司效率更高

不利方面:

    公司的职员越多,老板就得发越多的薪水给他们,还得耗费大量精力去管理他们,协调他们之间的矛盾和利益


说了那么多,现在我们来讨论一下在Win32环境中常用的一些模型。

(掌握级别:了解)

  ·单线程模型

  在这种线程模型中,一个进程中只能有一个线程,剩下的进程必须等待当前的线程执行完。这种模型的缺点在于系统完成一个很小的任务都必须占用很长的时间。

  ·块线程模型(单线程多块模型STA

  这种模型里,一个程序里可能会包含多个执行的线程。在这里,每个线程被分为进程里一个单独的块。每个进程可以含有多个块,可以共享多个块中的数据。程序规定了每个块中线程的执行时间。所有的请求通过Windows消息队列进行串行化,这样保证了每个时刻只能访问一个块,因而只有一个单独的进程可以在某一个时刻得到执行。这种模型比单线程模型的好处在于,可以响应同一时刻的多个用户请求的任务而不只是单个用户请求。但它的性能还不是很好,因为它使用了串行化的线程模型,任务是一个接一个得到执行的。(此模型相当于在多线程中在一个线程执行的过程中,另外的线程都处于挂起的状态,直到此线程执行结束,后再开始另一个线程)

  ·多线程块模型(自由线程块模型)

  多线程块模型(MTA)在每个进程里只有一个块而不是多个块。这单个块控制着多个线程而不是单个线程。这里不需要消息队列,因为所有的线程都是相同的块的一个部分,并且可以共享。这样的程序比单线程模型和STA的执行速度都要块,因为降低了系统的负载,因而可以优化来减少系统idle的时间。这些应用程序一般比较复杂,因为程序员必须提供线程同步以保证线程不会并发的请求相同的资源,因而导致竞争情况的发生。这里有必要提供一个锁机制。但是这样也许会导致系统死锁的发生。

多线程在.NET里如何工作?

(掌握级别:了解)

在本质上和结构来说,.NET是一个多线程的环境。有两种主要的多线程方法是.NET所提倡的:使用ThreadStart来开始你自己的进程,直接的(使用ThreadPool.QueueUserWorkItem)或者间接的(比如Stream.BeginRead,或者调用BeginInvoke)使用ThreadPool类。一般来说,你可以"手动"为长时间运行的任务创建一个新的线程,另外对于短时间运行的任务尤其是经常需要开始的那些,进程池是一个非常好的选择。进程池可以同时运行多个任务,还可以使用框架类。对于资源紧缺需要进行同步的情况来说,它可以限制某一时刻只允许一个线程访问资源。这种情况可以视为给线程实现了锁机制。线程的基类是System.Threading。所有线程通过CLI来进行管理。


二操纵一个线程

下面通过一些实例解决对线程的控制,多线程间通讯等问题

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

  [CODE]
  //SystemThread.cs
  using System;
  using System.Threading;
  
  namespace ThreadTest
  {
  
  class RunIt
  
  {
  
    [STAThread]
  
    static void Main(string[] args)
  
    {
  
      Thread.CurrentThread.Name="System Thread";
 Console.WriteLine(Thread.CurrentThread.Name+"'Status:"+Threa d.CurrentThread.ThreadState); //
当前状态(ThreadState
  
      Console.ReadLine();
  
    }
  
  }
  }
  [/CODE]
  
编译执行后你看到了什么?是的,程序将产生如下输出:

  
  System Thread's Status:Running

CurrentThread Thread类的静态属性

所谓静态属性,就是这个类所有对象所公有的属性,不管你创建了多少个这个类的实例,但是类的静态属性在内存中只有一个。很容易理解CurrentThread为什么是静态的——虽然有多个线程同时存在,但是在某一个时刻,CPU只能执行其中一个。


现在注意到程序的头部

 using System.Threading;
所有与多线程机制应用相关的类都是放在System.Threading命名空间中的

Thread用于创建线程

ThreadPool用于管理线程池等等

如果你想在你的应用程序中使用多线程,就必须包含这个Thread

Thread类有几个至关重要的方法:

 Start():启动线程

 Sleep(int):
静态方法,暂停当前线程指定的毫秒数

 Abort():
通常使用该方法来终止一个线程

 Suspend()
:该方法并不终止未完成的线程,它仅仅挂起线程,以后还可恢复。

 Resume():
恢复被Suspend()方法挂起的线程的执行


下面我们来看一个两个线程的例子

(在下面的例子中有一个明显的线程争夺资源的问题)

namespace Programming_CSharp
{
using System;
using System.Threading;
class Test
{
static void Main( )
{
// 生成一个本类的实例
Test t = new Test( );
t.DoTest( );
}
public void DoTest( )
{
//Incrementer开一个线程并传入ThreadStart代理
Thread t1 =new Thread(new ThreadStart(Incrementer) );
//Decrementer开一个线程并传入ThreadStart代理
Thread t2 =new Thread(new ThreadStart(Decrementer) );
// 线程开始
t1.Start( );
t2.Start( );
}
public void Incrementer( )
{
for (int i =0;i<1000;i++)
{
Console.WriteLine("
增加: {0}", i);
}
}
public void Decrementer( )
{
for (int i = 1000;i>=0;i--)
{
Console.WriteLine("
减少: {0}", i);
}
}
}
}


       
输出:

增加: 102
增加: 103
增加: 104
增加: 105
增加: 106
减少: 1000
减少: 999
减少: 998
减少: 997
注意:输出为两个线程交替执行的结果(资源争夺)


阻塞线程:阻塞调用线程,直到某个线程终止时为止

例:t2.Join( )

暂停线程:

例:t2.Sleep(毫秒数)


挂起线程:

例:t2. Suspend()---------[恢复挂起的线程
例:t2. Resume()]


线程的高级使用__异步调用

例:

namespace Programming_CSharp
{
using System;
using System.Threading;
class Test
{
private int counter = 0;
static void Main( )
{
//生成一个本类的实例
Test t = new Test ( );
t.DoTest( );
}
public void DoTest( )
{
Thread t1 = 
new Thread( new ThreadStart(Incrementer) );
t1.IsBackground=
true;
t1.Name = "
线程1";
t1.Start( );
Console.WriteLine("
开始线程 {0}",
t1.Name);
Thread t2 = 
new Thread( new ThreadStart(Incrementer) );
t2.IsBackground=
true;
t2.Name = "ThreadTwo";
t2.Start( );
Console.WriteLine("
开始线程 {0}",
t2.Name);
t1.Join( );
t2.Join( );

Console.WriteLine("所有线程完成.");
}

public void Incrementer( )
{
try
{
while (counter < 1000)
{
int temp = counter;
temp++; 
// 增加
Thread.Sleep(1);
counter = temp;
Console.WriteLine("
线程 {0}. 增加: {1}",Thread.CurrentThread.Name,counter);
}
}
catch (ThreadInterruptedException)
{
Console.WriteLine("
线程 {0} 中断清空中…",Thread.CurrentThread.Name);
}
finally
{
Console.WriteLine("
线程 {0} 退出. ",Thread.CurrentThread.Name);
}
}
}
}



输出:


线程开始 线程1
线程开始 线程2
线程 线程1. 增加: 1
线程 线程2. 增加: 2
线程 线程1. 增加: 3
线程 线程2. 增加: 4
线程 线程1. 增加: 5
线程 线程2. 增加: 6
线程 线程1. 增加: 7
线程 线程2. 增加: 8
线程 线程1. 增加: 9
线程 线程2. 增加: 10
线程 线程增加: 11
线程 线程2. 增加: 12
线程 线程1. 增加: 13
线程 线程2. 增加: 14
线程 线程1. 增加: 15
线程 线程2. 增加: 16
线程 线程1. 增加: 17
线程 线程2. 增加: 18
刚才我们看了一个异步调用的例子,那现在我们就来对异步调用进行些探讨。

一:

首先:

    我们先对异步调用同步调用进行个比较:

    1.在单线程方式下,计算机是一台严格意义上的冯·诺依曼式机器,一段代码调用另一段代码时,只能采用同步调用,必须等待这段代码执行完返回结果后,调用方才能继续往下执行。

2. 在多线程方式下,可以采用异步调用,调用方和被调方可以属于两个不同的线程,调用方启动被调方线程后,不等对方返回结果就继续执行后续代码。被调方执行完毕后,通过某种手段通知调用方

另外,异步调用用来处理从外部输入的数据特别有效


例:

某个程序启动后如果需要打开文件读出其中的数据,再根据这些数据进行一系列初始化处理,程序主窗口将迟迟不能显示,让用户感到这个程序怎么等半天也不出来,太差劲了。借助异步调用可以把问题轻松化解:把整个初始化处理放进一个单独线程,主线程启动此线程后接着往下走,让主窗口瞬间显示出来。等用户盯着窗口犯呆时,初始化处理就在背后悄悄完成了。程序开始稳定运行以后,还可以继续使用这种技巧改善人机交互的瞬时反应。用户点击鼠标时,所激发的操作如果较费时,再点击鼠标将不会立即反应,整个程序显得很沉重。借助异步调用处理费时的操作,让主线程随时恭候下一条消息,用户点击鼠标时感到轻松快捷,肯定会对软件产生好感。

二:

在上面我们提到:"被调方执行完毕后,通过某种手段通知调用方",下面我们就来看看可以采取哪些手段来通知调用方

在同一进程中有很多手段可以利用,常用的手段有1.回调;2.event 对象; 3.消息。

  1. 回调: 调用异步函数时在参数中放入一个函数地址,异步函数保存此地址,待有了结果后回调此函数便可以向调用方发出通知。如果把异步函数包装进一个对象中,可以用事件取代回调函数地址,通过事件处理例程向调用方发通知。


示意图见下一页





  1. event 对象:event 是 Windows 系统提供的一个常用同步对象,以在异步处理中对齐不同线程之间的步点。如果调用方暂时无事可做,可以调用 wait 函数等在那里,此时 event 处于 nonsignaled 状态。当被调方出来结果之后,把 event 对象置于 signaled 状态,wait 函数便自动结束等待,使调用方重新动作起来,从被调方取出处理结果。这种方式比回调方式要复杂一些,速度也相对较慢,但有很大的灵活性,可以搞出很多花样以适应比较复杂的处理系统。
  2. 消息:   借助 Windows 消息发通知是个不错的选择,既简单又安全。程序中定义一个用户消息,并由调用方准备好消息处理例程。被调方出来结果之后立即向调用方发送此消息,并通过 WParam 和 LParam 这两个参数传送结果。消息总是与窗口 handle 关联,因此调用方必须借助一个窗口才能接收消息,这是其不方便之处。另外,通过消息联络会影响速度,需要高速处理时回调方式更有优势。

另外:如果调用方和被调方分属两个不同的进程,由于内存空间的隔阂,一般是采用 Windows 消息发通知比较简单可靠,被调方可以借助消息本身向调用方传送数据。event 对象也可以通过名称在不同进程间共享,但只能发通知,本身无法传送数据,需要借助 Windows 消息和 FileMapping 等内存共享手段或借助  MailSlot 和 Pipe 等通信手段。


提醒:异步调用原理并不复杂,但实际使用时容易出莫名其妙的问题,特别是不同线程共享代码或共享数据时容易出问题,编程时需要时时注意是否存在这样的共享,并通过各种状态标志避免冲突。Windows 系统提供的 mutex 对象用在这里特别方便。mutex 同一时刻只能有一个管辖者。一个线程放弃管辖权后,另一线程才能接管。当某线程执行到敏感区之前先接管 mutex,使其他线程被 wait 函数堵在身后;脱离敏感区之后立即放弃管辖权,使 wait 函数结束等待,另一个线程便有机会光临此敏感区。这样就可以有效避免多个线程进入同一敏感区(异步调用容易出问题,要设计一个安全高效的编程方案需要比较多的设计经验,所以最好不要滥用异步调用)。


线程安全

    在前面我举了一个抛球的例子,这里我们再来看看,想象一下如果每次我们都是抛三个球,让在空中的那个球悬停在原处,这时将左手的球抛向右手,好,这是让空中的那个球落下同时右手的球抛向空中,左手的球抛向右手,此时又让空中的球停住,这个时候,之前停在空中的球已经顺利到了你的左手。依次这般,抛球就会变的非常简单。这就是线程安全。在我们的程序之中强行的让一个线程等待另一个线程完成后再开始,这种情况就叫做线程阻塞或是线程异步。在C# 中我们会锁住内存的一部分(通常是一个对象的实例)不允许其他线程进入,直到调用这部分内存的线程结束时才可以。好了,说了那么多,还是让我们来看看一些例子吧

在下面的例子中,将会创建两个线程,Thread 1 Thread 2一个共享变量threadOurput.threadOutput 将会根据他所在的线程而被赋予不同的消息。下面我们就来看看。

1

using System;

using System.Collections.Generic;

using System.Text;

using System.Threading;


namespace ConsoleApplication1

{


class
Program

{



bool stopThreads = false;



private
string threadOutput = "";




void DisplayThread1()

{



while (stopThreads == false)

{



Console.WriteLine("显示线程");

threadOutput = "好啊,线程1";


Thread.Sleep(1000);


Console.WriteLine("线程输出--> {0}", threadOutput);

}


}


void DisplayThread2()

{



while (stopThreads == false)

{


Console.WriteLine("显示线程");

threadOutput = "好啊,线程2";


Thread.Sleep(1000);


Console.WriteLine("线程输出--> {0}", threadOutput);

}


}


void class1()

{


Thread thread1 = new
Thread(new
ThreadStart(this.DisplayThread1));


Thread thread2 = new
Thread(new
ThreadStart(this.DisplayThread2));

thread1.Start();

thread2.Start();

}


static
void Main(string[] args)

{


Program program = new
Program();

program.class1();

}


}


}

上面代码的输出结果显示在图2中。仔细地看这个结果,你会发现这段程序给出了与预料的不一致的结果。虽然我们认真地给threadOutput赋予了相应的字符串,但是输出依然不是我们所预期的那样,这是为什么呢?


2 –两个不正常的输出.



解释,为什么?

以本程序为例,看到这样结果的原因是代码执行了两个方法DisplayThread1DisplayThread2,每个方法都使用变量threadOutput , 所以很有可能threadOutput虽然在线程1中被赋上了值"好啊,线程1"并且把它显示出来,但是在线程1threadOutput赋值并将其值显示的时候,这时线程2threadOutput的值赋为"好啊,线程2"。由于对同一变量的占用,出现如图1那样的奇怪结果也就完全可能了。这个很是令人头疼的线程问题在线程的编程中相当平常,通常被叫做争用条件。对于程序而言,在某些时候争用简直就是噩梦,因为它不常出现而且很难很难去再现。

在争用中获胜

 避免争用的最好方式是编写线程安全的代码。 如果你的代码是线程安全的,就能防止一些突发的线程问题。
下面是一些编写线程安全代码的技巧。首先是尽量少的去共享内存。如果你创建了一个类的实例而且他运行在线程中,而此时你又在另一个线程中创建相同类的实例,这些类是线程安全的,只要他们没有包含任何静态变量。这两个类在内存中都有自己的运行域,没有共享内存
。如果在类或实例中确实含有静态变量并对其他线程共享,你就必须要寻找到某个方式确保一个线程不能使用这部分内存知道其他的某一线程完成对其的使用。
我们管这种防止其他线程影响被占用内存的方法叫做锁定C#中允许我们上锁代码通过Monitor类或lock{}结构 。对lock的使用,让我们看看下面这段代码吧。


例 2:

using System;

using System.Collections.Generic;

using System.Text;

using System.Threading;


namespace ConsoleApplication1

{


class
Program

{


bool stopThreads = false;


private
string threadOutput = "";


void DisplayThread1()

{


while (stopThreads == false)

{


lock (this)

{


Console.WriteLine("显示线程1");

threadOutput = "好啊,线程1";


Thread.Sleep(1000);


Console.WriteLine("线程输出--> {0}", threadOutput);

}

}

}


void DisplayThread2()

{


while (stopThreads == false)

{


lock (this)

{


Console.WriteLine("显示线程2");

threadOutput = "好啊,线程2";


Thread.Sleep(1000);


Console.Wri

抱歉!评论已关闭.