使用多线程可以充分利用 CPU 资源。提高 CPU 的使用率,采用多线程的方式去同时完成几件事情而不互相干扰。在处理大量的 IO 操作或处理的情况需要花费大量的时间时(如:读写文件,视频图像的采集,处理,显示,保存等)有较大优势。
优点
- 多线程可以把占据时间长的程序中的任务放到后台去处理而不影响主程序的运行
- 程序的运行效率可能会提高
- 在一些等待的任务实现上如用户输入,文件读取和网络收发数据等,线程比较有用
不足
- 如果有大量的线程,会影响性能,因为操作系统需要在它们之间切换
- 更多的线程需要更多的内存空间
概念了解#
并发(Concurrency)#
逻辑上的同时发生,一个处理器(在不同时刻或者说在同一时间间隔内)“同时"处理多个任务。宏观上是并发的,微观上是按排队等待、唤醒、执行的步骤序列执行。并发性是对有限物理资源强制行使多用户共享(多路复用)以提高效率。
并行(Parallel)#
物理上的同时发生,多核处理器或多个处理器(在同一时刻)同时处理多个任务。并行性允许多个程序同一时刻可在不同 CPU 上同时执行。
进程(Process)#
程序在计算机上的一次执行活动。运行一个程序、启动一个进程.程序是死的(静态的),进程是活的(动态的)。Windows 系统利用进程把工作划分为多个独立的区域,每个应用程序实例对应一个进程。进程是操作系统分配和使用系统资源的基本单位.进程包含一个运行-ing 应用程序的所有资源、进程(占用的资源)间相互独立。
线程(Thread)#
轻量级进程,是进程的一个实体(线程本质上是进程中一段并发运行的代码),执行线程、体现程序的真实执行情况,是处理器上系统独立调度和时间分配的最基本的执行单元。同一进程的所有线程共享相同的资源和内存(共享代码,全局变量,环境字符串等),使得线程间上下文切换更快、可以在同一地址空间内访问内存。
同步#
如果一个程序调用某个方法,等待其执行所有处理后才继续执行,这样的方法是同步的。
异步#
如果一个程序调用某个方法,在该方法处理完成之前就返回到调用方法,则这个方法是异步的。
线程创建#
默认创建#
C#中使用 Thread 类创建和控制线程,该类允许创建线程,以及设置线程的优先级。
/// <summary>
/// 线程创建
/// </summary>
public static void ThreadCreate_Basic()
{
static void ThreadMethod()
{
Thread.Sleep(2000);
Console.WriteLine("子线程结束");
}
Console.WriteLine("程序在启动时创建一个线程,称为主线程");
//创建线程
Thread t = new Thread(ThreadMethod);
//启动线程
t.Start();
Console.WriteLine("主线程结束");
}Thread 构造函数接收 ParameterrizeThreadStart 和 ThreadStart 委托参数,所以也可以这么写:
Thread t = new Thread(new ThreadStart(ThreadMethod));lambad 表达式创建#
Thread t = new Thread(() => Console.WriteLine("ThreadMethod"));线程调用顺序#
/// <summary>
/// 线程调用顺序
/// </summary>
/// <remarks>
/// 观察输出结果说明
/// 线程是由操作系统调度的,每次哪个线程先被执行不确定,线程的调度是无序的
///</remarks>
public static void Thread_Order()
{
Console.WriteLine("程序在启动时创建一个线程,称为主线程");
Thread t = new Thread(() => Console.WriteLine("A"));
Thread t1 = new Thread(() => Console.WriteLine("B"));
Thread t2 = new Thread(() => Console.WriteLine("C"));
Thread t3 = new Thread(() => Console.WriteLine("D"));
t.Start();
t1.Start();
t2.Start();
t3.Start();
Console.WriteLine("主线程结束");
}注意:线程是由操作系统调度的,每次哪个线程先被执行可以不同,就是说该例中线程的调度是无序的。不同 PC 运行结果可能不一致,只作示例。
线程传递数据#
使用带 ParameterrizeThreadStart 委托参数的 Thread 构造函数创建自定义类。把线程方法定义为实例方法,之后初始化实例数据,启动线程。
不带参数:
Thread t = new Thread(() => Console.WriteLine("ThreadMethod"));一个参数:
Thread t = new Thread((object message) => Console.WriteLine(message));多个参数(自定义类):
class Data
{
public string name;
public int age;
public Data(string name, int age)
{
this.name = name;
this.age = age;
}
public void Write()
{
Console.WriteLine("name:{0},age:{1}", this.name, this.age);
}
}var data = new Data("Wang", 24);
Thread t = new Thread(data.Write);
t.Start();后台线程#
默认情况下:用 Thread 类创建的线程总是前台线程,线程池中的线程总是后台线程。
前台线程和后台线程的区别在于:
- 前台线程:应用程序必须运行完所有的前台线程才可以退出
- 后台线程:应用程序可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束
前台线程阻止进程的关闭
Thread thread = new Thread(() =>
{
Thread.Sleep(5000);
Console.WriteLine("前台线程执行");
});
thread.Start();
Console.WriteLine("主线程执行完毕");这里主线程马上执行完成,并不马上关闭,前台线程等待 5 秒再执行输出,然后控制台退出。
后台线程不阻止进程的关闭
Thread thread = new Thread(() =>
{
Thread.Sleep(5000);
Console.WriteLine("前台线程执行");
})
{ IsBackground = true };
thread.Start();
Console.WriteLine("主线程执行完毕");不等后台线程执行完毕,主线程执行完毕后立即退出,控制台立即退出。
线程优先级#
之前说到,线程是由操作系统调度的,给线程指定优先级,可以影响调度顺序,C#中 Thread 类的 Priority 属性提供了五种线程优先级别,这是一个枚举对象
Normal(正常,默认值)Highest(最高)AboseNormal(高于正常)BelowNormal(低于正常)Lowest(最低)
Thread normal = new Thread(() =>
{
Console.WriteLine("优先级为正常线程");
});
normal.Start();
Thread aboseNormal = new Thread(() =>
{
Console.WriteLine("优先级为高于正常线程");
})
{ Priority = ThreadPriority.AboveNormal };
aboseNormal.Start();
Thread belowNormal = new Thread(() =>
{
Console.WriteLine("优先级为低于正常线程");
})
{ Priority = ThreadPriority.BelowNormal };
belowNormal.Start();
Thread highest = new Thread(() =>
{
Console.WriteLine("优先级最高线程");
})
{ Priority = ThreadPriority.Highest };
highest.Start();
Thread lowest = new Thread(() =>
{
Console.WriteLine("优先级最低线程");
})
{ Priority = ThreadPriority.Lowest };
lowest.Start();
Console.WriteLine("主线程执行完毕");
结果可知:设置优先级并不会指定线程固定执行的顺序,设置线程优先级只是提高了线程被调用的概率,并不是定义 CPU 调用线程的顺序,具体还是要由操作系统内部来调度。
线程控制#
| 属性 | 描述 |
|---|---|
| CurrentThread | 获取当前正在运行的线程 |
| IsAlive | 获取一个值,该值指示当前线程的执行状态 |
| Name | 获取或设置线程的名称 |
| Priority | 获取或设置一个值,该值指示线程的调度优先级 |
| ThreadState | 获取一个值,该值包含当前线程的状态 |
| 方法 | 描述 |
| Abort | 调用此方法通常会终止线程 |
| Join | 阻止调用线程,直到某个线程终止时为止 |
| Resume | 继续已挂起的线程 |
| Sleep | 将当前线程阻止指定的毫秒数 |
| Start | 使线程被安排进行执行 |
| Suspend | 挂起线程,或者如果线程已挂起,则不起作用 |
Thread.Sleep()#
static void ThreadMethod()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("a");
}
}
static void ThreadMethodSleep()
{
for (int i = 0; i < 10; i++)
{
//暂停2s
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine("b");
}
}
Thread t = new Thread(ThreadMethodSleep);
t.Start();
ThreadMethod();工作原理
当程序运行时,主线程创建,而后创建线程 t ,该线程首先执行 ThreadMethodSleep 方法中的代码。然后会立即执行 ThreadMethod 方法。关键之处在于在ThreadMethodSleep 方法中加入了 Thread.Sleep 方法调用。这将导致线程执行该代码时,在打印任何数字之前会等待指定的时间(本例中是 2 秒),当线程处于休眠状态时,它会占用尽可能少的 CPU 时间。结果发现通常后运行的 ThreadMethod 方法中的代码会比独立线程中的 ThreadMethodSleep 方法中的代码先执行。
Thread.Join()#
static void ThreadMethod()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("a");
}
}static void ThreadMethodSleep()
{
for (int i = 0; i < 20; i++)
{
//暂停2s
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine("b");
}
}
Thread t = new Thread(ThreadMethodSleep);
t.Start();
Thread.Sleep(TimeSpan.FromSeconds(5));
t.Abort();
static void ThreadMethodSleep()
{
for (int i = 0; i < 10; i++)
{
//暂停2s
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine("b");
}
}
Thread t = new Thread(ThreadMethodSleep);
t.Start();
t.Join();
ThreadMethod();工作原理
程序运行后,创建线程 t ,调用 ThreadMethodSleep() 方法,该方法循环打印 3 个"b”,但是每次打印前都要暂停 2s,在主程序中调用 t.join 方法,该方法允许等待线程 t 完成,只有 t 线程完成后才会继续执行主程序的代码,该例中就是主线程等待 t 线程完成后再继续执行,主线程等待时处于阻塞状态。
Suspend/Resume#
需要多线程编程时为了挂起与恢复线程可以使用 Thread 类的 Suspend() 与 Resume() 方法。
注意:这两个方法已经过时
Thread.Abort()#
static void ThreadMethodSleep()
{
for (int i = 0; i < 20; i++)
{
//暂停2s
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine("b");
}
}
Thread t = new Thread(ThreadMethodSleep);
t.Start();
Thread.Sleep(TimeSpan.FromSeconds(5));
t.Abort();工作原理
程序运行后,创建线程 t ,调用 ThreadMethodSleep() 方法,该方法循环打印 20 个"b",但是每次打印前都要暂停 2s,在主线程中设置等待 6s 后调用 t.Abort() 方法,这有可能给线程注入了 ThreadAbortException 异常,导致线程被终结。这非常危险,因为该异常可以在任何时刻发生并可能彻底摧毁应用程序。另外,使用该技术也不一定总能终止线程。目标线程可以通过处理该异常并调用 Thread.ResetAbort 方法来拒绝被终止。因此并不推荐使用 Abort 方法来关闭线程。可优先使用一些其他方法,比如提供一个 CancellationToken 方法来,取消线程的执行。
Thread.ThreadState#
通过 Thread.ThreadState 属性读取当前线程状态。
static void ThreadMethodSleep()
{
for (int i = 0; i < 3; i++)
{
//暂停2s
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine("b");
}
}
Thread t = new Thread(ThreadMethodSleep);
Console.WriteLine("创建线程,线程状态:{0}", t.ThreadState);
t.Start();
Console.WriteLine("线程调用Start()方法,线程状态:{0}", t.ThreadState);
Thread.Sleep(TimeSpan.FromSeconds(5));
Console.WriteLine("线程休眠5s,线程状态:{0}", t.ThreadState);
t.Join();
Console.WriteLine("等待线程结束,线程状态:{0}", t.ThreadState);工作原理
程序运行后,创建线程 t (Unstarted)-> t.start() (Running)-> t.sleep() (WaitSleepJoin)-> t.join() 线程结束(Stopped)
异常处理#
static void ThreadMethodA()
{
throw new Exception("AError");
}
static void ThreadMethodB()
{
try
{
throw new Exception("BError");
}
catch (Exception ex)
{
Console.WriteLine("Error:{0}", ex.Message);
}
}
Thread t = new Thread(ThreadMethodB);
t.Start();
t.Join();
try
{
Thread t1 = new Thread(ThreadMethodA);
t1.Start();
}
catch (Exception e)
{
Console.WriteLine("Error:{0}", e.Message);
}
static void AddThread(object e)
{
Console.WriteLine("当前线程ID:{0}", Thread.CurrentThread.ManagedThreadId);
}
ThreadPool.GetMaxThreads(out int workThread, out int ioThread);
Console.WriteLine("工作线程数:{0},io线程数{1}", workThread, ioThread);
for (int i = 0; i < 5; i++)
{
ThreadPool.QueueUserWorkItem(AddThread);
workThread = ioThread = 0;
}
Console.WriteLine($"{workThread},{ioThread}");工作原理
程序运行后,定义了两个会抛出异常的线程,其中一个对异常进行了处理,另一个没有,可以看到 ThreadMethodB() 方法中的异常没有被主程序包裹线程启动的 try/catch代码块捕获到,所以如果直接使用线程,一般不要在主程序线程中抛出异常,而在线程代码中使用 try/catch 代码块。
线程池 ThreadPool#
创建线程需要时间,如果有不同的短任务要完成,就可以事先创建许多线程,在应完成这些任务时发出请求。这个线程数最好在需要更多的线程时增加,在需要释放资源时减少。C#中不需要自己创建维护这样一个列表,该列表由 ThreadPool 类托管。该类会在需要时增减池中线程的线程数,直到最大线程数,线程数的值是可配置的。如果线程池中个数到达了设置的极限,还是有更多的作业要处理,最新的作业就要排队,且必须等待线程完成其任务。
static void AddThread(object e)
{
Console.WriteLine("当前线程ID:{0}", Thread.CurrentThread.ManagedThreadId);
}
ThreadPool.GetMaxThreads(out int workThread, out int ioThread);
Console.WriteLine("工作线程数:{0},io线程数{1}", workThread, ioThread);
for (int i = 0; i < 5; i++)
{
ThreadPool.QueueUserWorkItem(AddThread);
workThread = ioThread = 0;
}
Console.WriteLine($"{workThread},{ioThread}");ThreadPool.GetMaxThreads(out workThread, out ioThread) 接收两个 out int 类型参数返回最大工作线程数和 io 线程数,for 循环中使用ThreadPool.QueueUserWorkItem() 方法传递 WaitCallback 类型委托,将 AddThread() 方法赋予线程池中的线程,线程池收到请求后,如果线程池还没有运行,就会创建一个线程池,并启动第一个线程,如果已经启动,且有一个空闲线程来完成任务,就把该任务传递给这个线程。
线程池使用限制#
- 线程池中所有线程都是后台线程,如果进程的所有前台线程都结束了,所有的后台线程就会停止,不能把入池的线程改为前台线程
- 不能给入池的线程设置优先级或名称
- 入池的线程只能用于时间较短的任务,如果线程要一直运行,就应使用
Thread类创建一个线程 - 对于
COM对象,入池的所有线程都是多线程单元(MTA)线程,许多 COM 对象都需要单线程单元(STA)