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