任务
System.Threading.Tasks
命名空间中的Task
类- 不仅可以获得一个抽象
- 还可以对底层线程进行很多控制
任务:表示 将完成的 某个工作单元 这个工作单元:
- 可以在单独的线程中运行
- 也可以以同步方式启动。这需要等待主调线程
在安排需要完成的工作时,任务提供了非常大的灵活性
- 可以定义一个任务完成后,该执行什么工作
- 可根据任务成功与否才进一步区分执行什么
- 可以在层次结构中安排任务
- 父任务可以创建新的子任务
- 取消父任务,也会取消子任务
启动任务的 四种方式
以如下方式启动的任务,都使用了线程池中的线程
-
方式一:
TaskFactory
类:- 调用它的
StartNew()
的实例方法 - 立即启动任务
- 调用它的
-
Task
类:- 方式二:通过
Task
类的静态属性Factory
来访问TaskFactory
- 和方式一类似,但对创建
TaskFactory
的控制不如方式一全面
- 和方式一类似,但对创建
- 方式三:
Task
类的构造函数 +Start()
方法- 实例化
Task
对象时,任务不会立即启动,而是处于Created
状态 - 需要通过调用
Task
类的Start()
方法来启动任务 - 可以指定
Action
或Action<object>
- 实例化
- 方式四:
Task
类的Run
方法- 立即启动任务
Run
方法没有可以接受Action<object>
的重载版本。但可以通过传递Action
类型的lamda
表达式并在其实现中使用参数,来模拟接受Action<object>
- 方式二:通过
// 立即启动任务
var tf = new TaskFactory();
Task t1 = tf.StartNew(TaskMethod, "using a task factory");
Task t2 = Task.Factory.StartNew(TaskMethod, "factory via a task");
var t3 = new Task(TaskMethod, "using a task constructor and Start");
t3.Start();
Task t4 = Task.Run(() => TaskMethod("using the Run method"));
输出:
>dotnet TaskSamples.dll -p
factory via a task
Task id: 1, thread: 4
is pooled thread: True
is background thread: True
using a task factory
Task id: 2, thread: 5
is pooled thread: True
is background thread: True
using a task constructor and Start
Task id: 3, thread: 6
is pooled thread: True
is background thread: True
using the Run method
Task id: 4, thread: 3
is pooled thread: True
is background thread: True
示例:
打 Log 的信息:
- 任务ID:
Task.CurrentId
- 线程ID:
Thread.CurrentThread.ManagedThreadId
- 线程来自线程池:
Thread.CurrentThread.IsThreadPoolThread
- 线程是一个后台线程:
Thread.CurrentThread.IsBackground
//避免多次打Log的操作彼此交叉
private static object s_logLock = new object();
public static void Log(string title)
{
lock (s_logLock)
{
Console.WriteLine(title);
Console.WriteLine($"Task id: {Task.CurrentId?.ToString() ?? "no task"}, thread: {Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine($"is pooled thread: {Thread.CurrentThread.IsThreadPoolThread}");
Console.WriteLine($"is background thread: {Thread.CurrentThread.IsBackground}");
Console.WriteLine();
}
}
public static void TaskMethod(object o)
{
Log(o?.ToString());
}
线程池
- 提供了一个后台线程的池子
- 独立管理线程
- 根据需要,增加或减少,线程池中的线程数
- 其中的线程,用完后,仍然返回到线程池中
当线程来自线程池时,任务调度器 可以决定等待已经运行的任务完成,然后使用这个线程
同步运行Task:RunSynchronously()
Task
类的RunSynchronously()
任务其实也可以同步运行,即:使用和主调线程相同的线程来运行任务 这时,如果主调线程不是线程池里的线程,那么任务也不是使用线程池里的线程
//主调线程先调一遍 TaskMethod
TaskMethod("just the main thread");
//在新创建的Task上,再调一遍 TaskMethod
var t1 = new Task(TaskMethod, "run sync");
t1.RunSynchronously();
输出:
>dotnet TaskSamples.dll -s
just the main thread
Task id: no task, thread: 1
is pooled thread: False
is background thread: False
run sync
Task id: 1, thread: 1
is pooled thread: False
is background thread: False
让长时间运行的任务 使用单独的线程 TaskCreationOptions.LongRunning
Task
的构造函数和TaskFactory
的StartNew()
方法,都可以接受TaskCreationOption
TaskCreationOptions.LongRunning
- 告诉任务调度器,创建一个新线程,而不是使用线程池中的线程。
- 线程不由线程池管理
var t1 = new Task(TaskMethod, "long running", TaskCreationOptions.LongRunning);
t1.Start();
输出:
>dotnet TaskSamples.dll -l
long running
Task id: 1, thread: 3
is pooled thread: False
is background thread: True
任务的结果
如何得到任务的结果:
- 方法一:当任务结束时,将结果写到共享对象中。这个共享对象必须是线程安全的
方法二
:一开始就使用能返回结果的任务。- 早期,这种任务在TPL中被定义为
Future
类 - 现在,这种任务在TPL被定义为Task类的一个泛型版本,
Task<TResult>
类
- 早期,这种任务在TPL中被定义为
Task<TResult>
的构造函数- 第一个参数:Func委托
- 第二个参数: 传入第一个参数的方法的入参
Task<TResult>
的Result
属性被禁用,直到该任务完成
示例:结果可以为元组
public static void TaskWithResultDemo()
{
var t1 = new Task<(int Result, int Remainder)>(TaskWithResult, (8, 3));
t1.Start();
// Result属性被禁用,直到该任务完成
Console.WriteLine(t1.Result);
t1.Wait();
Console.WriteLine($"result from task: {t1.Result.Result} {t1.Result.Remainder}");
}
private static (int Result, int Remainder) TaskWithResult(object division)
{
(int x, int y) = ((int x, int y))division;
int result = x / y;
int remainder = x % y;
Console.WriteLine("task creates a result...");
return (result, remainder);
}
输出:
>dotnet TaskSamples.dll -r
task creates a result...
(2, 2)
result from task: 2 2
连续的任务
可以指定,在任务完成后,开始另一个任务
- 头一个任务:或者不带参数,或者带一个对象参数
- 后续的任务:带一个Task类型的参数
//头一个任务
private static void DoOnFirst()
{
Console.WriteLine($"doing some task {Task.CurrentId}");
Task.Delay(3000).Wait();
}
//后续的任务
private static void DoOnSecond(Task t)
{
Console.WriteLine($"task {t.Id} finished");
Console.WriteLine($"this task id {Task.CurrentId}");
Console.WriteLine("do some cleanup");
Task.Delay(3000).Wait();
}
public static void ContinuationTasks()
{
Task t1 = new Task(DoOnFirst);
Task t2 = t1.ContinueWith(DoOnSecond);
Task t3 = t1.ContinueWith(DoOnSecond);
Task t4 = t2.ContinueWith(DoOnSecond);
t1.Start();
}
输出:
>dotnet TaskSamples.dll -c
doing some task 1
task 1 finished
this task id 2
do some cleanup
task 1 finished
this task id 3
do some cleanup
task 2 finished
this task id 4
do some cleanup
后续的任务,总是在前一个任务结束时启动
TaskContinuationOptions 可以指定:
- OnlyOnFaulted
- NotOnFaulted
- OnlyOnCanceled
- NotOnCanceled
- OnlyOnRanToCompletion
Task t5 = t1.ContinueWith(DoOnSecond, TaskContinuationOptions.OnlyOnFaulted);
任务的层次结构
一个任务启动一个新任务时,就有了一个父子层次结构
- 子任务完成与否,影响父任务的状态:
- 如果父任务在子任务结束之前结束,父任务的状态就会显式为
WaitingForChildrenForComplete
- 所有子任务结束时,父任务的状态就变成
RanToCompletion
- 如果父任务在子任务结束之前结束,父任务的状态就会显式为
- 如果父任务用
TaskCreationOptionDetachedFromParent
来创建子任务,子任务的完成与否就不影响父任务的状态 - 取消父任务,子任务也会被取消
public static void ParentAndChild()
{
var parent = new Task(ParentTask);
parent.Start();
Task.Delay(2000).Wait();
Console.WriteLine(parent.Status);
Task.Delay(4000).Wait();
Console.WriteLine(parent.Status);
}
private static void ParentTask()
{
Console.WriteLine($"task id {Task.CurrentId}");
var child = new Task(ChildTask);
child.Start();
Task.Delay(1000).Wait();
Console.WriteLine("parent started child");
}
private static void ChildTask()
{
Console.WriteLine("child");
Task.Delay(5000).Wait();
Console.WriteLine("child finished");
}
输出:
>dotnet TaskSamples.dll -pc
task id 1
child
parent started child
RanToCompletion
child finished
RanToCompletion
Task.FromResult()
创建已完成的任务的结果,该任务的状态为
RanToCompletion
public Task<IEnumerable<string>> TaskMethodAsync ()
{
return Task.FromResult<IEnumerable<string>>(new List<string> {"one", "two"});
}
等待任务
等所有都完成
WhenAll()
:返回一个任务,从而使用async关键字等待结果,它不会阻塞调用线程WaitAll()
:它会阻塞调用线程
等任意一个完成
WhenAny()
:返回一个任务,从而使用async关键字等待结果,它不会阻塞调用线程WaitAny()
:它会阻塞调用线程
等待几秒
Task.Delay()
:可以指定,这个方法返回的任务完成之前,要等待的毫秒数
释放CPU
Task.Yield()
: 释放CPU, 从而允许其他任务运行。如果没有其他任务等待运行,那么它自己就立即继续执行。否则,就要等到再次调度CPU才会执行它自己。
ValueTask
- 是一个结构,相较于Task, 它没有堆中对象的开销。
- 它的构造函数接收:
- 返回类型
TResult
, 或Task<TResult>
- 返回类型
当
Task
在堆上的开销不可忽略时,考虑使用ValueTask
例如:
- 对API服务器或数据库进行调用时,与需要完成工作的时间相比,
Task
类型的开销可以忽略。 - 而若非网络调用,方法会被调用数千次,此时,
Task
类型的开销便不可以被忽略。
当有时是异步运行,有时又不是异步运行时,可以考虑用
ValueTask
,如下例: 一会儿从cache里获取,一会儿从费时的异步方法里获取
static async Task Main(string[] args)
{
for (int i = 0; i < 20; i++)
{
IEnumerable<string> data = await GetSomeDataAsync();
await Task.Delay(1000);
}
Console.ReadLine();
}
private static DateTime _retrieved;
private static IEnumerable<string> _cachedData;
public static async ValueTask<IEnumerable<string>> GetSomeDataAsync()
{
if (_retrieved >= DateTime.Now.AddSeconds(-5))
{
Console.WriteLine("data from the cache");
return await new ValueTask<IEnumerable<string>>(_cachedData);
}
Console.WriteLine("data from the service");
(_cachedData, _retrieved) = await GetTheRealData();
return _cachedData;
}
public static Task<(IEnumerable<string> data, DateTime retrievedTime)> GetTheRealData() =>
Task.FromResult(
(Enumerable.Range(0, 10)
.Select(x => $"item {x}").AsEnumerable(),
DateTime.Now));
输出:
data from the service
data from the cache
data from the cache
data from the cache
data from the cache
data from the service
data from the cache
data from the cache
data from the cache
data from the cache
data from the service
data from the cache
data from the cache
data from the cache
data from the cache
data from the service
data from the cache
data from the cache
data from the cache
data from the cache