这是一个高频,深刻的问题,无论去哪都逃不过被询问这个问题。Task是基于Thread的,这是众所周知的。但是Task和Thread的联系如此简单和纯粹确实我没想到的。甚至只需要几十行代码就能呈现其原理。一个简单的模拟实例说明Task及其调度问题,这真是一篇好文章。
任务体系由两个类组成,Task
,以及TaskScheduler
。
Task储存需要到多线程去执行的委托方法,尽管经过层层封装,内部最终还是调用这个委托。但是任务的执行方法不向程序员开放,而是交给了TaskScheduler,暴露给程序员的只有把任务交给任务调度器这个方法。任务说白了是围绕委托这个中心构建的。至于委托在哪个线程上执行,职责不在此,交给了任务调度器。
TaskScheduler用于决定将Task放到哪个线程上执行,最简单的是new Thread
将Task及其内部的委托放进新线程去执行。复杂一点的就是调用线程池的排队方法,将Task放到线程池要访问的待执行Task队列中,让队列不断弹出Task,然后放到某个线程中去执行。
我举两个任务调度器来说明这有多简单。一个调度器用于对每个任务创建一个线程执行,另一个调度器用于创建一个线程池,并用线程池的线程去取任务执行。
//使用新线程执行任务
public class ThreadScheduler:TaskScheduler
{
protected override void QueueTask(Task task)
{
//没想到就是直接把Task放到Thread中去执行了
new Thread(()=>TryExecuteTask(task))
.Start();
}
}
//测试
ThreadScheduler threadScheduler = new ThreadScheduler();
Task.Factory.StartNew(() => Console.WriteLine($"Task1 is excuted in thread {Thread.CurrentThread.ManagedThreadId}"), default, TaskCreationOptions.None, threadScheduler);
Task.Factory.StartNew(() => Console.WriteLine($"Task2 is excuted in thread {Thread.CurrentThread.ManagedThreadId}"), default, TaskCreationOptions.None, threadScheduler);
Task.Factory.StartNew(() => Console.WriteLine($"Task3 is excuted in thread {Thread.CurrentThread.ManagedThreadId}"), default, TaskCreationOptions.None, threadScheduler);
Task.Factory.StartNew(() => Console.WriteLine($"Task4 is excuted in thread {Thread.CurrentThread.ManagedThreadId}"), default, TaskCreationOptions.None, threadScheduler);
//使用线程池执行任务
public class ThreadPoolScheduler:TaskScheduler
{
private BlockingCollection<Task> tasks=new();
private Thread[] threads;
//创建一个线程池,让线程不断去队列中取出任务执行
public ThreadPoolScheduler(int threadNum)
{
threads=new Thread[threadNum];
for(int i=0;i<threadNum;i++)
{
threads[i]=new Thread(InvokeNext);
threads[i].Start();
}
void InvokeNext()
{
while(true)
{
var task=tasks.Take();
if(task!=null)
{
TryExecuteTask(task);
}
}
}
}
//新任务入队
protected override void QueueTask(Task task)
{
tasks.Add(task);
}
}
//测试
ThreadPoolScheduler threadScheduler = new ThreadPoolScheduler(2);
Task.Factory.StartNew(() => Console.WriteLine($"Task1 is excuted in thread {Thread.CurrentThread.ManagedThreadId}"), default, TaskCreationOptions.None, threadScheduler);
Task.Factory.StartNew(() => Console.WriteLine($"Task2 is excuted in thread {Thread.CurrentThread.ManagedThreadId}"), default, TaskCreationOptions.None, threadScheduler);
Task.Factory.StartNew(() => Console.WriteLine($"Task3 is excuted in thread {Thread.CurrentThread.ManagedThreadId}"), default, TaskCreationOptions.None, threadScheduler);
Task.Factory.StartNew(() => Console.WriteLine($"Task4 is excuted in thread {Thread.CurrentThread.ManagedThreadId}"), default, TaskCreationOptions.None, threadScheduler);
从这两种调度器可以看出,开始一个任务这个动作是唯一明确的只有一点,就是把任务交给调度器,而不是立即执行任务。至于任务有没有立即被放到线程中执行,这却决于任务调度器的实现。比如在第一种调度器中,任务被立即执行;在第二种调度器中,任务可能会等待,直到有空闲线程把它从队列中取出来。
任务和多线程还有一个区别是拥有回调ContinueWith
。这样就不需要使用阻塞或线程同步去解决这种很常见的,在一件事完成后再做另一件事的问题。大内老A提出的方式是,在任务内部,在执行委托的那个函数中,在前一个委托执行完成后,开启一个新任务,执行下一个委托。
由于这个触发节点在前一个线程即将结束时,所以能实现回调。
由于回调和开始任务这两个方法有相同返回类型Task,所以又实现了链式调用。
异步的解释是以同步的方式进行异步编程
。这是对任务的进一步改进。这玩意只能通过语法糖去实现,达到的效果是将任务的回调执行模型变换为了直观上看起来的顺序执行模型。在多线程同步这个问题上,可以得出这样一条演变链条。
Thread | Task | async/await |
---|---|---|
锁,信号量等线程同步 | 回调 | 同步的方式编程 |
异步和多线程有什么区别?主要在于线程同步方式的区别吧。