多线程(Multithreading)
一些基本的关于线程和与其相关的概念
1.系统资源管理器
管理进程
3.线程
也称控制点,由控制点进入Main函数,逐步执行程序。
4.多线程
多个控制点同时执行。
5.线程池
分配线程去执行任务的线程管理器
6.任务 System.Threading.Tasks
工作的内容,为了生成某些结果。任务运行在线程中,即线程是执行任务的对象。任务由Task表示,不同类型的任务则由Task的派生Task<T>表示。
7.进程 System.Diagnostics.Process
一个应用程序至少有一个进程,进程在一小块内存中保存着应用的资源,它是应用程序的实例。而一个进程里至少有一个线程,线程就是进程的执行路径,多个线程就表示一个进程有多条执行路径。在一个进程里多个线程共享着它们隶属的进程所在的内存单元。多线程的意义在于它们可以在一个进程中同时执行任务,共享同一块内存,提高了完成任务的速度,节约了时间。多线程之间可以并发执行,也可以相互终结。
8.同步
比如A线程执行完之后B线程才开始执行。也即B 必须等待A完成处理后它才会执行任务。
9.异步
比如A线程还未完成任务,但B线程可以不用等待A线程任务的完成就可以向下执行。所以多线程的实现靠的必然就是以异步的方式去完成任务。
10.时间分片
操作系统通过时间分片来模拟多线程的并行执行,它用极快的速度从A线程切换到B线程,每个线程在执行的那段时期被称为时间片段或量子,在进程里切换线程的执行称为上下文切换
11.并发
具备处理多个任务的能力,比如一个任务暂停后去处理另一个任务,另一个任务完成后继续处理前面暂停的任务。并发看起来似乎是同时在处理多个任务,但它实际上靠的是时间分片技术。除非cpu个数与线程数对等,否则算不上是并行,而是看起来像是并行行为实际上是交替执行任务的时间分片。
12.并行
真正具备同时处理多个任务的能力,它是靠多个cpu(多核)运行多个线程从而达到真正的同时执行任务。
13.串行
完全遵守一个任务一个任务的执行逻辑,没有时间分片。
14.任务的原子性
原子是不可再分割的最小单位,这个任务不能由多个线程来执行,因为它不可由时间分片或多核cpu运行多个线程来处理,这个任务要么执行成功,要么可能遭遇断电从而失败。假设A和B同时在网上买票,票只有一张,但他们同时下单付款,那么买票这个任务就是原子性的,因为它应该排斥多个线程对其进行操作。所以,单线程就是原子性的操作,而多线程可以归为非原子性的操作。
15.竞态条件
假设A和B同时在网上买票,票只有一张,但他们同时下单付款,请求发送到服务端电脑上,电脑上开启两个线程处理这两个任务,但你根本无法知晓计算机在什么时候开始时间分片,也即有可能会发生这样的情况:当A和B同时付款后,突然切换到B线程,B成功买到票,而A失败,或者正好相反,这些结果是我们无法提前预测掌控的。为了防止非原子性的操作,C#提供Lock语句将多个针对同一目标的线程阻塞起来,然后队列执行,每处理一个线程就为其上锁,处理完成再解锁,接着再处理下一个线程的操作。
16.什么时候使用多线程?
1.程序需要同时并发/并行执行多个任务
2.程序需要等待远程返回结果,同时本地代码还得继续执行
3.解决延迟(当用户使用网络导入功能导入大量数据时,如果发生网络延迟,他可能想要点击取消或尝试其它操作)
直接操纵线程的API
通过Thread类来表示在CRL中非托管的线程,这个对象代表了应用程序的控制点,通过给Thread类的构造函数传递一个委托即可,该委托用于代理执行任务。
namespace ConsoleApp1
{
class Program
{
public const int i=1000;
public static void Dowork()
{
for (int c = 0; c < i; c++)
{
Console.Write(‘+‘);
}
}
static void Main(string[] args)
{
//Main就是一个线程入口(控制)点,所以它是一个线程
//在Main中创建了另一个新的线程
//可以将Main看成主线程
//ThreadStart start = Dowork;
//Thread thread = new Thread(start);
//或
Thread thread = new Thread(Dowork);
thread.Start(); //开启新线程
thread.Join();//阻止主线程执行直到本线程执行完毕再执行主线程
for (int h = 0; h < i; h++)
{
Console.Write(‘-‘);
}
}
}
}
Thread类
Thread(ParameterizedThreadStart parameterizedThread | ThreadStart threadStart)
//创建线程实例,参数可以是两种委托中的任意一种,前者带一个object参数,后者无参。
Join(Int Millisecond | TimeSpan time)
//阻止其它线程的执行直到本线程(调用Join方法的线程)执行完毕,参数可以指定最多等待当前线程执行多少时间,过期则阻止变成失效
Abort()
//立即终止线程
//属性
IsBackground
//设置或获取线程是否是后台线程,后台线程的意思是即使进程关闭,后台线程也不会退出直到它自己执行完毕,前台线程则相反,进程必须等待线程全部退出才能终止
Priority
//设置或获取线程优先级,优先级越高的线程,时间分片会优先考虑它。值为ThreadPriority枚举,可能的值:AboveNormal | BelowNormal | Highest | Lowest | Normal
IsAlive
//获取线程是否还活着
ThreadState
//获取线程的更多状态信息,返回一个ThreadState标志枚举
//静态方法
Thread.Sleep(int Millisecond | TimeSpan time)
//使线程进入休眠状态,也即至少在指定时间内操作系统不为线程分配时间分片。假设设为10000毫秒,则唤醒时间根本无法预料,我们唯一知道的只是线程会沉睡至少10000毫秒而已
//假设有一个A线程的异步调用正在工作,你寄希望于B线程等待A完成任务后再执行,你可能会调用Sleep方法让B线程沉睡。但此方法作为线程同步的滥用方法应予以摈弃,因为你不知道它什么时候可以被唤醒,此方法只能作为有意图的人工延迟被使用
利用线程池间接操纵线程
重复性的创建、启动一个新线程的开销很大,而且创建太多的线程会使cpu不停地在时间分片上切换,耗费大量时间在切换任务上。而将异步任务交由线程池对象,由线程池去创建、管理线程,就可以达到重复使用闲置线程的目的,线程池只分配一定份额的线程数,然后对它们进行统一调度,这样就可以节省存开销。线程池的缺点:1.假如池中的线程已经被占尽,那么其他异步任务将被阻塞直到完成任务的线程回到池中接受新任务。2.因为你不能直接使用Thread或Task操纵线程,这些都由线程池来管理,所以如果需要处理花费以天计数的耗时任务或需要同步的任务则线程池就不合适。
namespace ConsoleApp1
{
class Program
{
public const int i=1000;
public static void Dowork(object parameter)
{
for (int c = 0; c < i; c++)
{
Console.Write(‘+‘);
}
}
static void Main(string[] args)
{
ThreadPool.QueueUserWorkItem(Dowork, ‘+‘);
for (int h = 0; h < i; h++)
{
Console.Write(‘-‘);
}
}
}
}
使用TPL(Task Parallel Library)并行扩展库
.NET4.0提供了新的并行编程模型TPL,TPL也是利用线程池来管理线程,它向外界公开一个Task的类来表示异步任务。Task.Run(Action action)方法接收一个Action委托进行异步任务的执行,委托与任务(Task)的区别在于,控制流从调用者处进入委托,要等待委托执行完毕才会将控制权返回到调用者处,然后继续下一行代码的执行,而Task任务则是封装一个委托,控制流从调用者处进入Task后会立即将控制权返回到调用者处,这样多个任务得以并行执行且互不影响。
//第一种
Task task = new Task(() => { TestRun.RunTask(); });
task.Start();
//第二种
Task task = Task.Run(() => { TestRun.RunTask(); });
//第三种
Task task = Task.Factory.StartNew(() => { TestRun.RunTask(); });
Task类
如果异步任务需要返回结果,则可以使用Task<T>,泛型类型参数T就是返回的结果类型。通过在主线程中轮询异步任务的IsCompleted来获得异步是否执行完毕以便获取执行的返回结果。
CurrentId
//获取异步任务的 ID,这对调试有帮助
Wait()
//阻止其它任务的执行直到本任务(调用Wait方法的任务)执行完毕,参数可以指定最多等待当前任务执行多少时间,过期则阻止变成失效
//属性
IsCompleted
//异步任务是否已经执行完成,对于其Status属性返回的TaskStatus枚举值是RanToCompletion、Faulted、Canceled中的任何一个,则IsCompleted=true
Result
//获取异步任务的返回值
Status
//获取异步任务的详细状态,一个TaskStatus 枚举。可能的值如下:
//1.RanToCompletion:任务成功完成
//2.Faulted:任务出现异常,也算完成任务
//3.Running:任务正在执行,尚未完成
//4.Canceled:任务已经取消,也算完成任务
//5.Created:任务已初始化,但尚未列入激活计划
//6.WaitingForActivation:任务正在等待线程池列入激活计划
//7.WaitingToRun:任务已列入激活计划,但尚未开始执行
//8.WaitingForChildrenToComplete:任务已完成,正在隐式的等待附加的子任务完成
AsyncState
//在创建Task时附加到Task对象上的其它数据,通过参数state指定,用于需要时可通过AsyncState属性存取
var task = Task.Factory.StartNew(s => "119", "火警");
Console.WriteLine(task.AsyncState);
//静态方法
Task.WaitAll(Task[] tasks)
//阻止非参数指定的任务的执行直到参数指定的任务全部执行完毕
Task.WaitAny(Task[] tasks)
//阻止非参数指定的任务的执行直到参数指定的任务中的任意一个执行完毕,也即只要参数指定的任意一个任务已经完毕则不再等待
轮询的例子:
namespace ConsoleApp1
{
public class PiCalculator
{
public static string Calculate()
{
Thread.Sleep(10000);
return "异步任务已经完成……";
}
}
class Program
{
static void Main(string[] args)
{
//Task<string> task = new Task<string>(()=> PiCalculator.Calculate());
//task.Start();
//Task<string> task = Task.Run(() => PiCalculator.Calculate());
Task<string> task = Task.Factory.StartNew<string>(() => PiCalculator.Calculate()); //新线程的异步任务
string rotate = @"↘↓ ↙";
for (int c = 0; c <= 1000000; c++) //主线程的遍历任务
{
if (task.IsCompleted) //轮询新线程
{
Console.WriteLine(task.Result); //取异步任务返回值
break;
}
else
{
for (int f = 0; f < rotate.Length; f++)
{
Console.Write($"{rotate[f]}");
Console.Write("\b"); //退格删除状态标
Console.Write("\b");
}
}
}
}
}
}
这是一个比较重要的与异步任务有关的位标志枚举,用于控制任务的行为。
//None:默认,先驱任务一旦完成则继续执行延续任务,而无视先驱任务的状态
//PreferFairness:异步任务无法保证谁最先开始执行,PreferFairness可指定线程池的任务调度器对象按任务计划的顺序安排任务,因此较早安排的任务将更可能较早运行
//LongRunning:通知线程池的任务调度器对象,这可能是一个I / O受限的高延迟任务,调度器可处理已经队列的其他任务。应尽量少用此值。
//AttachedToParent:指定将某个任务附加到任务层次结构中的父级,使其成为子任务
//DenyChildAttach:父任务中指定此选项则会阻止子任务使用AttachedToParent选项附加到父任务上
//一般通过在父任务类构造函数中或Task.Factory.StartNew方法中指定 TaskCreationOptions.DenyChildAttach选项即可成功阻止子任务的附加
//DenyChildAttach:任何延续任务(ContinueWith)如果被指定为附加到父任务的子任务,则引发异常
//OnlyOnRanToCompletion:如果先驱任务成功完成,才会执行此延续任务,否则不。也即,先驱任务未成功完成,延续任务就不会执行
//NotOnRanToCompletion:如果先驱任务成功完成,则不会执行此延续任务,否则不。也即,先驱任务未成功完成,延续任务就会执行
//OnlyOnFaulted:如果先驱任务没有成功完成,才会执行此延续任务,否则不。也即,先驱任务成功完成,延续任务就不会执行
//NotOnFaulted:如果先驱任务没有成功完成,则不会执行此延续任务,否则不。也即,先驱任务成功完成,延续任务就会执行
//OnlyOnCanceled:如果先驱任务已经被取消,则执行此延续任务,否则不。也即,先驱任务没有被取消,延续任务就不会执行
//NotOnCanceled:如果先驱任务已经被取消,则不会执行此延续任务,否则不。也即,先驱任务没有被取消,延续任务就会执行
//特别注意:任何试图为任务手动捕获异常的行为都将被视为任务正常完成,为了正确掌握这种先驱任务是否正常完成,应考虑手动捕获Wait方法而不是在任务内部使用try catch
//例子:
static void Run(string num)
{
int n = Convert.ToInt32(num);
}
static void Main(string[] args)
{
Task taskA = Task.Run(() => Run("123"));
Task taskB = taskA.ContinueWith(a => Console.WriteLine("先驱任务正常完成,我才会执行"), TaskContinuationOptions.OnlyOnRanToCompletion);
try { taskB.Wait(); }
catch (Exception ex) { }
}
异步任务中可以附加子任务,可以把包含子任务的任务称为父任务,父任务会等待子任务完成,子任务的状态、异常等都会冒泡到父任务层面上。一般通过在子任务类的构造函数中或Task.Factory.StartNew方法中指定 TaskCreationOptions选项来附加或阻止附加子任务。Task.Run方法默认阻止子任务的附加,所以不需要手动调用TaskContinuationOptions显示设定。
{
//在父任务的委托中再创建一个任务,该任务就是一个子任务
var parent = Task.Factory.StartNew
(
() => {
Console.WriteLine("父任务完毕");
var child = Task.Factory.StartNew
(
() => { Console.WriteLine("子任务完毕"); },
TaskCreationOptions.AttachedToParent
);
}
);
parent.Wait(); //主线程可能先执行完完毕,Task异步执行时就看不到效果,所以此处要wait
}
延续任务
Task taskB = taskA.ContinueWith(a => Console.WriteLine("延续A……"));
Task taskC = taskA.ContinueWith(a => Console.WriteLine("延续A……"));
Task.WaitAll(taskB, taskC);
Console.WriteLine("完成……"); //taskA执行完成后将并发执行taskB,taskC,但两者的执行顺序是不确定的
多线程的异常捕获
不要尝试用try包含异步任务的Start方法,因为控制会立即从异步任务返回到主线程。所以在异步任务的异常还未发生时,控制已经立即返回主线程,也即控制立即会离开try块,此时,异常的捕获就是失败的。任何线程上的异常都必须手动捕获,否则Clr会终止程序。
AggregateException 异常集合
可以使用异常包(异常集合)去捕获所有线程任务的异常信息,再调用异常信息的Handle方法,通过一个委托来迭代异常信息。
{
Task taskA = Task.Run(()=> { throw new InvalidOperationException(); });
Task taskB = Task.Run(() => { throw new DllNotFoundException(); });
try
{
Task.WaitAll(taskA, taskB);
}
catch (AggregateException ex)
{
ex.Handle(eachException => { Console.WriteLine(eachException.Message); return true; }); //如果返回false则会重新引发异常
}
}
Task.Exception异常集合
在确定任务必然出现异常的情况下,可以使用Task的Exception.Handle方法,Task的Exception也是一个异常集合,Handle接收一个委托,可以输出异常信息。
Task taskB = task.ContinueWith(a => { Console.WriteLine("hello"); },TaskContinuationOptions.OnlyOnFaulted);
try
{
//说明taskB已经正确执行,则task必然有异常
taskB.Wait();
//输出task的异常
task.Exception.Handle(eachException => { Console.WriteLine(eachException.Message); return true; });
}
catch (Exception ex)
{
//说明taskB有异常,而task没有异常
}
异常事件
AppDomain.CurrentDomain.UnhandledException事件
{
static Stopwatch clock = new Stopwatch();
static void Message(string text)
{
Console.WriteLine($"当前线程ID:{Thread.CurrentThread.ManagedThreadId},{clock.ElapsedMilliseconds}:0000:{text}");
}
static void Delay(int Milliseconds)
{
Message($"睡眠{Milliseconds}毫秒");
Thread.Sleep(Milliseconds);
Message("唤醒");
}
static void Main(string[] args)
{
try
{
clock.Start(); //计时
AppDomain.CurrentDomain.UnhandledException += (objSender, eventArgs) => { Message("事件被触发……"); Delay(4000); }; //注册异常事件
Thread thread = new Thread(() => { Message("即将抛出异常……"); throw new Exception(); }); //触发异常事件
thread.Start();
Delay(2000);
//主线程ID是1,它将先执行Delay(2000);接着打印睡眠2000毫秒,再使主线程沉睡2秒,在主线程苏醒后,接下来thread.Start()将执行
//该线程ID是3,它主动将一个异常抛出,在抛出异常之前会进入Message方法打印相关的信息,打印完成后异常在被手动抛出之前会先触发UnhandledException事件
//UnhandledException事件调用Message方法打印相关的信息,打印完成后调用Delay方法使thread沉睡4秒后苏醒,再次打印相关信息后因为没有catch,所以最终异常会直接抛出
}
finally
{
}
}
}