19.1 多线程编程知识

19.1.1 进程与线程的概念

进程:
  1. 可以理解为一块包含某些资源的内存区域,操作系统通过进程方式把它工作划分为不同的单元。

  2. 一个应用程序可以对应多个进程。

线程:
  1. 线程是进程中的独立执行单元,操作系统调度线程来使应用程序工作。

  2. 一个进程中至少包含一个线程,称为主线程。

进程与线程的关系
线程是进程的执行单元,操作系统通过调度线程使应用程序工作。
进程是线程的容器,由操作系统创建,由在具体的执行过程中创建线程。

19.1.2 线程的调度

生活中吃饭的时候看电视,你需要来回切换这两个动作,他们由你来进行调度的。计算机里,线程相当于你的动作,操作系统相当于你,操作系统需要调度线程使他们轮流工作。
Windows是抢占式多线程系统。因为线程可以在任意事件里被抢占,来调度另一个线程。操作系统为每个线程分配了0~31某一优先级,优先级高的优先分配给CPU。
windows 支持7个相对线程优先级,Idle、Lowest、BelowNormal、Normal、AboveNormal、Highest、Time-Critical 。程序可以设置 Thread 的 Priority 属性来改变线程的优先级,该属性为 ThreadPriority 枚举类型,成员包含了 Lowest、BelowNormal、Normal、AboveNormal、Highest,CLR 为自己保留了 Idle、Time-Critical 

1、Time-critical:关键时间(最高的相对线程优先级)

2、Heightest:最高(翻译是这么翻译,但是并不是最高的相对线程优先级)

3、Above normal:高于标准

4、Normal:标准

5、Below normal:低于标准

6、Lowest:最低(翻译是这么翻译,但是并不是最低的相对线程优先级)

7、Idle:空闲


19.1.3 线程也分前后台

前台线程:只有所有的前台线程都关闭才能完成程序关闭。(主线程一直是前台线程)
后台线程:只要所有的前台线程都结束,后台线程自动结束(CLR会强制结束所有仍运行的后台线程,却不会抛出异常)。
  1. class Program

  2. {

  3. static void Main(string[] args)

  4. {

  5. Thread backThread = new Thread(Worker);

  6. backThread.IsBackground = true;

  7. backThread.Start();

  8. Console.WriteLine( "从主线程中退出");

  9. }

  10. public static void Worker()

  11. {

  12. Thread.Sleep(1000);

  13. Console.WriteLine("从后台线程退出");

  14. }

  15. }

CLRT + F5 (不调试)效果图

上面代码通过 Thread 创建一个线程对象,设置 IsBackground 属性指明线程为后台线程。不设置 IsBackground  属性,则所创建的线程默认为前台线程。
接着,调用 Start 函数启动该线程,此时后台线程会执行 Worker 函数代码。
从前面分析中看出,该控制台有两个线程,一个运行 Main 函数的主线程,另一个运行 Worker 函数的后台线程。由于前台执行完毕后 CLR 会无条件终止后台线程运行,所以前面代码中若启动后台线程,则主线程将会继续运行。
主线程运行完  Console.WriteLine( "从主线程中退出") 语句就会退出。此时, CLR 发现主线程运行结束,则会终止后台线程,然后整个程序结束运行。所以 Worker 函数中的 Console.WriteLine("从后台线程退出") 语句将不会执行。

通过分析,按 CLRT + F5 运行程序,你将会看到如图效果。如果按 F5 你将会看到输出结果一闪而过,因为主线程退出后,整个应用程序也跟着退出,就关闭了控制台程序。

分析执行后台线程的方法

1.所创建线程默认为非后台线程,所以注释掉 //backThread.IsBackground = true;(这时候不是后台线程了)



2.使主线程在后台线程执行完毕后再执行,即使主线程进入睡眠,且睡眠时间比后台线程长。

 

3.通过调用主函数的 Join 函数方法,确保主线程会在后台线程执行结束后开始运行。

 
以上代码调用 backThread.Start() 确保主线程会在后台线程结束后再运行。这种方式涉及
线程同步的概念:在某些情况下,需要两个线程同步运行。即一个线程必须等待另外一个线程结束之后才能运行
        //     System.Threading.ParameterizedThreadStart 委托,它表示此线程开始执行时要调用的方法。
        public Thread(ParameterizedThreadStart start);
        //     System.Threading.ThreadStart 委托,它表示此线程开始执行时要调用的方法。
        public Thread(ThreadStart start);

public Thread(ParameterizedThreadStart start, int maxStackSize);  

 ParameterizedThreadStart 与 ThreadStart 的区别:ParameterizedThreadStart 可以有参数,ThreadStart 没有参数。本实例就是创建的ThreadStart。


19.2 线程的容器——线程池

通过 Thread 类手动创建线程的创建和销毁会耗费大量时间,这样的手动操作将造成性能的损失。因此,.NET引入了线程池机制。

19.2.1 线程池

  • 线程池是一个存放线程的地方,这种集中存放有利于线程的管理。

  • CLR初始化时,线程池中没有线程。(线程池内部维护了一个操作请求队列).

  • 执行异步操作时,需要调用 QueueUserWorkItem 方法将任务添加到线程池队列。线程池实现的代码会从队列中提取任务,并委派给线程池中的线程执行。

    1. 没有空闲的线程,线程池会创建一个新线程去执行提取的任务。

    2. 当线程池完成了某个任务,线程不会销毁,而是返回到线程池中,等待下一个请求。(由于不销毁,所以不会产生性能损失)

  • 线程池创建的是后台线程,优先级是 Normal 。

19.2.2 通过线程池来实现多线程

使用线程池的线程,要调用静态方法 ThreadPool.QueueUserWorkItem ,以指定线程要调用的方法。该静态方法有两种:

public static bool QueueUserWorkItem(WaitCallback callBack);

public static bool QueueUserWorkItem(WaitCallback callBack, object state);

  • 这两个方法用于向线程池队列添加一个工作项(work item)以及一个可选的状态数据。

  • 工作项是指一个由 callback 参数标识的委托对象,被委托对象包装的回调方法由线程池来执行。

  • 传入的回调方法匹配 System.Threading.WaitCallback 委托类型

  1. static void Main(string[] args)

  2. {

  3. Console.WriteLine("主线程ID={0}",Thread.CurrentThread.ManagedThreadId);

  4. ThreadPool.QueueUserWorkItem(CallbackWorkItem);

  5. ThreadPool.QueueUserWorkItem(CallbackWorkItem, "work");

  6. Thread.Sleep(3000);

  7. Console.WriteLine("主线程退出");

  8. }

  9. private static void CallbackWorkItem(object state)

  10. {

  11. Console.WriteLine("线程池开始执行");

  12. if (state !=null)

  13. {

  14. Console.WriteLine("线程池线程ID = {0} 传入的参数为 {1}",Thread.CurrentThread.ManagedThreadId,state.ToString());

  15. }

  16. else

  17. {

  18. Console.WriteLine("线程池线程ID ={0}",Thread.CurrentThread.ManagedThreadId);

  19. }

  20. }

19.2.3 协作式取消线程池线程

.NET Framework 提供了取消操作的模式,这个模式是协作式的。为了取消操作,我们必须创建一个 System.Threading.CancellationTokenSource 对象。

  1. static void Main(string[] args)

  2. {

  3. Console.WriteLine("主线程运行");

  4. CancellationTokenSource cts = new CancellationTokenSource();

  5. ThreadPool.QueueUserWorkItem(callback, cts.Token);

  6. Console.WriteLine("按下回车键来取消操作");

  7. Console.Read();

  8. cts.Cancel();//取消请求

  9. Console.ReadKey();

  10. }

  11. private static void callback(object state)

  12. {

  13. CancellationToken token = (CancellationToken) state;

  14. Console.WriteLine("开始计数");

  15. Count(token,1000);//开始计数

  16. }

  17. private static void Count(CancellationToken token, int countto)

  18. {

  19. for (int i = 0; i < countto; i++)

  20. {

  21. if (token.IsCancellationRequested)

  22. {

  23. Console.WriteLine("计数取消");

  24. return;

  25. }

  26. Console.WriteLine("计数为:" + i);

  27. Thread.Sleep(300);

  28. }

  29. Console.WriteLine("计数完成");

  30. }

首先创建一个 CancellationTokenSource 实例,将该实例作为参数传入QueueUserWorkItem 方法。线程池会创建一个线程池线程,运行该方法传入的回调函数 callback  ,并在 callback 中执行 Count 函数来计数。

在 Count 函数中检查 CancellationTokenSource 类实例的状态。当用户按回车键时,该实例的 IsCancellationRequested 属性将返回 ture 。因此退出 Count 方法。否则一直运行。

19.3 线程同步

多线程中,为了保证后者线程,只有等待前者线程完成之后才能继续执行。好比排队买票,前面的人没买票之前,后面的人必须等待。

19.3.1 多线程程序中存在的隐患

多线程可以提高程序的性能和用户体验。然而当我们创建多个线程后,它们可能同时访问某一个共享资源,这将损坏资源中所保存的数据。这时候我们需要使用线程同步,确保某一时刻只有一个线程在操作共享资源。

举例来说,火车票销售系统允许多人同时购买,因此该系统肯定采用了多线程技术。但由于系统中有多个线程在对同一资源(火车票)进行操作,我们必须确保只有其他线程执行结束后,新的线程才开始执行。这样可以避免多位顾客买到同一张票。

  1. private static int tickets = 100;//100张票

  2. static void Main(string[] args)

  3. {

  4. Thread thread1 = new Thread(SaleTicketThread1);

  5. Thread thread2 = new Thread(SaleTicketThread2);

  6. thread1.Start();

  7. thread2.Start();

  8. Thread.Sleep(4000);

  9. }

  10. private static void SaleTicketThread1()

  11. {

  12. while (true)

  13. {

  14. if (tickets>0)

  15. {

  16. Console.WriteLine("线程1出票:"+tickets--);

  17. }

  18. else

  19. break;

  20. }

  21. }

  22. private static void SaleTicketThread2()

  23. {

  24. while (true)

  25. {

  26. if (tickets > 0)

  27. {

  28. Console.WriteLine("线程2出票:" + tickets--);

  29. }

  30. else

  31. break;

  32. }

  33. }

线程1从100张票开始出售,执行一段时间后,线程2开始执行,执行一段时间后,线程1又继续执行,两个线程交替,直至售完100张票。


线程1和线程2在售票时,火车票号码不连续,说明以上程序售票过程不正确,这就是多线程所存在的问题,因为两个线程访问同一个全局静态变量——tickets。

19.3.2 使用监视器对象实现线程同步

监视器对象(Monitor)能够确保线程拥有对共享资源的互斥访问权,C# 通过 lock 关键字来提供简化语法。

  1. private static int tickets = 100;//100张票

  2. static  object gloalObj =new object();//辅助对象

  3. static void Main(string[] args)

  4. {

  5. Thread thread1 = new Thread(SaleTicketThread1);

  6. Thread thread2 = new Thread(SaleTicketThread2);

  7. thread1.Start();

  8. thread2.Start();

  9. Console.ReadKey();

  10. }

  11. private static void SaleTicketThread1()

  12. {

  13. while (true)

  14. {

  15. try

  16. {

  17. Monitor.Enter(gloalObj); //在object对象上获得排它锁

  18. Thread.Sleep(1);

  19. if (tickets > 0)

  20. {

  21. Console.WriteLine("线程1出票:" + tickets--);

  22. }

  23. else

  24. break;

  25. }

  26. finally

  27. {

  28. Monitor.Exit(gloalObj);//释放指定对象的排它锁

  29. }

  30. }

  31. }

  32. private static void SaleTicketThread2()

  33. {

  34. while (true)

  35. {

  36. try

  37. {

  38. Monitor.Enter(gloalObj); //在object对象上获得排它锁

  39. Thread.Sleep(1);

  40. if (tickets > 0)

  41. {

  42. Console.WriteLine("线程2出票:" + tickets--);

  43. }

  44. else

  45. break;

  46. }

  47. finally

  48. {

  49. Monitor.Exit(gloalObj);//释放指定对象的排它锁

  50. }

  51. }

  52. }

使用 Monitor 锁定的对象需要引用类型,而不是值类型。

因为值类型变量传递给方法时,它将被装箱为一个单独的对象,之后传递给 Enter 方法;

而在将变量传递给 Exit 方法时,也会创建一个单独的引用对象。此时,传递给 Enter 方法的对象和传递给 Exit 方法的对象不同, Monitor 将会引发 SynchronizationLockException 异常。

使用 try.....finally 语句执行 Monitor ,如果不使用,程序会出现“死锁现象”;

 原因:售出最后一张票后,tickets变量为0,若正在执行的是线程1,此时它们将进入 else 语句块执行 break 语句,线程1直接退出,却没有执行 Monitor.Exit 语句。所以线程2在Monitor.Enter 代码处一直等待,导致线程2不能退出。线程2又是前台线程,使整个程序也不能退出,这时发生“死锁现象”。

19.3.3 线程同步技术存在的问题

  • 它的使用比较繁琐。要用额外的代码把被多个线程同时访问的数据保卫起来,并获取和释放线程的同步锁。如果一个代码块忘记获取锁,就可能造成数据损坏。

  • 使用线程同步会影响程序性能。因为获取和释放同步锁需要时间,并且决定哪个线程先获取锁,CPU也必须进行协调。

  • 线程同步每次只允许一个线程访问资源,会导致线程阻塞。继而,系统会创建更多的线程,CPU也就负担更繁琐的调度工作。

  1. static void Main(string[] args)

  2. {

  3. int x = 0;

  4. const int iterationNumber = 5000000;

  5. Stopwatch sw = Stopwatch.StartNew();

  6. for (int i = 0; i < iterationNumber; i++)

  7. {

  8. x++;

  9. }

  10. Console.WriteLine("不使用锁的情况下话费的时间:{0} ms", sw.ElapsedMilliseconds);

  11. sw.Restart();

  12. for (int i = 0; i < iterationNumber; i++)  //使用锁的情况

  13. {

  14. Interlocked.Increment(ref x);

  15. }

  16. Console.WriteLine("使用锁的情况下话费的时间:{0} ms", sw.ElapsedMilliseconds);

  17. Console.Read();

  18. }

调用 StartNew 函数对 Stopwatch 实例化,将运行时间设为0,开始测量时间。


《learning hard C#学习笔记》读书笔记(19)多线程的更多相关文章

  1. 《Hands-On Machine Learning with Scikit-Learn&TensorFlow》读书笔记

    一 机器学习概览 机器学习的广义概念是:机器学习是让计算机具有学习的能力,无需进行明确编程. 机器学习的工程性概念是:计算机程序利用经验E学习任务T,性能是P,如果针对任务T的性能P随着经验E不断增长 ...

  2. 鸟哥Linux私房菜 基础学习篇读书笔记(10):Linux磁盘和文件系统管理(3)

    本文总结了Linux操作系统来管理我们的硬盘和文件系统需要使用命令.当我们在系统中增加一个硬盘驱动器.什么是我们需要去通过这个硬盘就可以真正使用步骤?下面步骤: (1)对磁盘进行分区,新建能够使用的分 ...

  3. 鸟哥的Linux私房菜 基础学习篇读书笔记(9):Linux磁盘与文件系统管理(2)

    上一篇文章主要从理论上分析了Linux的Ext2文件系统.这一篇主要解说怎样查看Linux的文件系统的容量以及解说Linux文件系统中的连接文件. 能够通过df和du命令来查看磁盘与文件夹的容量.df ...

  4. 人体和电脑的关系——鸟哥的LINUX私房菜基础学习篇读书笔记

    CUP=脑袋: 每个人会做的事情都不一样(指令集的差异),但主要都是通过脑袋来判断与控制身体各部分的行动 内存=脑袋中存放正在思考的数据区块: 在实际活动过程中,我们的脑袋需要有外界刺激的数据(例如光 ...

  5. 人生效率手册:如何卓有成效地过好每一天--By张萌姐姐--读书笔记

    读书笔记:<人生效率手册>:如何卓有成效地过好每一天--By张萌姐姐... 整本书看完的感受: 这本书主要讲的是生活中我们需要给自己一个目标,然后通过自己的努力去实现这个目标,书中说的很多 ...

  6. 强化学习读书笔记 - 06~07 - 时序差分学习(Temporal-Difference Learning)

    强化学习读书笔记 - 06~07 - 时序差分学习(Temporal-Difference Learning) 学习笔记: Reinforcement Learning: An Introductio ...

  7. 【Deep Learning读书笔记】深度学习中的概率论

    本文首发自公众号:RAIS,期待你的关注. 前言 本系列文章为 <Deep Learning> 读书笔记,可以参看原书一起阅读,效果更佳. 概率论 机器学习中,往往需要大量处理不确定量,或 ...

  8. Deep Learning(深度学习)学习笔记整理系列之(八)

    Deep Learning(深度学习)学习笔记整理系列 zouxy09@qq.com http://blog.csdn.net/zouxy09 作者:Zouxy version 1.0 2013-04 ...

  9. 强化学习读书笔记 - 02 - 多臂老O虎O机问题

    # 强化学习读书笔记 - 02 - 多臂老O虎O机问题 学习笔记: [Reinforcement Learning: An Introduction, Richard S. Sutton and An ...

  10. 强化学习读书笔记 - 05 - 蒙特卡洛方法(Monte Carlo Methods)

    强化学习读书笔记 - 05 - 蒙特卡洛方法(Monte Carlo Methods) 学习笔记: Reinforcement Learning: An Introduction, Richard S ...

随机推荐

  1. 【转】真正从零开始,TensorFlow详细安装入门图文教程!(帮你完成那个最难的从0到1)

    AI这个概念好像突然就火起来了,年初大比分战胜李世石的AlphaGo成功的吸引了大量的关注,但其实看看你的手机上的语音助手,相机上的人脸识别,今日头条上帮你自动筛选出来的新闻,还有各大音乐软件的歌曲& ...

  2. SQL 归来

    1. PL/SQL 转义 select order#, ………  from **** select col1 from A where col2 like '%\_keywors%' escape ' ...

  3. php-fpm优化方法详解

    php-fpm优化方法 php-fpm存在两种方式,一种是直接开启指定数量的php-fpm进程,不再增加或者减少:另一种则是开始时开启一定数量的php-fpm进程,当请求量变大时,动态的增加php-f ...

  4. 关于tkCommand的各种事件的解释

    superclass for callback/observer methods vtkCommand is an implementation of the observer/command des ...

  5. javascript数组array

    注意:1.array的length不是只读的.可以从数组的末尾移出项或者向数组中添加新项.看下面例子: var colors = ["red","yellow" ...

  6. js构造函数的方法与原型prototype

    把方法写在构造函数内的情况我们简称为函数内方法,把方法写在prototype属性上的情况我们简称为prototype上的方法 函数内的方法: 使用函数内的方法我们可以访问到函数内部的私有变量,如果我们 ...

  7. 虚函数的使用 以及虚函数与重载的关系, 空虚函数的作用,纯虚函数->抽象类,基类虚析构函数使释放对象更彻底

    为了访问公有派生类的特定成员,可以通过讲基类指针显示转换为派生类指针. 也可以将基类的非静态成员函数定义为虚函数(在函数前加上virtual) #include<iostream> usi ...

  8. linux系统内核流转浅析

    SJTUBEAR  原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 我们通过简单 ...

  9. 常用shell 命令整理 一 进程 cpu

    1.查看内存从大到小排列 ps -e -o "%C : %p : %z : %a"|sort -k5 -nr 分析: -e 显示进程 -o 按用户自定义格式显示 %C cpu %p ...

  10. eclipse常用快捷键

    1. ctrl+shift+r:打开资源 这可能是所有快捷键组合中最省时间的了.这组快捷键可以让你打开你的工作区中任何一个文件,而你只需要按下文件名或mask名中的前几个字母,比如applic*.xm ...