一、 多线程的概念
1.线程和进程
线程和进程是现代操作系统的重要概念。
前者是应用程序的实例,一个正在运行的应用程序在操作系统中被视为一个进程。进程拥有自身独立的资源,进程之间相互隔离,互不干扰。为了使多个任务互不干扰,每个进程都拥有独立的虚拟地址空间,代码段,数据段以及堆栈,另外进程还占了各种系统资源(如文件,窗体对象,环境变量等等)。
线程是操作系统分配处理器时间的基本单元。一组指令的集合,可以在程序中独立的执行,也被称为“轻量级进程”或者“微进程”。一个进程可以包含一个或者多个线程。线程共享其所属进程所拥有的资源,可以访问进程的内存区域和代码段。同时线程还拥有各自的局部变量和独立的栈空间。每个进程至少拥有一个进程来执行它的代码,如果没有没有线程来执行,系统就会自动撤销该进程和它的地址空间。
2.线程的生命周期和状态
从线程被创建到被终止称为线程的生命周期。
在线程被创建以后,该线程出于开始状态。
一般情况下,线程会从开始状态转为就绪状态,只有出于就绪状态的线程才会被操作系统按照一定的调度算法进行调度,从而转入运行状态。
出于运行状态的线程也会在一定的时间(如时间片用完)被调度出运行状态进入就绪状态,等待下次调度。
处于运行状态的线程也可能因为等待某个资源,或者被人为的设定进入休眠状态,直至需要的资源被释放或者是人为地休眠指令结束,就会转入就绪状态。
处于运行状态的线程会因指令执行完毕,或者被人为地终止而进入终止状态。
3.线程的优先级
每个线程都被赋予了一定的优先级。在线程被调度执行的时候,回报根据该线程的优先级来进行调度,一般来说,优先级搞得线程会被优先调度执行。线程的优先级可以通过Thread类Priority属性设置,Priority属性是一个ThreadPriority型枚举,列举了5个优先等级:AboveNormal、BelowNormal、Highest、Lowest、Normal。普通线程的优先级默认为Normal;如果想有更高的优先级,可设置为AboveNormal或Highest;如果想有较低的优先级,可设置为BelowNormal或Lowest。
二、线程创建与控制
1.创建和启动线程
命名空间:
using System; using System.Threading;
a.线程用Thread类来创建, 通过ThreadStart委托来指明方法从哪里开始运行,下面是ThreadStart委托如何定义的:
public delegate void ThreadStart();
调用Start方法后,线程开始运行,线程一直到它所调用的方法返回后结束。下面是一个例子,使用了C#的语法创建TheadStart委托。
static void Main(string[] args) { ThreadStart ts = new ThreadStart(Print); Thread t = new Thread(ts); t.Start(); Print(); Console.ReadLine(); } static void Print() { Console.WriteLine("Hello World!"); }
b.将数据传入ThreadStart中
在创建托管的线程时,在该线程上执行的方法将通过一个传递给 Thread 构造函数的 ThreadStart 委托或 ParameterizedThreadStart 委托来表示。在调用 System.Threading.Thread.Start 方法之前,该线程不会开始执行。执行将从 ThreadStart 或 ParameterizedThreadStart 委托表示的方法的第一行开始。
public delegate void ParameterizedThreadStart (object obj); static void Main(string[] args) { Thread t = new Thread(new ParameterizedThreadStart(Print)); t.Start("线程a"); //Thread.Sleep(20); Print("主线程"); Console.ReadLine(); } static void Print(Object obj) { Console.WriteLine(obj.ToString()); }
C.线程的一些属性
2.控制线程
当执行一个线程后,该线程会经历一个生命周期,及开始,就绪,运行,休眠,终止等。这些状态可以通过线程的ThreadState属性来获取。
三、多线程的同步
1.线程安全
1.1争用条件
两个或者多个线程同事访问统一数据或者资源时,会导致不符合要求或者无法预期的结果。
a.当线程们引用了一些公用的目标实例的时候,他们会共享数据.
class ThreadTest { bool done; static void Main(string[] args) { ThreadTest tt = new ThreadTest(); new Thread(tt.Print).Start(); tt.Print(); Console.ReadLine(); } void Print() { if (!done) { done = true; Console.WriteLine("Hello World!"); } } }
b.静态字段提供了另一种在线程间共享数据的方式.
class ThreadTest { static bool done; static void Main(string[] args) { new Thread(Print).Start(); Print(); Console.ReadLine(); } static void Print() { if (!done) { done = true; Console.WriteLine("Hello World!"); } } }
上述两个例子足以说明, 另一个关键概念, 那就是线程安全. 输出实际上是不确定的:它可能(虽然不大可能) , 可以被打印两次。然而,如果我们在Print方法里调换指令的顺序, 打印两次的机会会大幅地上升:
if (!done) { Console.WriteLine("Hello World!"); done = true; }
补救措施是当读写公共字段的时候,提供一个排他锁
class ThreadTest { static bool done; static object locker = new object(); static void Main(string[] args) { new Thread(Print).Start(); Print(); Console.ReadLine(); } static void Print() { lock (locker ) { if (!done) { Console.WriteLine("Hello World!"); done = true; } } } }
当两个线程争夺一个锁的时候(在这个例子里是locker),一个线程等待,或者说被阻止到那个锁变的可用。在这种情况下,就确保了在同一时刻只有一个线程能进入临界区。
1.2死锁
如果多个线程彼此等待对方释放其所占用的资源,则也会遇到线程安全的问题。这种对线程的阻塞称为死锁。
2.线程同步策略
2.1同步上下文
上下文是一组有序的属性或者规则,这组属性或规则将类似的对象绑定在一起。同步上下文策略就是直接使用.net提供的SynchronizationAttribute类的构造函数对驻留上下文中,符合上下文规则的对象启用简单的自动同步,确保同一时刻只有一个线程可以访问该对象。该策略不处理静态字段和方法的同步。
可以使用SynchronizationAttribute属性,为ContextBoundObject的派生对象启用简单的自动同步,该属性为当前上下文和所有共享同一实例的上下文强行创建一个同步域。将该属性应用于某一个对象时,在共享该属性的实例的所有上下文中只能有一个线程执行,多个线程可以访问方法和字段,但在任一时刻只允许一个线程访问。
2.2同步代码区
Monitor类和Lock关键字
Monitor类用于同步代码区,其思想是首先使用Monitor.Enter()方法获得一个锁,然后使用Monitor.Exit()方法释放该锁。一个线程一旦获得重要代码区的锁,其他的线程就要等到该锁被释放后才能使用该代码区。这样就能通过同步最少量的代码,实现最大限度的并发。
使用Lock关键字同样可以获取一个Monitor锁。
2.3手工同步
ReaderWriterLock类等等
ReaderWriterLock类提供单个进程写和多个进程度的控制机制,优点是资源开销非常低。该类有两个锁:读线程锁和写线程锁。当请求写线程锁后,在写线程取得访问权之前,不会接受任何新的读线程,从而实现多个线程在任何时刻执行读方法,或允许单个线程在任何时刻执行写方法。