线程基础

.NET线程机制(一) 线程基础

线程作用及开销

  早期计算机一次只能运行一个程序,长时间执行程序容易出现计算机“瘫痪”的问题,如果程序进入死循环则只能重启系统。即使计算机不崩溃,也难免让用户崩溃。为了解决这个问题,操作系统设计者设计出了进程的概念,使得每个应用程序运行在一个虚拟的内存空间中。进程中又包含多个线程,CPU则根据操作系统调度执行每个进程中的线程任务。通过线程这种对CPU的虚拟化管理方式,操作系统形成了多任务执行的机制。但与一切虚拟化机制一样,线程会产生空间和时间的开销。这其中的开销包括:

1、线程内核对象。该数据结构中包含线程上下文。Windows在x86架构CPU上为每个线程内核对象分配的空间大约为700字节,x64和IA64架构CPU分别为大约1240字节和2500字节。

2、线程环境块。占用一个内存页,包含线程的异常处理链首。当线程进入try块时,在链首插入一个节点,在线程对出退出try块时,删除该节点。另外线程环境块中还包括一些其他的本地储蓄数据。

3、用户模式栈。用户存储传给方法的局部变量和实参;还包含一个地址,指出当方法返回时,线程应该从什么地方开始接着执行。默认情况下每个线程的用户模式栈分配1MB内存。

4、内核模式栈。记录用户程序调用内核模式函数时函数的实参。32位系统分配12KB内存,64位系统则分配24KB。

5、DLL线程连接和线程分离通知。进程中每创建和终止一个线程时,都会调用进程中加载的所有DLL的DllMain方法。

6、上下文切换。对单CPU计算机来说,操作系统每次只将一个线程分配给CPU执行,执行完后将线程上下文数据记录下来保存在线程内核对象结构中;然后装载另一个线程的上下文,将CPU执行控制交给此线程,如果该线程有另一个进程拥有,那么在装载该线程之前,Windows还必须使得CPU能够处理该虚拟地址空间。Windows操作系统为各个线程每次分配大概30毫秒的执行时间,称为“时间片”。上下文切换是净开销,不会换来任何在存储空间或者性能上的收益。但是能向用户提供一个健壮的能灵活相应的操作系统。

空间开销测试

测试代码:

复制代码
 1         static void Main(string[] args)
 2         {
 3             List<Thread> threadList = new List<Thread>();
 4             for (int i = 0; i < 1000; i++)
 5             {
 6                 Thread thread = new Thread(
 7                     new ParameterizedThreadStart(o => { Console.WriteLine("第{0}线程", o); Thread.Sleep(100000); })
 8                     );
 9                 threadList.Add(thread);
10             }
11             Console.ReadLine();
12             for (int i = 0; i < threadList.Count; i++)
13             {
14                 threadList[i].Start(i);
15             }
16             Console.ReadLine();
17         }
复制代码

pslist的观测结果结果:

程序开始运行时

所有线程Start以后

1000个线程Start以后虚拟物理内存占用量增加了31MB,而虚拟内存增加了1000MB之多。

线程池

  由于专用线程(实例化Thread类所创建的线程)存在的巨大内存和性能开销,CRL被设计出支持线程池技术,为应用程序提供线程管理。每个CLR独立维护一个自己的线程池,CLR在线程池中只建立必要的线程供给应用程序使用,在应用程序把多个任务分配给线程池后,CLR将任务轮流分配给线程池中线程来执行,当任务执行完毕后,线程池中的线程并不会回收,而是等待分配新的任务。这就能有效的减少线程的数量,并且减少了线程创建时的性能开销(线程池相关的内容笔者正在整理之中,后续会陆续发布)。另外,对于Thread类有一个实例属性IsBackground指示线程是前台还是后台运行,前台运行指当关闭线程所在进程时,线程不会等待执行完毕就关闭。后台线程是当进程结束运行时,线程即刻停止运行,不必等待线程运行完毕。(此处错误,感谢 圣灵石等朋友指正)前台线程指线程所在进程关闭时,进程需要等待线程执行完成才能关闭,后台进程指当进程关闭时,线程立即停止执行,不会等待执行完成既退出运行。该属性默认值为True,即专用线程默认为前台线程。

进程、AppDomain和线程

  这里涉及到进程,AppDomain和线程,我觉得有必要把这三个概念放一起做一个大体的比对。

  1、进程是操作系统为应用程序虚拟的执行地址空间,应用程序中的所有数据都装载在相互独立的进程中运行。

  2、AppDomain是.NET托管应用装载的内部相互隔离的托管执行空间。如:IIS进程中所有的Web应用都运行在独立的AppDomain中。

  3、线程是应用程序内部虚拟化的CPU执行单元,操作系统对内存中所有应用程序进程中的线程进行调度,交给CPU进行执行。

  进程是一个虚拟的地址空间,操作系统不会对其进行调度而是调度执行其中包含的线程。CLR在AppDomain内部也有自己的运行线程,AppDomain中的线程由CLR维护,但最终CLR仍需将AppDomain中的线程映射为应用程序进程中的线程,交给操作系统进行调度。并且AppDomain中的线程不一定与操作系统线程完全一一对应。

  下图是大体描述进程、AppDomain和线程的关系,以及操作系统对线程的调度。详细内容本文不做进一步说明。

线程优先级

  线程调度根据线程优先级进行,Windows将系统内的线程分为0至31,共32个等级,优先级为31的线程是最高优先级线程,最先得到执行权限。例如,一个优先级为8的线程正在执行,而此刻操作系统确认一个优先级为31的线程已经做好了执行准备,那么操作系统会立刻挂起正在执行的线程,把CPU的执行权限交给优先级为31的线程,即使优先级为8的线程还没有执行完一个完整的时间片,优先级为31的线程将获得一个完整的时间片,如果该线程执行完后操作系统发现还有优先级为31的线程准备执行,那么CPU执行权限将分给这个线程,前面优先级为8的线程始终得不到执行,这种情况称为饥饿。

  ​​如果开发人员没有合理的设置自己程序内的线程优先级,就可能会造成其他应用程序很难得到执行,甚至影响计算机响应速度。所以Windows又设计了一个进程优先级类来控制各个进程中的线程优先级的关系,进程优先级是一个虚拟的概念,因为操作系统不会对进程进行调度,这个概念只是为了控制进程中线程优先级的范围。进程优先级有6个级别(详见下表),根据进程优先级类,应用程序对内部线程设置相对优先级,会得到一个操作系统调度的线程优先级值。这样,使线程优先级能得到有效控制。

线程相对

优先级

进程优先级类

Idle

Below Normal

Normal

Above Normal

High

Real-Time

Time-critical

15

15

15

15

15

31

Highest

6

8

10

12

15

26

Above normal

5

7

9

11

14

25

Normal

4

6

8

10

13

24

Below normal

3

5

7

9

12

23

Lowest

2

4

6

8

11

22

Idle

1

1

1

1

1

16

完!

  声明:本文内容属个人理解,如有错误之处,请一定不吝指导。万分感激。。。

  时隔这么久,继续开写。本文系上一篇的补充:爬虫部分技术要点浅析

  本次将主要讨论上次提出的后两个问题:如何处理抽取出来的Url

3、如何处理抽取出来的Url(爬取策略)?

  所谓爬取策略,就是网络爬虫从种子网址开始,应该按照怎样的顺序向下爬取。以下是几种典型的爬取策略(既然是浅析,这里只做介绍,想要深入了解请自行查阅资料):

  (1)深度优先策略

    看到这个名词估计多数人立马都会明白,该策略的实现思路采用的是图的深度遍历。事实上当我们通常把网络看做一个图,网络中每个页面看做图中节点,这样在考虑遍历的问题时,很自然的就会使用图的遍历方法(其实也就是树的深度优先遍历)。看下图:

  

    按照深度优先遍历,顺序是:A-->B-->C-->D-->E-->F-->G-->H。

    这里需要说的是在多数情况下,全局的爬去策略是不会采取这种方法的,对于复杂的网路图而言,勇往直前并不见得好,而且特别是实现时使用并行处理时很难控制。

    参考:深度优先策略

  (2)广度优先策略

    这个和第一个相似,也是图的遍历方法之一(树的层次遍历),是一种非常简单直观且历史很悠久的遍历方法,在搜索引擎爬虫一出现就开始采用了。新提出的抓取策略往往会将这种方法作为比较基准,但应该注意到的是,这种策略也是一种相当强悍的方法,很多新方法实际效果不见昨比宽度优化遍历策略好,所以至今这种方法也是很多实际爬虫系统优先采用的爬取策略。

    对于上图而言,广度优先的顺序是:A-->B-->F-->C-->D-->E-->G-->H。

    根据一些研究,一般有效的网页深度不会超过10层(这里记不清了,实际应该远小于10),也就是说树高一般很小,但是每递进一层,阶段却是几何倍的增长。例如一个十分简陋的新闻门户网站,一般为:主页(一个)-->栏目列表页(十几个)-->内容页(几千至几十万)。由此考虑两种策略,很明显广度更优。而且采取广度时我们可以很轻易的在合适的层次采用并行处理,如前边可以在栏目列表页将每个栏目分配给子线程(或子爬虫程序),各自进行内容页的抓取。

  (3)非完全pagerank策略

    首先可以了解下何为pagerank:pagerank算法是Google排名运算法则(排名公式)的一部分,是Google用于用来标识网页的等级/重要性的一种方法,是Google用来衡量一个网站的好坏的唯一标准。

    既然pagerank能够很好的对url评级,很自然地可以想到用PageRank的思想来对URL优化级进行排序。但是这里有个问题,PageRank是个全局性算法,也就是说当所有网页下载完成后,其计算结果才是可靠的,而爬虫的目的就是去下载网页,在运行过程中只能看到一部分页面,所以在爬取阶段的网页是无法获得可靠的PageRank得分的。对于已经下载的网页,加上待爬取的URL队列中的一URL一起,形成网页集合,在此集合内进行PageRank计算,计算完成之后,将待爬取URL队列里的网页按照按照PageRank得分由高低排序,形成的序列就是爬虫接下来应该依次爬取的URL列表。这也是为何称之为“非完全PageRank”的原因。

    参考:百科视频讲解(hadoop的,挺不错,这集里边简单介绍了pagerank的实现)。

  (4)OPIC策略

    OPIC,即,字面含义是“在线页面重要性计算”,可以将其看做是一种改进的PageRank算法。在算法开始之前,每个互联网页面都给予相同的“现金”(cash),每当下载了某个页面P后,P将自己拥有的“现金”平均分配给页面中包含的链接页面,把自己的“现金”清空。而对于待抓取URL队列中的网页,则根据其手头拥有的现金金额多少排序,优先下载现金最充裕的网页。OCIP从大的框架上与PageRank思路基本一致,区别在于:PageRank每次需要迭代计算,而OCIP策略不需要迭代过程,所以计算速度远远快于PageRank,适合实时计算使用。同时,PageRank在计算时,存在向无链接关系网页的远程跳转过程,而OCIP没有这一计算因子。实验结果表明,OCIP是种较好的重要性衡量策略,效果略优于宽度优先遍历策略。

    这个只是摘抄,具体实现没接触过,而且感觉这样的处理更适用于推荐系统。

    参考:这就是搜索引擎 核心技术详解》

  (5)大站优先策略

    这个从字面就很容易理解,在爬去过程中会倾向于大网站的url,以网站为单位来选题网页重要性,对于待爬取URL队列中的网页根据所属网站归类,如果哪个网站等待下载的页面最多,则优化先下载这些链接,其本质思想倾向于优先下载大型网站。因为大型网站往往包含更多的页面。鉴于大型网站往往是著名企业的内容,其网页质量一般较高,所以这个思路虽然简单,但是有一定依据。实验表明这个算法效果也要略优先于宽度优先遍历策略。

    个人感觉这种策略可以和其他策略结合使用,能够达到更好的效果。

  (6)网页更新策略

     互联网的动态是其显著特征,随时都有新出现的页面,页面的内容被更改或者本来存在的页面删除。对于爬虫来说,并非将网页抓取到本地就算完成任务,也要体现出互联网这种动态性。本地下载的网页可被看做是互联网页的镜像,爬虫要尽可能保证其一致性。可以假设一种情况:某 个网页已被删除或者内容做出重大变动,而搜索引擎对此惘然无知,仍然按其旧有内容排序,将其作为搜索结果提供给用记,其用户体验度之糟糕不言而喻。所以对于已经爬取的网页,爬虫还要负责保持其内容和互联网页面内容的同步,这取决于爬虫所彩用的网页更新策略。网页更新策略的任务是要决定何时重新爬取之前已经下载过和网页,以尽可能使得本地下载网页和互联网原始页面内容保持一致。常用的网页更新策略有三种:历史参考策略,用户体验度策略和聚类抽样策略。

  说实话以上的多数策略对于一个高性能的通用爬虫至关重要,但是对于菜鸟级的入门级爬虫而言还很难很好的应用于实践。所以下边说几点入门级爬虫的爬取处理技巧:

  (1)限制域名,这对于多数垂直爬虫都应该考虑的,对于广告半边天的的互联网来说,如果你不想看到辛辛苦苦爬来的数据里边出现一大堆广告信息,这个处理是非常有效的。

  (2)加大列表页面的优先级,这根没啥好解释的,多数时候要爬取的精华都在此。

  (3)可以根据抓取测试结果对url做适当过滤,比如网站的一些子栏目不需要,可以直接将对应url前缀加入黑名单,抓取时直接跳过。

  暂时写这么多吧,感觉现在写的越来越水了,可能快毕业了有些烦躁。好好整理下思路,下次的滤重处理争取能写好。

 
 
原文地址:https://www.cnblogs.com/Leo_wl/p/3054179.html