多线程教程--多线程基础

1 定义

线程(thread):是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

2 线程的开销

2.1 线程内核对象(thread kernel object)

OS为系统中创建的每个线程都分配并初始化包含一组对线程描述的属性和线程上下文的数据结构。对于X86,X64和ARM CUPU架构,线程上下文分别使用约700,1240和350字节的内存。

2.2 线程环境块(thread environment block, TEB)

TEB 是在用户模式(应用程序代码能快速访问的地址空间)中分配和初始化的内存块。TEB 耗用1 个内存页(86,x64 和ARM CPU 中是4KB)。 TEB 包含线程的异常处理链首(head)。线程进入的每个try 块都在链首插入一个节点(node); 线程退出try 块时从链中删除该节点。此外, TEB 还包含线程的"线程本地存储"数据,以及由GDI(Graphics Device Interface ,图形设备接口)和OpenGL 图形使用的一些数据结构。

2.3 用户模式栈(user-mode stack)

用户模式栈存储传给方法的局部变量和实参。它还包含一个地址:指出当前方法返回时,线程应该从什么地方接着执行。Windows 默认为每个线程的用户模式战分配1 MB内存。更具体地说, Windows 只是保留1MB 地址空间,在线程实际需要时才会提交(调拨)物理内存。

2.4 内核模式栈(kernel-mode stack)

应用程序代码向操作系统中的内核模式函数传通实参时,还会使用内核模式栈。出于对安全的考虑,针对从用户模式的代码传给内核的任何实参, Windows 都会把它们从线程的用户模式栈复制到线程的内核模式栈。一经复制,内核就可验证实参的值。由于应用程序代码不能访问内核模式栈,所以应用程序无法更改验证后的实参值。OS内核代码开始处理复制的值。除此之外,内核会调用它自己内部的方法,并利用内核模式栈传递它自己的实参、存储函数的局部变量以及存储返回地址。在32 位Windows上运行,内核模式技大小是12 KB; 64 位Windows 是24KB 。

2.5 DLL 线程连接(attach)和线程分离(detach)通知

Windows 的一个策略是,任何时候在进程中创建线程,都会调用进程中加载的所有非托管DLL 的DllMain 方法,并向该方法传递DLL_THREAD_ATTACH 标忐。类似地,任何时候线程终止,都会调用进程中的所有非托管DLL 的DllMaìn方法,并向方法传递DLL_THREAD_DETACH 标志。有的DLL 需要获取这些通知,才能为进程中创建/销毁的每个线程执行特殊的初始化或(资源)清理操作。例如, C-Runtime 库DLL 会分
配一些线程本地存储状态。线程使用C-Runtime 库中包含的函数时需要用到这些状态。

了解了线程开销后,下面我们来了解一下线程的工作机制。系统任何时刻只将一个线程分配给一个CPU。.操作系统通过称为 时间分片(Time slicing) 的机制来模拟多个线程并发运行。操作系统以极快的速度从一个线程切换到另一个线程,给人的感觉就是所有线程都在同时执行。处理器在切换到下一个线程之前,执行一个特定线程的时间周期称为 时间片(time slicc) 或量子( quantum ) 。在一个给定的内核中改换执行线程的动作称为 上下文切换

Windows 大约每30 毫秒执行一次上下文切换。上下文切换是净开销;也就是说,上下文切换所产生的开销不会换来任何内存或性能上的收益。

事实上,上下文切换对性能的影响可能超出你的想象。当Windows上下文切换到另一个线程时,会产生一定的性能损失。但是,CPU 现在是要执行一个不同的线程,而之前的线程的代码和数据还在CPU 的高速缓布存(cache)中,这使得CPU不必经常访问RAM(它的速度比CPU 高地缓在慢得多)。当Windows 上下文切换到新线程时,这个新线程极有可能要执行不同的代码并访问不同的数据,这些代码和数据不在CPU的高速缓存中。因此,CPU必须访问RAM来填充它的高速缓存,以恢复高速执行状态。但在30毫秒之后,一次新的上下文切换又发生了。

执行上下文切换所需的时间取决于CPU架构和速度。而填充CPU缓存所需的时间取决于系统中运行的应用程序、CPU缓存的大小以及其他各种因素。所以,无法为每一次上下文切换的时间开销给出确定值,甚至无法给出估计值。唯一确定的是,要构建高性能应用程序和组件,就应该尽量避免上下文切换。

此外,执行垃圾回收时,CLR 必须挂起(暂停)所有线程,遍历它们的栈来查找根以便对堆中的对象进行标记,再次遍历它们的栈(有的对象压缩期间发生了移动,所以要更新它们的根),再恢复所有线程。所以,减少线程的数量也会显著提升垃圾回收器的性能。每次
使用调试器并遇到断点, Windows 部会挂起正在调试的应用程序中的所有线程。并在单步执行或者运行应用程序时恢复所有线程。所以,线程越多,调试体验越差。

注意:一个时间片结束时,如果Windows决定再次调度同一个线程(而不是切换到另一个线程),那么Windows 不会执行上下文切换,线程将继续运行。这显著改进了性能。注意,在设计自己的代吗时,上下文切换能避免就要尽量避免。

3 使用线程的理由

3.1 可响应性(通常是对于客户端GUI应用程序)

Windows 为每个进程提供它自己的线程,确保发生死循环的应用程序不会妨碍其他应用程序。类似地,在自己的客户端GUI 应用程序中,可以将一些工作交给一个线程进行,使GUI 线程能灵敏地响应用户输入。在这个过程中创建的线程数可能超过CPU的核数,会浪费系统资源和损害性能。但用户得到了一个响应灵敏的UI,所以应用程序的总体使用体验增强了。

3.2 性能(对于客户端和服务器应用程序)

由于Windows 每个CPU 调度一个线程,而且多个CPU 能并发执行这些线程,所以同时执行多个操作能提升性能。当然,只有多CPU(或多核CPU)才能得到性能的提升。

4 如何设计线程数量

如果只关心性能,那么任何机器最优的线程数就是那台机器的CPU 敬门(从现在开始将CPU的每个内核都当作一个CPU)。所以,安装一个CPU的机器最好只有一个线程,安装两个CPU 的机器最好只有两个线程,以此类推。理由非常明显:如果线程数超过了CPU 的数量
就会产生上下文切换和性能损失。如果每个CPU 只有一个线程,就不会有上文切换,线程将全速运行。

然而, Microsoft 设计Windows 时,决定侧重于可靠性和响应能力,而非侧重于速度和性能。因此具有一个程序需要多少线程,要根据实际情况而定。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值