一、基本概念
进程(Process)是Windows系统中的一个基本概念,它包含着一个运行程序所需要的资源。一个正在运行的应用程序在操作系统中被视为一个进程,进程可以包括一个或多个线程。线程是操作系统分配处理器时间的基本单元,在进程中可以有多个线程同时执行代码。进程之间是相对独立的,一个进程无法访问另一个进程的数据(除非利用分布式计算方式),一个进程运行的失败也不会影响其他进程的运行,Windows系统就是利用进程把工作划分为多个独立的区域的。进程可以理解为一个程序的基本边界。是应用程序的一个运行例程,是应用程序的一次动态执行过程。
应用程序域(AppDomain)是一个程序运行的逻辑区域,它可以视为一个轻量级的进程,.NET的程序集正是在应用程序域中运行的,一个进程可以包含有多个应用程序域,一个应用程序域也可以包含多个程序集。在一个应用程序域中包含了一个或多个上下文context,使用上下文CLR就能够把某些特殊对象的状态放置在不同容器当中。
线程(Thread)*是进程中的基本执行单元,是操作系统分配CPU时间的基本单位,一个进程可以包含若干个线程,在进程入口执行的第一个线程被视为这个进程的主线程*。在.NET应用程序中,都是以Main()方法作为入口的,当调用此方法时系统就会自动创建一个主线程。线程主要是由CPU寄存器、调用栈和线程本地存储器(Thread Local Storage,TLS)组成的。CPU寄存器主要记录当前所执行线程的状态,调用栈主要用于维护线程所调用到的内存与数据,TLS主要用于存放线程的状态信息。
进程、应用程序域、线程的关系如下图,一个进程内可以包括多个应用程序域,也有包括多个线程,线程也可以穿梭于多个应用程序域当中。但在同一个时刻,线程只会处于一个应用程序域内。
二、多线程基础
多线程指的是进程同时有多个线程活动。这可以通过时间片的线程模拟或是多cpu上的超线程来实现。可以提高性能。
当启动一个可执行程序时,将创建一个主线程。在默认的情况下,C#程序具有一个线程,此线程执行程序中以Main方法开始和结束的代码,Main()方法直接或间接执行的每一个命令都有默认线程(主线程)执行,当Main()方法返回时此线程也将终止。
2.1 多线程的优缺点
多线程的优点:
1)可以同时完成多个任务;
2)可以使程序的响应速度更快;
3)可以让占用大量处理时间的任务或当前没有进行处理的任务定期将处理时间让给别的任务;可以随时停止任务;4)可以设置每个任务的优先级以优化程序性能。
然而,多线程虽然有很多优点,但是也必须认识到多线程可能存在影响系统性能的不利方面,才能正确使用线程。
多线程的缺点:
1)线程也是程序,所以线程需要占用内存,线程越多,占用内存也越多。
2)多线程需要协调和管理,所以需要占用CPU时间以便跟踪线程。
3)线程之间对共享资源的访问会相互影响,必须解决争用共享资源的问题。
4)线程太多会导致控制太复杂,最终可能造成很多程序缺陷。
2.2 System.Threading.Thread类
Thread类是是控制线程的基础类,位于System.Threading命名空间下,具有4个重载的构造函数:
名称 | 说明 |
---|---|
Thread(ParameterizedThreadStart) | 初始化 Thread 类的新实例,指定允许对象在线程启动时传递给线程的委托。要执行的方法是有参的。 |
Thread(ParameterizedThreadStart,?Int32) | 初始化 Thread 类的新实例,指定允许对象在线程启动时传递给线程的委托,并指定线程的最大堆栈大小 |
Thread(ThreadStart) | 初始化 Thread 类的新实例。要执行的方法是无参的。 |
Thread(ThreadStart,?Int32) | 初始化 Thread 类的新实例,指定线程的最大堆栈大小。 |
2.3 线程的常用属性
属性名称 | 说明 |
---|---|
CurrentContext | 获取线程正在其中执行的当前上下文 |
CurrentThread | 获取当前正在运行的线程 |
ExecutionContext | 获取一个 ExecutionContext 对象,该对象包含有关当前线程的各种上下文的信息。 |
IsAlive | 获取一个值,该值指示当前线程的执行状态 |
IsBackground | 获取或设置一个值,该值指示某个线程是否为后台线程 |
IsThreadPoolThread | 获取一个值,该值指示线程是否属于托管线程池 |
ManagedThreadId | 获取当前托管线程的唯一标识符,程序在大部分情况下都是通过Thread.ManagedThreadId来辨别线程的。而Name是一个可变值,在默认时候,Name为一个空值 Null,开发人员可以通过程序设置线程的名称,但这只是一个辅助功能。 |
Name | 获取或设置线程的名称 |
Priority | 获取或设置一个值,该值指示线程的调度优先级 |
ThreadState | 获取一个值,该值包含当前线程的状态 |
2.4 线程的优先级别
当线程之间争夺CPU时间时,CPU按照线程的优先级给予服务。高优先级的线程可以完全阻止低优先级的线程执行。.NET为线程设置了Priority属性来定义线程执行的优先级别,里面包含5个选项,其中Normal是默认值。除非系统有特殊要求,否则不应该随便设置线程的优先级别。
成员名称 | 说明 |
---|---|
Lowest | 可以将 Thread 安排在具有任何其他优先级的线程之后 |
BelowNormal | 可以将 Thread 安排在具有 Normal 优先级的线程之后,在具有 Lowest 优先级的线程之前 |
Normal | 默认选择。可以将 Thread 安排在具有 AboveNormal 优先级的线程之后,在具有 BelowNormal 优先级的线程之前 |
AboveNormal | 可以将 Thread 安排在具有 Highest 优先级的线程之后,在具有 Normal 优先级的线程之前 |
Highest | 可以将 Thread 安排在具有任何其他优先级的线程之前 |
2.5 线程的状态
通过ThreadState可以检测线程是处于Unstarted、Sleeping、Running 等等状态,它比 IsAlive 属性能提供更多的特定信息。前面说过,一个应用程序域中可能包括多个上下文,而通过CurrentContext可以获取线程当前的上下文。
CurrentThread是最常用的一个属性,它是用于获取当前运行的线程。
2.6 System.Threading.Thread的方法
Thread 中包括了多个方法来控制线程的创建、挂起、停止、销毁,以后来的例子中会经常使用。
方法名称 | 说明 |
---|---|
Abort() | 终止本线程 |
GetDomain() | 返回当前线程正在其中运行的当前域 |
GetDomainId() | 返回当前线程正在其中运行的当前域Id |
Interrupt() | 中断处于 WaitSleepJoin 线程状态的线程 |
Join() | 已重载。 阻塞调用线程,直到某个线程终止时为止 |
Resume() | 继续运行已挂起的线程 |
Start() | 执行本线程 |
Suspend() | 挂起当前线程,如果当前线程已属于挂起状态则此不起作用 |
Sleep() | 把正在运行的线程挂起一段时间 |
2.7 System.Threading 命名空间
在System.Threading命名空间内提供多个方法来构建多线程应用程序,其中ThreadPool与Thread是多线程开发中最常用到的,在.NET中专门设定了一个CLR线程池专门用于管理线程的运行,这个CLR线程池正是通过ThreadPool类来管理。而Thread是管理线程的最直接方式,下面几节将详细介绍有关内容。
类 | 说明 |
---|---|
AutoResetEvent | 通知正在等待的线程已发生事件。无法继承此类 |
ExecutionContext | 管理当前线程的执行上下文。无法继承此类 |
Interlocked | 为多个线程共享的变量提供原子操作 |
Monitor | 提供同步对对象的访问的机制 |
Mutex | 一个同步基元,也可用于进程间同步 |
Thread | 创建并控制线程,设置其优先级并获取其状态 |
ThreadAbortException | 在对 Abort 方法进行调用时引发的异常。无法继承此类 |
ThreadPool | 提供一个线程池,该线程池可用于发送工作项、处理异步 I/O、代表其他线程等待以及处理计时器 |
Timeout | 包含用于指定无限长的时间的常数。无法继承此类 |
Timer | 提供以指定的时间间隔执行方法的机制。无法继承此类 |
WaitHandle | 封装等待对共享资源的独占访问的操作系统特定的对象 |
在System.Threading中的包含了下表中的多个常用委托,其中ThreadStart、ParameterizedThreadStart是最常用到的委托。
由ThreadStart生成的线程是最直接的方式,但由ThreadStart所生成并不受线程池管理。
而ParameterizedThreadStart是为异步触发带参数的方法而设的。
委托 | 说明 |
---|---|
ContextCallback | 表示要在新上下文中调用的方法 |
ParameterizedThreadStart | 表示在 Thread 上执行的方法 |
ThreadExceptionEventHandler | 表示将要处理 Application 的 ThreadException 事件的方法 |
ThreadStart | 表示在 Thread 上执行的方法 |
TimerCallback | 表示处理来自 Timer 的调用的方法 |
WaitCallback | 表示线程池线程要执行的回调方法 |
WaitOrTimerCallback | 表示当 WaitHandle 超时或终止时要调用的方法 |
三、线程操作
3.1 创建线程
一个进程可以创建一个或多个线程以执行与该进程关联的部分程序代码。
在C#中,线程是使用Thread类处理的,该类在System.Threading命名空间中。使用Thread类创建线程时,只需要提供线程入口,线程入口告诉程序让这个线程做什么。通过实例化一个Thread类的对象就可以创建一个线程。创建新的Thread对象时,将创建新的托管线程。Thread类接收一个ThreadStart委托或ParameterizedThreadStart委托的构造函数,该委托包装了调用Start方法时由新线程调用的方法。
ThreadStart 委托中作为参数的方法不需要参数,并且没有返回值。
ParameterizedThreadStart 委托一个对象作为参数,必须为object类型,利用这个参数可以很方便地向线程传递参数。
示例代码如下:
static void Main(string[] args)
{
//创建无参数的委托线程
Thread t1 = new Thread(new ThreadStart(PrintNumbers));
//启动线程
t1.Start();
Thread t2 = new Thread(new ParameterizedThreadStart(PrintNumbers));//有参数的委托
t2.Start(10);
Console.ReadLine();
}
static void PrintNumbers()
{
Console.WriteLine("Starting...");
for (int i = 0; i < 10; i++)
{
Console.WriteLine(i);
}
}
//注意:要使用ParameterizedThreadStart,定义的参数必须为object
static void PrintNumbers(object count)
{
Console.WriteLine("Starting...");
for (int i = 0; i < Convert.ToInt32(count); i++)
{
Console.WriteLine(i);
}
}
上面代码实例化了一个Thread对象,并指明将要调用的方法method(),然后启动线程。
创建多线程的步骤:
1)编写线程所要执行的方法
2)实例化Thread类,并传入一个指向线程所要执行方法的委托。(这时线程已经产生,但还没有运行)
3)调用Thread实例的Start方法,标记该线程可以被CPU执行了,但具体执行时间由CPU决定
如果为了简单,也可以通过匿名委托或Lambda表达式来为Thread的构造方法赋值。
使用lambda表达式引用另一个C#对方的方式被称为闭包。当在lambda表达式中使用任何局部变量时,C#会生成一个类,并将该变量作为该类的一个属性,但是我们无须定义该类,C#编译器会自动帮我们实现。
static void Main(string[] args)
{
//通过匿名委托创建
Thread thread1 = new Thread(delegate() { Console.WriteLine("我是通过匿名委托创建的线程"); });
thread1.Start();
//通过Lambda表达式创建
Thread thread2 = new Thread(() => Console.WriteLine("我是通过Lambda表达式创建的委托"));
thread2.Start();
Console.ReadKey();
}
3.2 前台线程和后台线程
前台线程:只有所有的前台线程都结束,应用程序才能结束。默认情况下创建的线程都是前台线程
后台线程:只要所有的前台线程结束,后台线程自动结束。通过Thread.IsBackground设置后台线程。
必须在调用Start方法之前设置线程的类型,否则一旦线程运行,将无法改变其类型。
通过BeginXXX方法运行的线程都是后台线程。
后台线程一般用于处理不重要的事情,应用程序结束时,后台线程是否执行完成对整个应用程序没有影响。如果要执行的事情很重要,需要将线程设置为前台线程。
static void Main(string[] args)
{
//创建前台线程
Thread fThread = new Thread(delegate() {
Console.WriteLine("我是通过匿名委托创建的前台线程");
});
//给线程命名
fThread.Name = "前台线程";
//创建后台线程
Thread bThread = new Thread(delegate() {
Console.WriteLine("我是通过匿名委托创建的后台线程");
});
bThread.Name = "后台线程";
//设置为后台线程
bThread.IsBackground = true;
//启动线程
fThread.Start();
bThread.Start();
}
3.3 挂起线程
为了等待其他后台线程完成后再结束主线程,就可以使用Thread.Sleep()方法。
public class Message
{
public void ShowMessage()
{
string message = string.Format("\nAsync threadId is:{0}",
Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(message);
for (int n = 0; n < 10; n++)
{
Thread.Sleep(300);
Console.WriteLine("The number is:" + n.ToString());
}
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Main threadId is:"+
Thread.CurrentThread.ManagedThreadId);
Message message=new Message();
Thread thread = new Thread(new ThreadStart(message.ShowMessage));
thread.IsBackground = true;
thread.Start();
Console.WriteLine("Do something ..........!");
Console.WriteLine("Main thread working is complete!");
Console.WriteLine("Main thread sleep!");
Thread.Sleep(5000);
}
}
如果系统无法预知异步线程需要运行的时间,所以用通过Thread.Sleep(int)阻塞主线程并不是一个好的解决方法。有见及此,.NET专门为等待异步线程完成开发了另一个方法thread.Join()。把上面例子中的最后一行Thread.Sleep(5000)修改为 thread.Join() 就能保证主线程在异步线程thread运行结束后才会终止。
3.4 Suspend 与 Resume (慎用)
Thread.Suspend()与 Thread.Resume()是在Framework1.0 就已经存在的老方法了,它们分别可以挂起、恢复线程。但在Framework2.0中就已经明确排斥这两个方法。这是因为一旦某个线程占用了已有的资源,再使用Suspend()使线程长期处于挂起状态,当在其他线程调用这些资源的时候就会引起死锁!所以在没有必要的情况下应该避免使用这两个方法。
3.5 终止线程
若想终止正在运行的线程,可以使用Abort()方法。在使用Abort()的时候,将引发一个特殊异常 ThreadAbortException 。
若想在线程终止前恢复线程的执行,可以在捕获异常后 ,在catch(ThreadAbortException ex){…} 中调用Thread.ResetAbort()取消终止。
而使用Thread.Join()可以保证应用程序域等待异步线程结束后才终止运行。
static void Main(string[] args)
{
Console.WriteLine("Main threadId is:" +
Thread.CurrentThread.ManagedThreadId);
Thread thread = new Thread(new ThreadStart(AsyncThread));
thread.IsBackground = true;
thread.Start();
thread.Join();
}
//以异步方式调用
static void AsyncThread()
{
try
{
string message = string.Format("\nAsync threadId is:{0}",
Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(message);
for (int n = 0; n < 10; n++)
{
//当n等于4时,终止线程
if (n >= 4)
{
Thread.CurrentThread.Abort(n);
}
Thread.Sleep(300);
Console.WriteLine("The number is:" + n.ToString());
}
}
catch (ThreadAbortException ex)
{
//输出终止线程时n的值
if (ex.ExceptionState != null)
Console.WriteLine(string.Format("Thread abort when the number is: {0}!",
ex.ExceptionState.ToString()));
//取消终止,继续执行线程
Thread.ResetAbort();
Console.WriteLine("Thread ResetAbort!");
}
//线程结束
Console.WriteLine("Thread Close!");
}
3.6 调整线程的优先级
static void Main(string[] args)
{
TestThreadPriority();
Console.ReadLine();
}
private static void TestThreadPriority()
{
Console.WriteLine($"Current thread priority: {Thread.CurrentThread.Priority}");
Console.WriteLine("Running on all cores available");//获取实例线程状态
RunThreads();
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine("Running on a single Core");
//让操作系统的所有线程运行在单个CPU核心上
Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(1);
RunThreads();
}
private static void RunThreads()
{
var sample = new ThreadCounter();
var t1 = new Thread(sample.CountNumbers);
t1.Name = "Thread One Highest";
var t2 = new Thread(sample.CountNumbers);
t2.Name = "Thread Two Lowest";
//使用lambda表达式引用另一个C#对方的方式被称为闭包。
//当在lambda表达式中使用任何局部变量时,C#会生成一个类,并将该变量作为该类的一个属性,但是我们无须定义该类,C#编译器会自动帮我们实现
var t3 = new Thread(() => sample.CountNumbers());
t3.Name = "Thread Three Normal";
t1.Priority = ThreadPriority.Highest;//使用Priority设置线程的优先级
t2.Priority = ThreadPriority.Lowest;
t3.Priority = ThreadPriority.Normal;
t1.Start();
t2.Start();
t3.Start();
Thread.Sleep(TimeSpan.FromSeconds(2));
sample.Stop();
}
public class ThreadCounter
{
private bool _isStopped = false;
public void Stop()
{
_isStopped = true;
}
public void CountNumbers()
{
long counter = 0;
while (!_isStopped)
{
counter++;
}
Console.WriteLine($"{Thread.CurrentThread.Name} with {Thread.CurrentThread.Priority} priority has a count={counter.ToString("N0")}");
}
}
注释:单核执行多线程耗费的时间比多核的多很多