第13章 C#中的多线程

13多线程 编程

13.1 线程概述 安全

计算机的操做系统多采用多任务和分时设计。多任务是指在一个操做系统中开以同时运行多个程序。例如,能够在使用QQ聊天的同时听音乐,即有多个独立的任务,每一个任务对应一个进程,每一个进程也可产生多个线程。 网络

13.1.1 进程 多线程

认识进程先从程序开始,程序(Program)是对数据描述与操做的代码的集合,如Office中的Word,影音风暴等应用程序。 并发

进程(Process)是程序的一次动态执行过程,它对应了从代码加载、执行至执行完毕的一个完整过程,这个过程也是进程自己从产生、发展直至消亡的过程。操做系统同时管理一个计算机系统中的多个进程,让计算机系统中的多个进程轮流使用CPU资源,或者共享操做系统的其它资源。 异步

进程的特定是: 函数

  • 进程是系统运行程序的基本单位
  • 每个进程都有本身独立的一块内存空间、一组系统资源。
  • 每个进程的内部数据和状态都是彻底独立的。

在操做系统中能够有多个进程,这些进程包括系统进程(由操做系统内部创建的进程)和用户进程(由用户程序创建的进程)。能够从Windows任务管理器中查看已启动的进程。图13-1中标注了已启动的Office Word进程。 性能

 

13-1 Windows任务管理器中的Office World进程 学习

13.1.2 线程 测试

线程是进程中执行运算的最小单位,可完成一个独立的顺序控制流程。每一个进程中,必须至少创建一个线程(这个线程称为主线程)来做为这个程序运行的入口点。若是在一个进程中同时运行了多个线程,用来完成不一样的工做,则称之为"多线程"。在操做系统将进程分红多个线程后,实际上每一个任务是一个线程,多个线程共享相同的地址空间而且共同分享同一个进程,这些线程能够在操做系统的管理下并发执行。从而大大提升了程序的运行效率。虽然线程的执行看似是多个线程同时执行,但实际上并不是如此。因为单CPU的计算机中,CPU同时只能执行一条指令,所以,在仅有一个CPU的计算机上不可能同时执行多个任务。而操做系统为了能提升程序的运行效率,将CPU的执行时间分红多个时间片,分配给不一样的线程,当一个时间片执行完毕后,该线程就可能让出CPU使用权限交付给下一个时间片的其它线程执行。固然有可能相邻的时间片分配给同一线程。之因此表面上看是多个线程同时执行,是由于不一样线程之间切换的时间很是短,也许仅仅是几毫秒,对普通人来讲是难以感知的,这样就看似多个线程在同时执行了。

为了更好地了解线程,下面举一个通俗的例子。张平是某互联网公司的开发人员,有时工做比较单一,仅是开发项目,写代码,这就好像程序只执行一个线程。在单线程环境中,每一个程序执行的方式是只有一个处理顺序。但对开发人员来讲,这只是一种理想的状态,有时还须要兼顾其它工做,如修改Bug,参与技术交流、编写项目文档,和其它部门同时沟通项目需求等。项目经理通常会但愿张平一天内多项工做都会有进展,可是在同一时间,张平只能作一个项目工做。这就好像程序开启了多个线程,能够处理多个不一样任务,可是CPU在同一时刻,只能执行该进程的一个线程。

13.1.3 多线程的好处

多线程做为一种多任务并发的工做方式,有着普遍的应用,合理地使用线程,将减小开发和维护的成本,甚至能够改善复杂应用程序的性能。使用多线的优点以下。

  • 充分利用CPU的资源:执行单线程程序时,若程序发生阻塞,CPU可能会处于空闲状态,这将形成计算机资源浪费,而使用多线程能够在某个线程处理休眠或阻塞状态时运行其它线程,这样,大大提升了资源利用率。
  • 简化编程模型:一个既长又复杂的进程能够考虑分为多个线程,成为几个独立的运行部分,如使用时、分、秒来描述当前时间,若是写成单线程程序可能须要多循环判断,而若是使用多线程,时、分、秒各使用一个线程控制,每一个线程仅需实现简单的流程,简化了程序逻辑,这样更有助于开发人员对程序的理解和维护。
  • 带来良好的用户体验:因为多个线程能够交替执行,减小或避免了因程序阻塞或意外状况形成的响应过慢现象,下降了用户等待的概率。

13.2 C#中实现多线程

.NET中,Thread类将线程所必需的功能作了封装,位于System.Threading命名空间。

13.2.1 主线程

在程序启动时,一个线程马上运行,该线程一般称为程序的主线程。程序的Main()方法是主线程的入口。每一个进程都至少有一个主线程。它是程序开始时就执行的。主线程的重要性体如今如下两方面。

  • 它是产生其它子线程的线程
  • 一般它必须最后完成执行,由于它执行各类关闭动做。

尽管主线程在程序启动时自动建立,但它能够由一个Thread对象控制。下面的实例1显示如何引用主线程。

示例1

     static void Main(string[] args)
    {
        //
判断当前线程是否已经命名
        if (Thread.CurrentThread.Name == null)
        {
            Thread.CurrentThread.Name = "MainThread";
        }
        Console.WriteLine("
当前线程的名称为:"+Thread.CurrentThread.Name);
    }

在实例1中,Thread类的CurrentThread静态属性能够得到当前主线程对象,线程对象的Name属性表示线程的名称,此属性默认值为null,且只可赋值一次。若是第二次赋值,则会引起异常,故在实例1中先判断线程对象是否已经命名。程序运行结果如图13-2所示。

 

 

13-2 显示当前线程名称

开发中,用户编写的线程通常是指除了主线程以外的其它线程。使用一个线程的过程能够分为如下三个步骤。

1)定义一个线程对象,同时指明这个线程所要执行的代码,即指望完成的功能。

2)启动线程。

3)终止线程。

13.2.2 建立线程

下面来编写代码,建立两个线程,在线程中输出1~100的整数。

实例2

static void Main(string[] args)
{
//
建立第一个线程对象
Thread thread1 = new Thread(DoWork);
thread1.Name = "myThread1";
//
建立第二个线程对象
Thread thread2 = new Thread(DoWork);
thread2.Name = "myThread2";
//
启动线程
thread1.Start();
thread2.Start();
}
//
线程要调用的方法
static void DoWork()
{
for(int i=1;i<=100;i++)
{
Console.WriteLine(Thread.CurrentThread.Name+":"+i);
}
}

实例2中,经过Thread类的构造函数建立线程对象,Thread的构造函数形式的以下:

public Thread(ThreadStart start);

其参数start的类型ThreadStart是一个委托。表示要执行的方法。

Thread对象的Start()方法用来启动线程。程序运行结果如图13-3所示。

 

 

13-3 建立线程并启动

经过图13-3的结果能够看出,两个线程对象调用Start()方法后,各自都会输出100之内的整数,互不影响,并行执行。在.NET中每一个线程都有本身独立的内存栈,因此每一个线程的本地变量都相互独立。但因为CPU在一个时间点只能执行一个线程,所以多个线程是交替执行的,得到CPU时间片的线程即刻执行,当前时间片执行完毕后,CPU就会执行得到下一个时间片的线程。分配的时间片长度不是彻底一致的,可多可少,所以每次运行的结果有所不一样,总之,是轮换交替执行的。

13.3 线程的状态

任何线程通常都具备五种状态,即建立、就绪、运行、阻塞、死亡状态。线程状态的转移与方法之间的关系如图13-4所示。

 

 

13-4 线程状态的转移与方法之间的关系

  • 建立状态
    在程序中建立一个线程对象后,新的线程对象就处于建立状态,此时,他已经获取了相应的资源,但尚未处于可运行的状态,这时能够设置
    Thread对象的属性,如线程名称、优先级等。
  • 就绪状态
    线程建立以后,就能够经过调用
    Start()方法启动线程,即进入就绪状态。此时,线程将进入线程队列排队,等待CPU资源,这代表它已经具有了运行条件,在未得到CPU资源时,仍不能真正执行。举例来讲,去医院看病,某主任的专家号天天只有20个,挂上号的患者还需在分诊处等待叫号。这里每一个挂到专家号的患者能够当作一个就绪状态的线程。
  • 运行状态
    当就绪状态的线程得到
    CPU资源时,便可转入运行状态。对只有一个CPU的计算机而言,任什么时候刻只能有一个处于运行状态的线程占用CPU,即得到CPU资源。延续上面医院看病的例子,被叫到的患者才能真正就诊,而每一个主任专家在一个时刻只能为一个患者看病。
  • 阻塞状态
    一个正在运行的线程因某种缘由不能继续运行时,进入阻塞状态。阻塞状态是一种"不可运行"的状态,而处于这种状态的线程在获得一个特定的事件以后会转回可运行状态。举例来讲,轮到小张看病了,医生为查明缘由要求他去作个化验,医生获得化验结果后才能继续诊断,若是把医生给小张看病看做一个线程,则该线程此时即处于阻塞状态。
    可能使线程暂停执行的条件以下。
    • 因为线程优先级比较低,所以它不能得到CPU资源。
    • 使用Sleep()方法使线程休眠。
    • 经过调用Wait()方法,使线程等待。
    • 经过调用Yield()方法,线程显示出让CPU控制权。
    • 线程因为等待一个文件,I/O事件被阻塞。
  • 死亡状态
    一个线程运行完毕,线程则进入死亡状态。处于死亡状态的线程不具备继续运行的能力。

13.4 线程的调度
在单CPU的计算机中,一个时刻只有一个线程运行,所谓多线程的并发运行,实际上是指从宏观上看,各个线程轮流得到CPU资源的使用权,分别执行各自的任务。.NET Framework能够对线程进行调度。线程调度是指按照特定机制为多个线程分配CPU的使用权。
13.4.1
线程的优先级
当同一时刻有一个或多个线程处于运行状态时,它们须要排队等待CPU资源,每一个线程会自动得到一个线程的优先级(Priority),优先级的高低反映线程的重要或紧急程度。那么此刻,通常状况下优先级高的线程得到CPU资源的几率较大,但这个结果不是绝对的,线程优先级调度是一个复杂的过程。
Thread
对象能够经过Priority属性(枚举类型)设置线程的优先级,优先级从低到高的五个取值为LowestBelowNormalNormalAboveNormalHighest
咱们对实例2中的代码进行修改,分别设置连个线程的优先级,如实例3所示。
实例3
static void Main(string[] args)
{
//
建立第一个线程对象
Thread thread1 = new Thread(DoWork);
thread1.Name = "myThread1";
//
建立第二个线程对象
Thread thread2 = new Thread(DoWork);
thread2.Name = "myThread2";
//
设置线程的优先级
thread1.Priority = ThreadPriority.Highest;
thread2.Priority = ThreadPriority.Lowest;
//
启动线程
thread1.Start();
thread2.Start();
}
//
线程要调用的方法
static void DoWork()
{
for(int i=1;i<=100;i++)
{
Console.WriteLine(Thread.CurrentThread.Name+":"+i);
}
}
运行结果如图13-5所示。

 

 

13-5 线程的优先级

从图13-5中看出,优先级高的thread1对象优先执行完成。
13.4.2 线程的休眠
在程序中容许一个线程进行暂时休眠,直接使用
Thread.Sleep()方法便可实现线程的休眠。Sleep()方法的定义语法以下。
public static void Sleep(int millisecondsTimeout)
参数表示休眠时长,单位为毫秒。线程由运行中的状态进入不可运行状态,睡眠时间事后线程会再次进入可运行状态。
实例
4模拟主线程休眠五秒后开始执行。
实例
4
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Wait");
WaitBySec(5); //
让主线程等待5秒再执行
Console.WriteLine("Start");
}

public static void WaitBySec(int s)
{
for(int i=0;i<s;i++)
{
Console.WriteLine(i+1+"
");
Thread.Sleep(1000); //
睡眠1
}
}
}
运行结果如图13-6所示。

 

 

13-6 线程的休眠

13.4.3 线程的强制运行

Join()方法使当前线程暂停执行,等待调用该方法的线程结束后再继续执行本线程。

下面经过实例来具体看一下Join()方法的应用。实例5为使用Join()方法阻塞线程的案例。

实例5

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

class Program

{

static void Main(string[] args)

{

Console.WriteLine("*********线程强制执行**********");

//建立子线程并启动

Thread temp = new Thread(DoWork);

temp.Name = "MyThread1";

temp.Start();

for(int i=0;i<20;i++)

{

if (i == 5)

temp.Join(); //阻塞主线程,子线程强制执行

Thread.Sleep(100);

Console.WriteLine("主线程运行:"+i);

}

Console.ReadKey();

}

static void DoWork()

{

for(int i=1;i<=10;i++)

{

Thread.Sleep(100); //增长线程交替执行的概率

Console.WriteLine(Thread.CurrentThread.Name+":"+i);

}

}

}

在示例5中,主线程的i的值为5时,子线程调用Join()方法,阻塞主线程,子线程强制执行,直到子线程运行完毕后,主线程才能继续执行。运行结果如图13-7所示。

 

 

13-7 线程的强制执行

13.4.4 线程的礼让

Yield()方法定义的语法以下。

public static bool Yield();

Yield()方法可暂停当前线程执行,容许其它具备相同优先级的线程得到运行机会,该线程仍处于就绪状态,不转为阻塞状态,此时,系统选择其它相同或更高优先级线程执行。若无其它相同或更高优先级线程,则该线程继续执行。返回值为bool类型,若是操做系统转而执行另外一个线程,则为 true;不然为 false。示例6实现了两个线程之间的礼让。

注意

使用Yield()的线程礼让只是提供一种可能,可是不能保证必定会礼让,由于礼让的线程处于就绪状态,还有可能被线程调度程序再次选中。

示例6

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

class Program

{

static void Main(string[] args)

{

Thread thA = new Thread(DoWork);

thA.Name = "线程A";

Thread thB = new Thread(DoWork);

thB.Name = "线程B";

thA.Start();

thB.Start();

Console.ReadKey();

}

static void DoWork()

{

for(int i=0;i<5;i++)

{

Console.WriteLine(Thread.CurrentThread.Name+"正在运行:"+i);

if (i==3)

{

Console.Write("线程礼让:");

Thread.Yield();

}

}

}

}

运行结果如图13-8所示

 

 

13-8 线程的礼让

从程序的运行结果中能够放发现,每当线程知足条件(i==3)时,建议当前线程暂停,而让其它线程先执行,固然,这仅是提供一种可能。

13.5 线程的同步

13.5.1 多线程共享数据引起的问题

前面学习的线程都是独立的,并且异步执行,也就是说每一个线程都包含了运行时所须要的数据或方法,而不须要外部资源或方法,也没必要关心其它线程的状态和行为。可是常常有一些同时运行的线程须要共享数据,此时就须要考虑其它线程的状态和行为,不然不能保证程序运行结果的正确性。

举个例子来讲,咱们都熟悉每一年春运抢票的场景。之前须要亲自到火车站或者售票点排队买票,火车站每一个车次会按期定量发放车票,先到先得。如今互联网愈来愈发达,能够网上买票了,这样又多了一个更加方便的购票渠道。如今,咱们使用多线程来模拟多人买票的过程。每一个人抢到票的机会均等,这样,能够把每个看做是一个线程,购票过程是线程的执行体,而每售出一张票,总票数就会减小,所以注意,预发售的火车票总数是多线程所共同操做的数据。假定如今有三我的抢十张票,实现思路以下。

1)定义类Site模拟售票网站。发放固定车次的车票,这里为简化过程,设定预出售的车票总共十张,定义变量Count记录剩余票数,变量Num记录当前售出第几张票。

2实现售票方法SaleTicket()。网站将持续提供售票服务,所以,这里使用到循环语句,Count做为循环变量。在循环体中,当还有余票时,购票过程分为如下两步。

第一步,修改数据,指当前售票序号(Num)以及剩余票数(Count)

第二步,显示售票信息。

在两步之间,为模拟网络延迟,使用Sleep()方法设置线程休眠500毫秒。

3)定义测试类模拟多人抢票。建立三个线程,指定线程名称,并启动线程。

代码如示例7所示。

示例7

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

public class Site

{

int Count = 10; //记录剩余票数

int Num = 0; //记录买到第几张票

//售票方法

public void SaleTicket()

{

while(true)

{

//没有余票时跳出循环

if (Count <= 0)

break;

//第一步:修改数据

Num++;

Count--;

Thread.Sleep(500); //模拟网络延时

//第二步:显示信息

Console.WriteLine("{0}抢到第{1}票,剩余{2}票!",

Thread.CurrentThread.Name,Num,Count);

}

}

}

//测试类Main()方法

static void Main(string[] args)

{

Site site = new Site();

Thread person1 = new Thread(site.SaleTicket);

person1.Name = "王小毛";

Thread person2 = new Thread(site.SaleTicket);

person2.Name = "抢票代理";

Thread person3 = new Thread(site.SaleTicket);

person3.Name = "黄牛党";

person1.Start();

person2.Start();

person3.Start();

}

示例7中,Main()方法中建立三个线程模拟三人开始抢票,而且启动线程。运行结果如图13-9所示。

 

 

13-9 多线程模拟网络购票

从图13-8中发现,最终显示结果存在如下问题。

  • 不是从第一张票开始。
  • 存在多人抢到一张票的状况。
  • 有些票号没有被抢到。

这是因为多线程并发执行操做同一共享资源时,将带来数据不安全问题。例如,在上述案例中。三我的共同抢票,各自执行完第一步修改数据,此时等待网络延时,再执行第二步显示信息时,因为前面修改了三次数据后Count值为7Num值为3,最终显示三我的都抢到了第三张票。这固然仅仅是一种状况。

要解决此类问题,就须要保证一我的在抢票过程未结束前,不容许其余人同时抢票。这在开发中,就须要使用线程同步。

注意

示例7中提出的问题仅在多线程共享统一资源时产生,如三人共抢十张票;反之,在不存在资源共享时无需考虑此类问题。

13.5.2 线程同步的实现

当两个或多个线程须要访问同一资源时,须要以某种顺序来确保该资源某一时刻只能被一个线程使用,这就称为线程同步。

C#中,咱们能够经过lock语句实现线程同步。

资料

C#中实现线程的同步有几种方法:lockMutexMonitorSemaphoreInterlockedReaderWriterLock等。同步策略也能够分为同步上下文、同步代码区、手动同步几种方式。你们能够查阅MSDN自行学习。

lock 关键字将语句块标记为临界区,方法是获取给定对象的互斥锁,执行语句,而后释放该锁。lock 确保当一个线程位于代码的临界区时,另外一个线程不进入临界区。若是其余线程试图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。其语法以下:

lock(locker)
{
//
须要同步的代码
}

提供给 lock 关键字的参数locker必须为基于引用类型的对象,该对象用来定义锁的范围,能够是任意类实例。

针对示例7的状况,咱们用lock语句完成线程同步。如实例8所示

示例8

public class Site
{
int Count = 10; //
记录剩余票数
int Num = 0; //
记录买到第几张票
//
定义锁定对象
private object locker = new object();

public void SaleTicket()
{
while(true)
{
lock (locker)
{
//
没有余票时跳出循环
if (Count <= 0)
break;
//
第一步:修改数据
Num++;
Count--;
Thread.Sleep(500); //
模拟网络延时
//
第二步:显示信息
Console.WriteLine("{0}
抢到第{1}票,剩余{2}票!",
Thread.CurrentThread.Name, Num, Count);
}
}
}

}

当同步对共享资源的线程访问时,请锁定专用对象实例(例如,private object locker = new object();)或另外一个不太可能被代码无关部分用做 lock 对象的实例。避免对不一样的共享资源使用相同的 lock 对象实例,由于这可能致使死锁或锁争用。具体而言,避免将如下对象用做 lock 对象:

  • this(调用方可能将其用做 lock)。
  • Type 实例(能够经过 typeof 运算符或反射获取)。
  • 字符串实例,包括字符串文本。

运行结果如图13-10所示。

 

 

13-10 使用线程同步的网络购票

13.5.3 线程安全的类型

若所在的进程中有多个线程在同时运行,而这些线程同时运行这段代码。若是每次运行结果和单线程运行的结果是同样的,并且其余的变量的值也和预期的是同样的,就是线程安全的。

一个类在被多个线程访问时,无论运行时环境执行这些线程有什么样的时序安排,它必须是以固定的、一致的顺序执行。这样的类型称为线程安全的类型。

ArrayList集合为例,在向ArrayList对象添加一个元素的时候,由两步来完成:

1)将索引值加1,即为集合扩容,确保可装入新元素。

2)在新增位置存放数据。

Add()方法是非线程安全的,若有线程A和线程B向同一个ArrayList对象中添加元素,两个线程同时得到Count值为5,以后同时执行完加1操做再赋值给Count,两个线程为同一个位置元素赋值,后一个覆盖前一个,引起数据不安全问题。说明ArrayList是非线程安全的类型。

资料

ArrayList类能够经过Synchronized()方法用来获得一个线程安全的ArrayList对象,语法以下:

ArrayList arr = ArrayList.Synchronized(new ArrayList());

另外Hashtable也有相似的方法。

.Net 4,新增System.Collections.Concurrent 命名空间中提供多个线程安全集合类,这些类提供了不少有用的方法用于访问集合中的元素,从而能够避免使用传统的锁(lock)机制等方式来处理并发访问集合。所以当有多个线程并发访问集合时,应首先考虑使用这些类代替 System.Collections System.Collections.Generic 命名空间中的对应类型。具体以下。

1.ConcurrentQueue

表示线程安全的先进先出 (FIFO) 集合。Enqueue(T) 方法用于将对象添加到 ConcurrentQueue 的结尾处。

2.ConcurrentStack

表示线程安全的后进先出 (LIFO) 集合。 ConcurrentStack 提供了几个主要操做:

  • Push 在顶部插入一个元素ConcurrentStack
  • TryPop 从顶部移除一个元素ConcurrentStack,或返回false若是不能删除该项。
  • TryPeek 返回位于顶部的元素ConcurrentStack但不会删除从ConcurrentStack
  • TryPopRangePushRange方法提供了有效推送和弹出的单个操做中的多个元素。

3.ConcurrentBag

表示对象的线程安全的无序集合。 Add(T)方法用于将对象添加到 ConcurrentBag 中。

4.ConcurrentDictionary

Dictionary相似,表示可由多个线程同时访问的键/值对的线程安全集合。 经常使用方法以下:

  • TryAdd(TKey, TValue) :尝试将指定的键和值添加到 ConcurrentDictionary 中。
  • TryGetValue(TKey, TValue) :尝试从 ConcurrentDictionary 获取与指定的键关联的值。
  • TryRemove(TKey, TValue) :尝试从 ConcurrentDictionary 中移除并返回具备指定键的值。
  • TryUpdate(TKey, TValue, TValue) :若是具备 key 的现有值等于 comparisonValue,则将与 key 关联的值更新为 newValue

经过上述提供的安全类,咱们能够方便的并发访问集合中的元素,而不须要之前的Synchronized方法或者lock(SyncRoot)等处理方式。

在多线程操做中,须要选择线程安全的类型或经过同步操做避免多个线程共享资源时引起的问题,但线程的同步也会损失性能,所以,为达到安全性和效率的平衡,可根据实际场景来选择合适的类型。

相关文章
相关标签/搜索