使用多线程的理由基本有三种
- 不想让后台任务阻塞UI主线程
- 任务太多,不允许CPU浪费时间去等待I/O的完成
- 需要让所有的处理器都为我所用
新建线程是一个开销很大的过程,因为需要分配核心资源,创建堆栈空间,还会造成更多的上下文切换。
.NET为每个托管进程维护着一个线程池,线程池中的线程会按需创建并保持存活,以满足后续的线程需求,也就避免了再次创建线程的开销。
使用Task
TPL内部使用了.NET的线程池,但效率更高。在把线程归还线程池之前,它会在同一个线程中顺序执行多个Task,通过对多个对象的智能调度来实现。(思考:顺序执行,那是在一个线程中顺序执行,还是在多个线程中?)
//task.ContinueWith()会在Task.Run()执行完后再运行
var task = Task.Run(()=> {
Thread.CurrentThread.Name = "Thread_Task_Run";
Console.WriteLine("-> Task.Run();ThreadName={0}", Thread.CurrentThread.Name);//ThreadName="Thread_Task_Run"
});
task.ContinueWith(res =>
{
if (string.IsNullOrWhiteSpace(Thread.CurrentThread.Name)) {
Thread.CurrentThread.Name = "Thread_Task_ContinueWith";
}
Console.WriteLine("-> task.ContinueWith();ThreadName={0}", Thread.CurrentThread.Name);//ThreadName="Thread_Task_ContinueWith"
});
//task.ContinueWith()会在Task.Run()执行完后再运行
//ContinueWith()加上参数TaskContinuationOptions.ExecuteSynchronously
//则会使task.ContinueWith()和Task.Run()在同一个线程中运行
//如果ContinueWith()代码很短,执行很快,则安排在同一个线程中运行,可以避免线程切换,上下文切换浪费时间。
var task = Task.Run(()=> {
Thread.CurrentThread.Name = "Thread_Task_Run";
Console.WriteLine("-> Task.Run();ThreadName={0}", Thread.CurrentThread.Name);//ThreadName="Thread_Task_Run"
});
task.ContinueWith(res =>
{
if (string.IsNullOrWhiteSpace(Thread.CurrentThread.Name)) {
Thread.CurrentThread.Name = "Thread_Task_ContinueWith";
}
Console.WriteLine("-> task.ContinueWith();ThreadName={0}", Thread.CurrentThread.Name);//ThreadName="Thread_Task_Run"
},TaskContinuationOptions.ExecuteSynchronously);
//可以对一个task执行多个ContinueWith(),多个之间没有关联,它们是并行运行,在多个线程中
//如果给每一个ContinueWith()添加参数TaskContinuationOptions.ExecuteSynchronously
//则它们都会在task的线程中依次执行,也就变成了顺序执行了。
var task = Task.Run(()=> {
Thread.CurrentThread.Name = "Thread_Task_Run";
Console.WriteLine("-> Task.Run();ThreadName={0}", Thread.CurrentThread.Name);//ThreadName="Thread_Task_Run"
});
task.ContinueWith(res =>
{
if (string.IsNullOrWhiteSpace(Thread.CurrentThread.Name)) {
Thread.CurrentThread.Name = "Thread_Task_ContinueWith";
}
Console.WriteLine("-> task.ContinueWith();ThreadName={0}", Thread.CurrentThread.Name);//ThreadName="Thread_Task_ContinueWith"
});
task.ContinueWith(res =>
{
if (string.IsNullOrWhiteSpace(Thread.CurrentThread.Name)) {
Thread.CurrentThread.Name = "Thread_Task_ContinueWith1";
}
Console.WriteLine("-> task.ContinueWith1();ThreadName={0}", Thread.CurrentThread.Name);//ThreadName="Thread_Task_ContinueWith1"
});
//可以将ContinueWith()串联起来依次执行,依然在多个线程中,前一个执行完再通知下一个执行
//如果给每一个ContinueWith()添加参数TaskContinuationOptions.ExecuteSynchronously
//则它们都是在task的线程中依次顺序执行。
var task = Task.Run(()=> {
Thread.CurrentThread.Name = "Thread_Task_Run";
Console.WriteLine("-> Task.Run();ThreadName={0}", Thread.CurrentThread.Name);//ThreadName="Thread_Task_Run"
});
task.ContinueWith(res =>
{
if (string.IsNullOrWhiteSpace(Thread.CurrentThread.Name)) {
Thread.CurrentThread.Name = "Thread_Task_ContinueWith";
}
Console.WriteLine("-> task.ContinueWith();ThreadName={0}", Thread.CurrentThread.Name);//ThreadName="Thread_Task_ContinueWith"
}).ContinueWith(res =>
{
if (string.IsNullOrWhiteSpace(Thread.CurrentThread.Name)) {
Thread.CurrentThread.Name = "Thread_Task_ContinueWith1";
}
Console.WriteLine("-> task.ContinueWith1();ThreadName={0}", Thread.CurrentThread.Name);//ThreadName="Thread_Task_ContinueWith1"
});
还可以指定Task在成功结束或者失败、取消之后,再执行相应的ContinueWith()
//根据task.Status来判断是否执行.
var task = Task.Run(()=> {
Thread.CurrentThread.Name = "Thread_Task_Run";
Console.WriteLine("-> Task.Run();ThreadName={0}", Thread.CurrentThread.Name);//ThreadName="Thread_Task_Run"
});
//仅在前一个task运行完成才执行此ContinueWith()
//task.Status = TaskStatus.RanToCompletion 时执行
task.ContinueWith(res =>
{
if (string.IsNullOrWhiteSpace(Thread.CurrentThread.Name)) {
Thread.CurrentThread.Name = "Thread_Task_ContinueWith";
}
Console.WriteLine("-> task.ContinueWith();ThreadName={0}", Thread.CurrentThread.Name);//ThreadName="Thread_Task_ContinueWith"
},TaskContinuationOptions.OnlyOnRanToCompletion);
//仅在前一个task运行没有完成才执行此ContinueWith()
//task.Status != TaskStatus.RanToCompletion 时执行
task.ContinueWith(res =>
{
if (string.IsNullOrWhiteSpace(Thread.CurrentThread.Name)) {
Thread.CurrentThread.Name = "Thread_Task_ContinueWith1";
}
Console.WriteLine("-> task.ContinueWith1();ThreadName={0}", Thread.CurrentThread.Name);//ThreadName="Thread_Task_ContinueWith1"
},TaskContinuationOptions.NotOnRanToCompletion);
还可以指定在多个task均完成之后(或者任意一个完成)在执行continueWith()
Task[] tasks = ....;
Task.Factory.ContinueWhenAll(tasks,()=>{...}); //所有tasks都执行完之后
Task.Factory.ContinueWhenAny(tasks,AnyFunc);// 任一task完成后执行
如果Task需要长时间运行(我理解的意思是线程多执行几个时间片,减少CPU切换线程)可以用带TaskCreationOptions.LongRunning参数的Task.Factory.StartNew方法进行创建,ContinueWith()方法也可以带TaskContinuationOptions.LongRuning参数。
var task = Task.Factory.StartNew(()=>{....},TaskCreationOptions.LongRunning);
task.ContinueWith(res=>{....},TaskContinuationOptions.LongRunning);
取消正在运行的Task需要一定的步骤,强行终止线程绝对不是个好主意,TPL也不允许直接访问底层的线程对象。
如果要取消Task,需要给Task的委托传递一个CancllationToken的对象,CancellationToken对象会轮询是否要执行终止操作。
static void Main(string[] args){
var tokenSource = new CancellationTokenSource();
CancellationToken token = tokenSource.Token;
var task = Task.Run(()=> {
while (true) {
//do some work ...
Console.WriteLine("do some work ...");
if (token.IsCancellationRequested) {
Console.WriteLine("Cancellation requested");
return;
}
Thread.Sleep(1000);
}
},token);
Console.WriteLine("Press any key to exit");
Console.ReadKey();
tokenSource.Cancel();
task.Wait();
Console.WriteLine("Task completed");
}
并行循环
系统提供专门的api来完成并行循环Parallel.For()和Parallel.Foreach()
//会在多个线程中循环,但是具体在几个线程中是不确定的
//可以确定也会在运行Parallel的当前主线程中循环
Parallel.For(0, 10, i => {
if (string.IsNullOrWhiteSpace(Thread.CurrentThread.Name)) {
Thread.CurrentThread.Name = $"Thread_{i}";
}
Console.WriteLine("i={0},ThreadName={1}",i,Thread.CurrentThread.Name);
});
如果需要中断循环执行,可以给循环委托传递一个ParallelLoopState对象,中断有两种方式
- Break:告诉循环不要执行大于当前迭代次数的任何迭代,小于的仍然会运行
- Stop:告诉循环不要执行任何迭代
(根据实验,break的意思也就是和正常的循环一样,如果在某一次循环终止,那么之后没有循环的就不会再循环了)
var names = new List<string>() {
"a",
"b",
"c",
"d"
};
Parallel.ForEach<string>(names, (name,loopState) => {
if (name =="c") {
loopState.Break();
}
if (string.IsNullOrWhiteSpace(Thread.CurrentThread.Name))
{
Thread.CurrentThread.Name = $"Thread_{name}";
}
Console.WriteLine("name={0},ThreadName={1}", name, Thread.CurrentThread.Name);
});
在使用并行循环时,应该确保每次迭代的工作量要明显大于同步共享状态的开销(即每次循环执行的代码的工作量要大,不能就是几行运行很快就结束的代码,这样的话相对就把大量时间都耗在了阻塞式访问共享的循环变量上了),所以尽量让每次循环都只进行局部访问(简单点说,就是把大的循环,分成几个小块,来并行处理这几个小块)。
Partitioner类可以把大的迭代拆分并存入Tuple对象中。
Stopwatch watch = new Stopwatch(); //监控运行的时间
const int MaxValue = 10000000;
long sum = 0;
//普通For循环
watch.Restart();
sum = 0;
Parallel.For(0, MaxValue, i => {
Interlocked.Add(ref sum, (long)Math.Sqrt(i));
});
watch.Stop();
//输出所耗时间
Console.WriteLine("Parallel.For():{0}",watch.Elapsed);
//分区的循环
var partitioner = Partitioner.Create(0, MaxValue);
watch.Restart();
sum = 0;
Parallel.ForEach(partitioner, range => {
long partialSum = 0;
for (int i=range.Item1; i<range.Item2;i++) {
partialSum += (long)Math.Sqrt(i);
}
Interlocked.Add(ref sum,partialSum);
});
watch.Stop();
Console.WriteLine("Partitioned Parallel.ForEach():{0}",watch.Elapsed);
//输出结果。运行的效率不言而喻!
//Parallel.For():00:00:40.0840333
//Partitioned Parallel.ForEach():00:00:00.0208418
避免阻塞
阻塞式调用会存在一定的问题,要尽量避免阻塞式调用
等待I/O完成期间阻塞当前线程,会发生以下两种情况:
- 线程会被阻塞为等待状态,不会参与线程调度,并会运行另一个线程。如果当前所有线程都被占用或阻塞,可能就会创建新的线程来完成后续任务。
- 线程遇到某个同步对象,也许会为了等待解锁而自旋(Spin)若干毫秒。如果无法及时获得信号量,就会进入第一步的状态。
这两种情况下,都没有必要扩大线程池中的线程数,也有可能会让CPU为了等待解锁而白白自旋,因此都是不可取的。
lock和其他种类的直接线程同步方式都是很明显的阻塞式调用。
尽量进行异步调用,而不是阻塞式调用。(例如与网络、文件系统、数据库等任何高延迟服务的交互操作)
正确使用Timer对象
请勿大量创建定时器,全部Timer都由线程池中的1个线程提供支持。如果Timer的数量太多,则执行回调方法会被延迟。
线程同步和锁
同步就是确保同时只能有一个线程访问共享信息(比如某个类的成员)的技术。
线程同步通常利用同步对象来完成,比如Monitor,Semaphore,ManualRestEvent等,这些同步对象也被非正式的称为“锁”,线程中处理同步的过程被称为“加锁”。
同步锁有一个基本真理:加锁决不会提高性能。做多也就是做到不增不减,还得靠高质量的同步原语并且不发生资源争用。同步锁会显式的阻止其他线程干活,并造成CPU闲置,增加上下文的切换时间。