什么是 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. 【技巧存档】常用网站如CSDN打开时加载慢怎么办?

    找到最快站点,更改host文件 F12打开控制台,查看网络中哪些站点的请求标红,如 img-home.csdnimg.cn 去站长之家测试ping值,找到最低ping值的ip,这里找到安徽合肥,ip为 ...

  2. window远程桌面之通过修改端口链接

      windows开启及连接远程桌面 技术标签: 后端开发  windows         桌面 -> 此电脑 图标右键 -> 属性 远程设置 远程桌面 -> 修改为允许远程连接到 ...

  3. python之PySimpleGUI(一)元素

    1themesg.theme_previewer()获取所有主题颜色sg.preview_all_look_and_feel_themes()同上theme_name_list = sg.theme_ ...

  4. kali 安装beef-xss (含输入正确密码却登录不了本机无法打开beef提示找不到服务器404常见解决办法)

    1:安装beef 2:修改配置文件 3:浏览器打开地址127.0.0.1:3000/ui/panel 常见问题解决本文最后 kali 打开终端 输入apt-get install beef-xss - ...

  5. Java 框架面试题-Spring Boot自定义配置与自动配置共存

    Spring Boot 是一个快速开发框架,可以简化 Spring 应用程序的开发,其中自定义配置是其中一个非常重要的特性. 在 Spring Boot 中,自定义配置允许开发者以自己的方式来配置应用 ...

  6. Java学习笔记02

    1. 运算符和表达式 运算符 ​ 就是对常量或者变量进行操作的符号. ​ 如:+ - * / 表达式 ​ 用运算符把常量或者变量连接起来的,符合Java语法的式子就是表达式. ​ 如:a + b ​ ...

  7. axios文件下载!!!!

    前端 download(){ debugger; this.loading = true; axios.post('http://localhost:8081/brand_case/dao.do?me ...

  8. Java并发(三)----创建线程的三种方式及查看进程线程

    一.直接使用 Thread // 创建线程对象 Thread t = new Thread() {    public void run() {        // 要执行的任务   } }; // ...

  9. Mac + IOS + Safari 抓取网络请求

    第一步:打开苹果手机 设置>Safari浏览器>高级>网页检查器 第二步:打开 Mac 上的Safari浏览器>偏好设置>高级>在菜单栏中显示"开发&qu ...

  10. 深入理解python虚拟机:黑科技的幕后英雄——描述器

    深入理解python虚拟机:黑科技的幕后英雄--描述器 在本篇文章当中主要给大家介绍一个我们在使用类的时候经常使用但是却很少在意的黑科技--描述器,在本篇文章当中主要分析描述器的原理,以及介绍使用描述 ...