什么是 long-running thread

long-running task 是指那些长时间运行的任务,比如在一个 while True 中执行耗时较长的同步处理。

下面的例子中,我们不断从队列中尝试取出数据,并对这些数据进行处理,这样的任务就适合交给一个 long-running task 来处理。

var queue = new BlockingCollection<string>();

Task.Factory.StartNew(() =>
{
while (true)
{
// BlockingCollection<T>.Take() 方法会阻塞当前线程,直到队列中有数据可以取出。
var input = queue.Take();
Console.WriteLine($"You entered: {input}");
}
}, TaskCreationOptions.LongRunning); while (true)
{
var input = Console.ReadLine();
queue.Add(input);
}

在 .NET 中,我们可以使用 Task.Factory.StartNew 方法并传入 TaskCreationOptions.LongRunning 来创建一个 long-running task。

虽然这种方式创建的 long-running task 和默认创建的 task 一样,都是分配给 ThreadPoolTaskScheduler 来调度的, 但 long-running task 会被分配到一个新的 Background 线程上执行,而不是交给 ThreadPool 中的线程来执行。

class ThreadPoolTaskScheduler : TaskScheduler
{
// ...
protected internal override void QueueTask(Task task)
{
TaskCreationOptions options = task.Options;
if (Thread.IsThreadStartSupported && (options & TaskCreationOptions.LongRunning) != 0)
{
// 在一个新的 Background 线程上执行 long-running task。
new Thread(s_longRunningThreadWork)
{
IsBackground = true,
Name = ".NET Long Running Task"
}.UnsafeStart(task);
}
else
{
// 非 long-running task 交给 ThreadPool 中的线程来执行。
ThreadPool.UnsafeQueueUserWorkItemInternal(task, (options & TaskCreationOptions.PreferFairness) == 0);
}
}
// ...
}

为什么long-running task要和普通的task分开调度

如果一个task持续占用一个线程,那么这个线程就不能被其他的task使用,这和 ThreadPool 的设计初衷是相违背的。

如果在 ThreadPool 中创建了大量的 long-running task,那么就会导致

ThreadPool 中的线程不够用,从而影响到其他的 task 的执行。

在 long-running task await 一个 async 方法后会发生什么

有时候,我们需要在 long-running task 中调用一个 async 方法。比如下面的例子中,我们需要在 long-running task 中调用一个 async

的方法来处理数据。

var queue = new BlockingCollection<string>();

Task.Factory.StartNew(async () =>
{
while (true)
{
var input = queue.Take();
Console.WriteLine($"Before process: thread id: {Thread.CurrentThread.ManagedThreadId}, task scheduler: {InternalCurrentTaskScheduler()}, thread pool: {Thread.CurrentThread.IsThreadPoolThread}");
await ProcessAsync(input);
Console.WriteLine($"After process: thread id: {Thread.CurrentThread.ManagedThreadId}, task scheduler: {InternalCurrentTaskScheduler()}, thread pool: {Thread.CurrentThread.IsThreadPoolThread}");
}
}, TaskCreationOptions.LongRunning); async Task ProcessAsync(string input)
{
// 模拟一个异步操作。
await Task.Delay(100);
Console.WriteLine($"You entered: {input}, thread id: {Thread.CurrentThread.ManagedThreadId}, task scheduler: {InternalCurrentTaskScheduler()}, thread pool: {Thread.CurrentThread.IsThreadPoolThread}");
} while (true)
{
var input = Console.ReadLine(); queue.Add(input);
} TaskScheduler InternalCurrentTaskScheduler()
{
var propertyInfo = typeof(TaskScheduler).GetProperty("InternalCurrent", BindingFlags.Static | BindingFlags.NonPublic);
return (TaskScheduler)propertyInfo.GetValue(null);
}

连续输入 1、2、3、4,输出如下:

1
Before process: thread id: 9, task scheduler: System.Threading.Tasks.ThreadPoolTaskScheduler, thread pool: False
You entered: 1, thread id: 4, task scheduler: , thread pool: True
After process: thread id: 4, task scheduler: , thread pool: True
2
Before process: thread id: 4, task scheduler: , thread pool: True
You entered: 2, thread id: 4, task scheduler: , thread pool: True
After process: thread id: 4, task scheduler: , thread pool: True
3
Before process: thread id: 4, task scheduler: , thread pool: True
You entered: 3, thread id: 4, task scheduler: , thread pool: True
After process: thread id: 4, task scheduler: , thread pool: True
4
Before process: thread id: 4, task scheduler: , thread pool: True
You entered: 4, thread id: 4, task scheduler: , thread pool: True
After process: thread id: 4, task scheduler: , thread pool: True

从执行结果中可以看出,第一次 await 之前,当前线程是 long-running task 所在的线程(thread id: 9),此后就变成了 ThreadPool

中的线程(thread id: 4)。

至于为什么之后一直是 ThreadPool 中的线程(thread id: 4),这边做一下简单的解释。在我以前一篇介绍 await 的文章中介绍了 await 的执行过程,以及 await 之后的代码会在哪个线程上执行。



https://www.cnblogs.com/eventhorizon/p/15912383.html

  1. 第一次 await 前,当前线程是 long-running task 所在的线程(thread id: 9),绑定了 TaskScheduler(ThreadPoolTaskScheduler),也就是说 await 之后的代码会被调度到 ThreadPool 中执行。
  2. 第一次 await 之后的代码被调度到 ThreadPool 中的线程(thread id: 4)上执行。
  3. ThreadPool 中的线程不会绑定 TaskScheduler,也就意味着之后的代码还是会在 ThreadPool 中的线程上执行,并且是本地队列优先,所以一直是 thread id: 4 这个线程在从本地队列中取出任务在执行。

线程池的介绍请参考我另一篇博客

https://www.cnblogs.com/eventhorizon/p/15316955.html

回到本文的主题,如果在 long-running task 使用了 await 调用一个 async 方法,就会导致为 long-running task 分配的独立线程提前退出,和我们的预期不符。

long-running task 中 调用 一个 async 方法的可能姿势

使用 Task.Wait

在 long-running task 中调用一个 async 方法,可以使用 Task.Wait 来阻塞当前线程,直到 async 方法执行完毕。

对于 Task.Factory.StartNew 创建出来的 long-running task 来说,因为其绑定了 ThreadPoolTaskScheduler,就算是使用 Task.Wait

阻塞了当前线程,也不会导致死锁。

并且 Task.Wait 会把异常抛出来,所以我们可以在 catch 中处理异常。

// ...
Task.Factory.StartNew( () =>
{
while (true)
{
var input = queue.Take();
Console.WriteLine($"Before process: thread id: {Thread.CurrentThread.ManagedThreadId}, task scheduler: {InternalCurrentTaskScheduler()}, thread pool: {Thread.CurrentThread.IsThreadPoolThread}");
ProcessAsync(input).Wait();
Console.WriteLine($"After process: thread id: {Thread.CurrentThread.ManagedThreadId}");
}
}, TaskCreationOptions.LongRunning);
// ...

输出如下:

1
Before process: thread id: 9, task scheduler: System.Threading.Tasks.ThreadPoolTaskScheduler, thread pool: False
You entered: 1, thread id: 5, task scheduler: , thread pool: True
After process: thread id: 9, task scheduler: System.Threading.Tasks.ThreadPoolTaskScheduler, thread pool: False
2
Before process: thread id: 9, task scheduler: System.Threading.Tasks.ThreadPoolTaskScheduler, thread pool: False
You entered: 2, thread id: 5, task scheduler: , thread pool: True
After process: thread id: 9, task scheduler: System.Threading.Tasks.ThreadPoolTaskScheduler, thread pool: False
3
Before process: thread id: 9, task scheduler: System.Threading.Tasks.ThreadPoolTaskScheduler, thread pool: False
You entered: 3, thread id: 5, task scheduler: , thread pool: True
After process: thread id: 9, task scheduler: System.Threading.Tasks.ThreadPoolTaskScheduler, thread pool: False
4
Before process: thread id: 9, task scheduler: System.Threading.Tasks.ThreadPoolTaskScheduler, thread pool: False
You entered: 4, thread id: 5, task scheduler: , thread pool: True
After process: thread id: 9, task scheduler: System.Threading.Tasks.ThreadPoolTaskScheduler, thread pool: False

Task.Wait 并不会对 async 方法内部产生影响,所以 async 方法内部的代码还是按照正常的逻辑执行。这边 ProcessAsync 方法内部打印的

thread id 没变纯粹是因为 ThreadPool 目前就只创建了一个线程,你可以疯狂输入看看结果。

关于 Task.Wait 的使用,可以参考我另一篇博客

https://www.cnblogs.com/eventhorizon/p/17481757.html

使用自定义的 TaskScheduler 来创建 long-running task

Task.Factory.StartNew(async () =>
{
while (true)
{
var input = queue.Take();
Console.WriteLine(
$"Before process: thread id: {Thread.CurrentThread.ManagedThreadId}, task scheduler: {InternalCurrentTaskScheduler()}, thread pool: {Thread.CurrentThread.IsThreadPoolThread}");
await ProcessAsync(input);
Console.WriteLine(
$"After process: thread id: {Thread.CurrentThread.ManagedThreadId}, task scheduler: {InternalCurrentTaskScheduler()}, thread pool: {Thread.CurrentThread.IsThreadPoolThread}");
}
}, CancellationToken.None, TaskCreationOptions.None, new CustomerTaskScheduler()); class CustomerTaskScheduler : TaskScheduler
{
// 这边的 BlockingCollection 只是举个例子,如果是普通的队列,配合锁也是可以的。
private readonly BlockingCollection<Task> _tasks = new BlockingCollection<Task>(); public CustomerTaskScheduler()
{
var thread = new Thread(() =>
{
foreach (var task in _tasks.GetConsumingEnumerable())
{
TryExecuteTask(task);
}
})
{
IsBackground = true
};
thread.Start();
} protected override IEnumerable<Task> GetScheduledTasks()
{
return _tasks;
} protected override void QueueTask(Task task)
{
_tasks.Add(task);
} protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
return false;
}
}

输出如下:

1
Before process: thread id: 9, task scheduler: CustomerTaskScheduler, thread pool: False
You entered: 1, thread id: 9, task scheduler: CustomerTaskScheduler, thread pool: False
After process: thread id: 9, task scheduler: CustomerTaskScheduler, thread pool: False
2
Before process: thread id: 9, task scheduler: CustomerTaskScheduler, thread pool: False
You entered: 2, thread id: 9, task scheduler: CustomerTaskScheduler, thread pool: False
After process: thread id: 9, task scheduler: CustomerTaskScheduler, thread pool: False
3
Before process: thread id: 9, task scheduler: CustomerTaskScheduler, thread pool: False
You entered: 3, thread id: 9, task scheduler: CustomerTaskScheduler, thread pool: False
After process: thread id: 9, task scheduler: CustomerTaskScheduler, thread pool: False
4
Before process: thread id: 9, task scheduler: CustomerTaskScheduler, thread pool: False
You entered: 4, thread id: 9, task scheduler: CustomerTaskScheduler, thread pool: False
After process: thread id: 9, task scheduler: CustomerTaskScheduler, thread pool: False

因为修改了上下文绑定的 TaskScheduler,会影响到 async 方法内部 await 回调的执行。

这种做法不推荐使用,因为可能会导致死锁。

如果我将 await 改成 Task.Wait,就会导致死锁。

Task.Factory.StartNew(() =>
{
while (true)
{
var input = queue.Take();
Console.WriteLine(
$"Before process: thread id: {Thread.CurrentThread.ManagedThreadId}, task scheduler: {InternalCurrentTaskScheduler()}, thread pool: {Thread.CurrentThread.IsThreadPoolThread}");
ProcessAsync(input).Wait();
Console.WriteLine(
$"After process: thread id: {Thread.CurrentThread.ManagedThreadId}, task scheduler: {InternalCurrentTaskScheduler()}, thread pool: {Thread.CurrentThread.IsThreadPoolThread}");
}
}, CancellationToken.None, TaskCreationOptions.None, new CustomerTaskScheduler());

输出如下:

1
Before process: thread id: 7, task scheduler: CustomerTaskScheduler, thread pool: False

后面就没有输出了,因为死锁了,除非我们在 ProcessAsync 方法内部每个 await 的 Task 后加上ConfigureAwait(false)。

同理,同学们也可以尝试用 SynchronizationContext 来实现类似的效果,同样有死锁的风险。

总结

如果你想要在一个 long-running task 中执行 async 方法,使用 await 关键字会导致 long-running task 的独立线程提前退出。

比较推荐的做法是使用 Task.Wait。如果连续执行多个 async 方法,建议将这些 async 方法封装成一个新方法,然后只 Wait 这个新方法的 Task。

如何在long-running task中调用async方法的更多相关文章

  1. 水火难容:同步方法调用async方法引发的ASP.NET应用程序崩溃

    之前只知道在同步方法中调用异步(async)方法时,如果用.Result等待调用结果,会造成线程死锁(deadlock).自己也吃过这个苦头,详见等到花儿也谢了的await. 昨天一个偶然的情况,造成 ...

  2. ASP.NET MVC 如何在一个同步方法(非async)方法中等待async方法

    问题 首先,在ASP.NET MVC 环境下对async返回的Task执行Wait()会导致线程死锁.例: public ActionResult Asv2() { //dead lock var t ...

  3. MVC 如何在一个同步方法(非async)方法中等待async方法

    MVC 如何在一个同步方法(非async)方法中等待async方法 问题 首先,在ASP.NET MVC 环境下对async返回的Task执行Wait()会导致线程死锁.例: public Actio ...

  4. angularjs 动态表单, 原生事件中调用angular方法

    1. 原生事件中调用angular方法, 比如 input的onChange事件想调用angular里面定义的方法 - onChange="angular.element(this).sco ...

  5. 【问题】Asp.net MVC 的cshtml页面中调用JS方法传递字符串变量参数

    [问题]Asp.net MVC 的cshtml页面中调用JS方法传递字符串变量参数. [解决]直接对变量加引号,如: <button onclick="deleteProduct('@ ...

  6. C# 构造函数中调用虚方法的问题

    请看下面代码: using System; public class A{ public A(){ M1(); } public virtual void M1(){} } public class ...

  7. 【09】绝不在构造和析构过程中调用virtual方法

    1.绝不在构造和析构过程中调用virtual方法,为啥? 原因很简单,对于前者,这种情况下,子类专有成分还没有构造,对于后者,子类专有成分已经销毁,因此调用的并不是子类重写的方法,这不是程序员所期望的 ...

  8. 避免在构造函数中调用虚方法(Do not call overridable methods in constructors)

    CLR中说道,不要在构造函数中调用虚方法,原因是假如被实例化的类型重写了虚方法,就会执行派生类型对虚方法的实现.但在这个时候,尚未完成对继承层次结构中所有字段的初始化.所以,调用虚方法会导致不可预测的 ...

  9. Python 在子类中调用父类方法详解(单继承、多层继承、多重继承)

    Python 在子类中调用父类方法详解(单继承.多层继承.多重继承)   by:授客 QQ:1033553122   测试环境: win7 64位 Python版本:Python 3.3.5 代码实践 ...

  10. JS与OC交互,JS中调用OC方法(获取JSContext的方式)

    最近用到JS和OC原生方法调用的问题,查了许多资料都语焉不详,自己记录一下吧,如果有误欢迎联系我指出. JS中调用OC方法有三种方式: 1.通过获取JSContext的方式直接调用OC方法 2.通过继 ...

随机推荐

  1. What's the best way to read and understand someone else's code?

    Find one thing you know the code does, and trace those actions backward, starting at the end Say, fo ...

  2. Burp Suite最新版本专业版激活2022.12.1附原文件

    Burp Suite 攻击web 应用程序的集成平台 Burp Suite 是用于攻击web 应用程序的集成平台,包含了许多工具.Burp Suite为这些工具设计了许多接口,以加快攻击应用程序的过程 ...

  3. 【LeetCode回溯算法#extra01】集合划分问题【火柴拼正方形、划分k个相等子集、公平发饼干】

    火柴拼正方形 https://leetcode.cn/problems/matchsticks-to-square/ 你将得到一个整数数组 matchsticks ,其中 matchsticks[i] ...

  4. 如何使用Redis做缓存

    如何使用Redis做缓存 我们都知道Redis作为NoSql数据库的代表之一,通常会用来作为缓存使用.也是我在工作中通常使用的缓存之一. 1.我们什么时候缓存需要用到Redis? 我认为,缓存可以分为 ...

  5. 随手记:redis 开发注意事项

    Redis开发建议 1.冷热数据分离,不要将所有数据全部都放到Redis中 虽然Redis支持持久化,但是Redis的数据存储全部都是在内存中的,成本昂贵.建议根据业务只将高频热数据存储到Redis中 ...

  6. Go For Web:Golang http 包详解(源码剖析)

    前言: 本文作为解决如何通过 Golang 来编写 Web 应用这个问题的前瞻,对 Golang 中的 Web 基础部分进行一个简单的介绍.目前 Go 拥有成熟的 Http 处理包,所以我们去编写一个 ...

  7. Django相关配置信息

    Django相关配置信息 1.配置数据库mysql 1.1 setting.py中配置信息 DATABASES = { 'default': { 'ENGINE': 'django.db.backen ...

  8. 简单理解重载运算符&位运算

    重载运算符 作用 重载运算符的作用大致可以理解为自定义一个运算法则,比如当我们在使用结构体的时候,我们有时候会用到优先队列,但是优先队列并不能对于结构体使用,所以这个时候我们就需要用到重载运算符来自定 ...

  9. C++ Primer 5th 阅读笔记:变量和基本类型

    一些语言的公共特性 内建类型,如整型,字符型等: 变量,为值绑定的一个名字: 表达式和语句,操作值. 分支和循环,允许我们条件执行和重复执行: 函数,定义抽象计算单元. 扩展语言的方式 自定义类型: ...

  10. OpenResty学习笔记03:再探WAF

    一. 再谈WAF 我们上一篇安装的WAF来自另一位技术大神 赵舜东,花名 赵班长,一直从事自动化运维方面的架构设计工作.阿里云MVP.华为云MVP.中国SaltStack用户组发起人 .新运维社区发起 ...