C#线程基础知识

1. 相关概念
       进程(process):进程就是当前运行的应用程序.进程由一个或多个线程以及程序在内存中的代码、数据和其他资源组成。程序资源通常有打开的文件、信号灯和动态分配的内存。
    线程(thread):线程是操作系统分配处理器时间的基本单元.每个线程都维护异常处理程序、调度优先级和一组系统用于在调度该线程前保存线程上下文的结构。线程上下文包括为使线程在线程的宿主进程地址空间中无缝地继续执行所需的所有信息,包括线程的 CPU 寄存器组和堆栈。
    dotnet framework中的两个线程类:托管线程(System.Threading.Thread);操作系统线程(System.Diagnostics.ProcessThread).

在托管环境中,对线程的管理都是通过Thread类来完成的.

 

2. Thread & ThreadPool 的一些背景知识
       CLR 目前的多线程技术依然是 Windows 操作系统所提供的,不过 .NET CLR 开发小组似乎保留了将其分离的权利。在某些环境,CLR 线程并不会直接映射到一个 Windows 线程上,他们可能会用 Windows fiber 来代替,以期获得更好的执行性能。未来的 CLR 版本甚至会直接用某个已存在的空闲线程来代替 "new Thread()" 执行其他任务。CLR 线程使用了 Windows 线程开发的很多技巧,这样我们可以使用简单的代码来处理原本需要花费很多精力才能完成的工作。这种包装分离带来的另外一个好处就是,我们无需改动我们的代 码就能获得 CLR 和操作系统升级带来的性能提升。

除了直接使用 CLR 线程,我们还可以使用 Thread.BeginThreadAffinity() 等手段直接使用操作系统级别的线程,只不过要记得调用相关方法去 "End"。

早 期的 DOS 和 Windows 16-bit 都是单线程操作系统,这种操作系统上的某个进程一旦陷入死循环,整个操作系统都完蛋,你能做的只有重启计算机。Windows NT 3.1 是微软开发的第一个支持多线程功能的操作系统,这从某种程度上可以说是 Windows 成为 "健壮性" 操作系统的标志。在支持多线程的操作系统里,每个进程都拥有自己的线程,也就是说理论上不会陷入上述那样的尴尬状况了。死循环的线程被冻结,而其他线程依 旧能正常运转,用户也就有机会强行结束那个死掉的家伙。

严格来说,线程是一笔昂贵的开支。创建线程并不简单,首先得分配并初始化一个线程 内核对象(thead kernel object),并为这个线程保留 1MB user-mode stack 和 12KB 以上的 kernel-mode stack。在完成这些之后,线程才被创建。Windows 会发送消息通知目标进程以及其所有 DLL 线程可用。而销毁线程同样需要需要发送消息通知,最后还得释放所有的保留空间。

在单 CPU 计算机上,任何时候都只有一个线程在执行。Windows 保持线程对象状态,并决定接下来哪个线程会被执行。每个线程每次大概可以获得 20 毫秒的 CPU 执行时间片,然后切换执行另外一个线程。这个过程有个专业术语叫 "线程上下文切换(context switch)"。操作系统需要花费相当代价才能走完一次切换:

(1) 进入内核模式。
(2) 将 CPU 寄存器信息保存到当前正在执行的线程内核对象。
(3) 获取一个 Spinlock,按计划决定下一个要执行的线程,然后释放 Spinlock。如果下一个线程属于其他的进程,那么我们还得为虚拟地址交换付出更多代价。
(4) 从要被执行的线程内核对象载入 CPU 寄存器信息。
(5) 离开内核模式。

所 有这些操作可能导致操作系统和应用程序比单线程操作系统执行得更慢,但这些都是值得的,芯片生产商带来的超线程(hyper-threading)和多核 (mulit-core) CPU 为多线程提供了真正的舞台,每个内核上都可以真正并发执行一个线程。超线程 CPU 包含两个逻辑内核,每个逻辑内核都拥有自己的寄存器,只是它们需要共享 CPU 缓存等资源。当一个逻辑 CPU 因某种原因被暂停,芯片会切换到另外一个逻辑 CPU 继续执行任务,超线程芯片能带来 10% - 30% 左右的性能提升。而像 Pentium D、Athlon 64 X2 这类真正的多核 CPU 芯片,它们集成了多个真正意义上的物理内核,每个内核都有自己的完整的寄存器和缓存,这才是 100% 的性能提升。现在某些服务器用的芯片会同时使用多核和超线程技术,因此你可能在任务管理器中看到 4 个或 8 个 CPU 显示。芯片发展已经从单纯的主频提升转移到多核集成上来,不久我们就可以使用 4 核、8 核,甚至是更多更强大的多核处理器。

创建和销 毁线程代价不菲,过多的线程会消耗掉大量的内存和 CPU 资源。为了改善这种状况,CLR 提供了一种称之为 "线程池(thread pool)" 的技术。直观来说,线程池就是为应用程序提供的一堆可用线程集合,线程池在进程所有应用程序域(AppDomain)间共享。

在 CLR 初始化之初,线程池内是没有任何线程的,其内部有一个专门存储请求的队列。当应用程序试图执行异步等操作时,这些方法调用会被包装并加入到线程池队列中。 线程池从队列提取任务请求,并为其分配可用线程。如果线程池内没有可用线程,那么一个新的线程会被创建。线程完成任务执行后,并不会被摧毁。相反,它被放 回到线程池中,然后等待被分配给其他任务。线程池会尝试用同一个线程来处理应用程序的多个任务请求。而一旦应用程序在极短时间内发出多个请求,那么它会尝 试创建额外的线程来分配队列中的任务,这有可能导致池内线程数量急剧增加,同时耗费大量的系统资源。当请求完成,池内多余的线程会在空闲 2 分钟后被释放并回收相关资源,直到某个最小线程阀值设置。

当任务请求超出最小阀值设置,线程池并不会立即创建新线程,而是等待大约 500 毫秒左右。这么做的目的是看看在这段时间内是否有其他工作线程完成任务来接手这个请求,这样就可以避免创建新线程的消耗。最小线程阀值设置 (ThreadPool.SetMinThreads)最好不要小于 CPU 内核数量,否则会导致性能问题。


在线程池内部,它包含两种 类型的线程,分别是 worker thread 和 I/O threads。工作线程用来处理 compute-bound 异步操作(包括初始化 I/O-bound 操作),而 I/O 线程则用于异步执行诸如文件访问、网络通讯、数据库操作、WebService 调用以及某些硬件设备控制等。CLR 允许开发人员设置线程池的最大线程数量,并确保池内线程数量不会超出这个设置。CLR 2.0 ThreadPool 默认为每个 CPU 处理器提供 25 个工作线程以及 1000 个 I/O 线程,通常情况下这已经足够了,并不需要我们做出特别的处理。

 

 

3. 摘自MSDN

操作系统使用进程将它们正在执行的不同应用程序分开。线程是操作系统分配处理器时间的基本单元,并且该进程中可以有多个线程同时执行代码。每个线程都维护异常处理程序、调度优先级和一组系统用于在调度该线程前保存线程上下文的结构。线程上下文包括为使线程在线程的宿主进程地址空间中无缝地继续执行所需的所有信息,包括线程的 CPU 寄存器组和堆栈。

.NET Framework 将操作系统进程进一步细分为由 System.AppDomain 表示的、称为应用程序域的轻量托管子进程。一个或多个托管线程(由 System.Threading.Thread 表 示)可以在同一个非托管进程中的一个或任意数目的应用程序域中运行。虽然每个应用程序域都是用单个线程启动的,但该应用程序域中的代码可以创建附加应用程 序域和附加线程。其结果是托管线程可以在同一个非托管进程中的应用程序域之间自由移动;您可能只有一个线程在若干应用程序域之间移动。

支 持抢先多任务处理的操作系统可以创建多个进程中的多个线程同时执行的效果。它通过以下方式实现这一点:在需要处理器时间的线程之间分割可用处理器时间,并 轮流为每个线程分配处理器时间片。当前执行的线程在其时间片结束时被挂起,而另一个线程继续运行。当系统从一个线程切换到另一个线程时,它将保存被抢先的 线程的线程上下文,并重新加载线程队列中下一个线程的已保存线程上下文。

时间片的长度取决于操作系统和处理器。由于每个时间片都很小,因此即使只有一个处理器,多个线程看起来似乎是在同时执行。这实际上就是多处理器系统中发生的情形,在此类系统中,可执行线程分布在多个可用处理器中。

何时使用多个线程
需要用户交互的软件必须尽可能快地对用户的活动作出反应,以便提供丰富多彩的用户体验。但同时它必须执行必要的计算以便尽可能快地将数据呈现给用户。如果应用程序仅使用一个执行线程,则可以将异步编程与 .NET 远程处理或使用 ASP.NET 创建的 XML Web services 结合使用,在使用自己的计算机的处理时间之外还使用其他计算机的处理时间,从而提高对用户的响应速度并减少应用程序的数据处理时间。如果您正在进行大量的输入/输出工作,则还可以使用 I/O 完成端口来提高应用程序的响应速度。

多个线程的优点
无 论如何,要提高对用户的响应速度并且处理所需数据以便几乎同时完成工作,使用多个线程是一种最为强大的技术。在具有一个处理器的计算机上,多个线程可以通 过利用用户事件之间很小的时间段在后台处理数据来达到这种效果。例如,在另一个线程正在重新计算同一应用程序中的电子表格的其他部分时,用户可以编辑该电 子表格。

无需修改,同一个应用程序在具有多个处理器的计算机上运行时将极大地满足用户的需要。单个应用程序域可以使用多个线程来完成以下任务:

通过网络与 Web 服务器和数据库进行通信。
执行占用大量时间的操作。
区分具有不同优先级的任务。例如,高优先级线程管理时间关键的任务,低优先级线程执行其他任务。
使用户界面可以在将时间分配给后台任务时仍能快速作出响应。
多个线程的缺点
建议您使用尽可能少的线程,这样可以最大限度地减少操作系统资源的使用,并可提高性能。线程处理还具有在设计应用程序时要考虑的资源要求和潜在冲突。这些资源要求如下所述:

系统将为进程、AppDomain 对象和线程所需的上下文信息使用内存。因此,可以创建的进程、AppDomain 对象和线程的数目会受到可用内存的限制。
跟踪大量的线程将占用大量的处理器时间。如果线程过多,则其中大多数线程都不会产生明显的进度。如果大多数当前线程处于一个进程中,则其他进程中的线程的调度频率就会很低。
使用许多线程控制代码执行非常复杂,并可能产生许多错误。
销毁线程需要了解可能发生的问题并对那些问题进行处理。
提 供对资源的共享访问会造成冲突。为了避免冲突,必须对共享资源进行同步或控制对共享资源的访问。如果在相同或不同的应用程序域中未能正确地使访问同步,则 会导致出现一些问题,这些问题包括死锁和争用条件等,其中死锁是指两个线程都停止响应,并且都在等待对方完成;争用条件是指由于意外地出现对两个事件的执 行时间的临界依赖性而发生反常的结果。系统提供了可用于协调多个线程之间的资源共享的同步对象。减少线程的数目使同步资源更为容易。

需要同步的资源包括:

系统资源(如通信端口)。
多个进程所共享的资源(如文件句柄)。
由多个线程访问的单个应用程序域的资源(如全局、静态和实例字段)。
 4. 一个另类的例子


丰富的用户体验是所有交互式应用程序的主要方面。需要用户交互的软件必须尽可能快地响应用户的活动。与此同时,应用程序必须能够处理数据以便将结果显示给用户。

在应用程序中使用多个线程可以将用户界面 (UI) 执行的任务与后台执行的任务分开。通过此方式组织任务,可以由 UI 响应用户输入,同时由后台进程进行数据处理。

创建辅助线程
应用程序可以创建一个或多个线程以执行方法。创建辅助(或从属)线程的第一个步骤是创建 ThreadStart 代理,指定要由该线程执行的方法。然后将 ThreadStart 代理传递给 Thread 类的构造函数。例如,要启动新的线程并执行 MyFunction 方法,请调用 Thread 类的 Start 方法,如下所示:

ThreadStart starter = new ThreadStart(MyFunction);
Thread t = new Thread(starter);
t.Start();

从辅助线程更新用户界面
您可以使用 Control.Invoke 从 UI 线程以外的其他线程更新用户界面 (UI)。此方法在 UI 线程上的控件线程上下文中执行代理。.NET Framework 精简版只支持 .NET Framework 完整版中的重载 Control.Invoke 方法。Control.Invoke 只使用一个参数:一个指定在 UI 线程上执行哪个方法的代理。该代理的类型必须为 EventHandler,并且具有以下签名:

void MyFunctionName(object sender, EventArgs e)

需要注意的一点是,如果要在辅助线程中更新 UI,就必须在代码中调用 Application.DoEvents()。调用 Application.DoEvents() 可确保辅助线程激发的任何事件都由该 UI 线程处理。

下面的示例代码说明如何创建辅助线程,然后从 UI 线程和辅助线程更新名为 listBox1 的 ListBox 控件:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值