乐趣区

关于c#:我没能实现始终在一个线程上运行-task

前文咱们总结了在应用常驻工作实现常驻线程时,应该留神的事项。然而咱们最终没有提到如何在解决对于带有异步代码的方法。本篇将承受笔者对于该内容的总结。

如何辨认以后代码跑在什么线程上

所有开始之前,咱们先来应用一种简略的形式来辨认以后代码运行在哪种线程上。

最简略的形式就是打印以后线程名称和线程 ID 来辨认。

private static void ShowCurrentThread(string work)
{Console.WriteLine($"{work} - {Thread.CurrentThread.Name} - {Thread.CurrentThread.ManagedThreadId}");
}

通过这段代码,咱们能够非常容易的辨认三种不同状况下的线程信息。

[Test]
public void ShowThreadMessage()
{new Thread(() => {ShowCurrentThread("Custom thread work"); })
    {
        IsBackground = true,
        Name = "Custom thread"
    }.Start();

    Task.Run(() => { ShowCurrentThread("Task.Run work"); });
    Task.Factory.StartNew(() => { ShowCurrentThread("Task.Factory.StartNew work"); },
        TaskCreationOptions.LongRunning);

    Thread.Sleep(TimeSpan.FromSeconds(1));
}
// output
// Task.Factory.StartNew work - .NET Long Running Task - 17
// Custom thread work - Custom thread - 16
// Task.Run work - .NET ThreadPool Worker - 12

别离为:

  • 自定义线程 Custom thread
  • 线程池线程 .NET ThreadPool Worker
  • 由 Task.Factory.StartNew 创立的新线程 .NET Long Running Task

因而,联合咱们之前昙花线程的例子,咱们也能够非常简单的看出线程的切换状况:

[Test]
public void ShortThread()
{new Thread(async () =>
    {ShowCurrentThread("before await");
        await Task.Delay(TimeSpan.FromSeconds(0.5));
        ShowCurrentThread("after await");
    })
    {
        IsBackground = true,
        Name = "Custom thread"
    }.Start();
    Thread.Sleep(TimeSpan.FromSeconds(1));
}
// output
// before await - Custom thread - 16
// after await - .NET ThreadPool Worker - 6

咱们心愿在同一个线程上运行 Task 代码

之前咱们曾经晓得了,手动创立线程并控制线程的运行,能够确保本人的代码不会于线程池线程产生竞争,从而使得咱们的常驻工作可能稳固的触发。

过后用于演示的谬误示例是这样的:

[Test]
public void ThreadWaitTask()
{new Thread(async () =>
    {ShowCurrentThread("before await");
        Task.Run(() =>
        {ShowCurrentThread("inner task");
        }).Wait();
        ShowCurrentThread("after await");
    })
    {
        IsBackground = true,
        Name = "Custom thread"
    }.Start();
    Thread.Sleep(TimeSpan.FromSeconds(1));
}
// output
// before await - Custom thread - 16
// inner task - .NET ThreadPool Worker - 13
// after await - Custom thread - 16

这个示例能够显著的看出,两头的局部代码是运行在线程池的。这种做法会在线程池资源缓和的时候,导致咱们的常驻工作无奈触发。

因而,咱们须要一种形式来确保咱们的代码在同一个线程上运行。

那么接下来咱们剖析一些想法和成果。

加配!加配!加配!

咱们曾经晓得了,实际上,常驻工作不能稳固触发是因为 Task 会在线程池中运行。那么减少线程池的容量天然就是最间接解决顶峰的做法。
因而,如果条件容许的话,间接减少 CPU 外围数实际上是最为无效和简略的形式。

不过这种做法并不适用于一些类库的编写者。比方,你在编写日志类库,那么其实无奈欲知用户所处的环境。并且正如大家所见,市面上简直没有日志类库中由阐明让用户只能在肯定的 CPU 外围数下应用。

因而,如果您的常驻工作是在类库中,那么咱们须要一种更为通用的形式来解决这个问题。

思考应用同步重载

在 Task 呈现之后,很多时候咱们都会思考应用异步重载的办法。这显然不是谬误的做法,因为这能够使得咱们的代码更加高效,晋升零碎的吞吐量。然而,如果你想要让 Thread 稳固的在同一个线程上运行,那么你须要思考应用同步重载的办法。通过同步重载办法,咱们的代码将不会呈现线程切换到线程池的状况。天然也就实现了咱们的目标。

总是应用 TaskCreationOptions.LongRunning

这个方法其实很不理论。因为任何一层没有指定,都会将工作切换到线程池中。

[Test]
public void AlwaysLogRunning()
{new Thread(async () =>
    {ShowCurrentThread("before await");
        Task.Factory.StartNew(() =>
        {ShowCurrentThread("LongRunning task");
            Task.Run(() => { ShowCurrentThread("inner task"); }).Wait();}, TaskCreationOptions.LongRunning).Wait();
        ShowCurrentThread("after await");
    })
    {
        IsBackground = true,
        Name = "Custom thread"
    }.Start();
    Thread.Sleep(TimeSpan.FromSeconds(1));
}
// output
// before await - Custom thread - 16
// LongRunning task - .NET Long Running Task - 17
// inner task - .NET ThreadPool Worker - 7
// after await - Custom thread - 16

所以说,这个方法能够用。但其实很怪。

自定义 Scheduler

这是一种可行,然而十分艰难的做法。尽管说自定义个简略的 Scheduler 也不是很难,只须要实现几个简略的办法。但要依照咱们的需要来实现这个 Scheduler 并不简略。

比方咱们尝试实现一个这样的 Scheduler:

留神:这个 Scheduler 并不能失常工作。

class MyScheduler : TaskScheduler
{
    private readonly Thread _thread;
    private readonly ConcurrentQueue<Task> _tasks = new();

    public MyScheduler()
    {_thread = new Thread(() =>
        {while (true)
            {while (_tasks.TryDequeue(out var task))
                {TryExecuteTask(task);
                }
            }
        })
        {
            IsBackground = true,
            Name = "MyScheduler"
        };
        _thread.Start();}

    protected override IEnumerable<Task> GetScheduledTasks()
    {return _tasks;}

    protected override void QueueTask(Task task)
    {_tasks.Enqueue(task);
    }

    protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
    {return false;}
}

下面的代码中,咱们期待通过一个繁多的线程来执行所有的工作。但实际上它反而是一个非常简单的死锁演示安装。

咱们构想运行上面这段代码:

[Test]
public async Task TestLongRunningConfigureAwait()
{var scheduler = new MyScheduler();
    await Task.Factory.StartNew(() =>
    {ShowCurrentThread("BeforeWait");
        Task.Factory
            .StartNew(() =>
                {ShowCurrentThread("AfterWait");
                }
                , CancellationToken.None, TaskCreationOptions.None, scheduler)
            .Wait();
        ShowCurrentThread("AfterWait");
    }, CancellationToken.None, TaskCreationOptions.None, scheduler);
}

这段代码中,咱们期待,在一个 Task 中运行另外一个 Task。但实际上,这段代码会死锁。

因为,咱们的 MyScheduler 中,咱们在一个死循环中,一直的从队列中取出工作并执行。然而,咱们的工作中,又会调用 Wait 办法。

咱们无妨构想这个线程就是咱们本人。

  1. 首先,老板交代给你一件工作,你把它放到队列中。
  2. 而后你开始执行这件工作,执行到一半发现,你须要期待第二件工作的执行后果。因而你在这里等着。
  3. 然而第二件工作这个时候也塞到了你的队列中。
  4. 这下好了,你手头的工作在期待你队列外面的工作实现。而你队列的工作只有你能力实现。
  5. 完满卡死。

因而,其实实际上咱们须要在 Wait 的时候告诉以后线程,此时线程被 Block 了,而后转而从队列中取出工作执行。在 Task 于 ThreadPool 的配合中,是存在这样的机制的。然而,咱们本人实现的 MyScheduler 并不能与 Task 产生这种配合。因而须要思考自定义一个 Task。跟进一步说,咱们须要自定义 AsyncMethodBuilder 来实现全套的自定义。

显然者是一项绝对高级内容,期待理解的读者,能够通过 UniTask1 我的项目来理解如何实现这样的全套自定义。

总结

如果你冀望在常驻线程可能稳固的运行你的工作。那么:

  1. 加配,以防止线程池不够用
  2. 思考在这部分代码中应用同步代码
  3. 能够学习自定义 Task 零碎

参考

  • .NET Task 揭秘(2):Task 的回调执行与 await2
  • Task3
  • TaskCreationOptions4
  • 这样在 C# 应用 LongRunningTask 是错的 5
  • async 与 Thread 的谬误联合 6
  • 实现常驻工作除了防止昙花线程,还须要防止重返线程池 7

感谢您的浏览,如果您感觉本文有用,快点击下方点赞按钮👍,让更多的人看到本文。

欢送关注作者的微信公众号“newbe 技术专栏”,获取更多技术内容。

  • 本文作者:newbe36524
  • 本文链接:https://www.newbe.pro/Others/0x029-I-can-not-manage-to-always-run-task-on-one-thread/
  • 版权申明:本博客所有文章除特地申明外,均采纳 BY-NC-SA 许可协定。转载请注明出处!

  1. https://github.com/Cysharp/UniTask ↩
  2. https://www.cnblogs.com/eventhorizon/p/15912383.html ↩
  3. https://threads.whuanle.cn/3.task/ ↩
  4. https://learn.microsoft.com/en-us/dotnet/api/system.threading… ↩
  5. https://www.newbe.pro/Others/0x026-This-is-the-wrong-way-to-u… ↩
  6. https://www.newbe.pro/Others/0x027-error-when-using-async-wit… ↩
  7. https://www.newbe.pro/Others/0x028-avoid-return-to-threadpool… ↩
退出移动版