Compare commits
9 Commits
master
...
blog/async
| Author | SHA1 | Date | |
|---|---|---|---|
|
759c878404
|
|||
| 8c2cce59e4 | |||
| baf50eeab0 | |||
| dc1b97fed4 | |||
| 9bfe091024 | |||
| 3a4ada50c6 | |||
| 05a22a0b29 | |||
| 6797028cc1 | |||
| 77e52fa11e |
@@ -12,7 +12,6 @@
|
|||||||
<File Path="README.md" />
|
<File Path="README.md" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/src/">
|
<Folder Name="/src/">
|
||||||
<Project Path="src/YaeBlog.Abstractions/YaeBlog.Abstractions.csproj" />
|
|
||||||
<Project Path="src/YaeBlog.Tests/YaeBlog.Tests.csproj" />
|
<Project Path="src/YaeBlog.Tests/YaeBlog.Tests.csproj" />
|
||||||
<Project Path="src/YaeBlog/YaeBlog.csproj" />
|
<Project Path="src/YaeBlog/YaeBlog.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
|
|||||||
884
YaeBlog/source/drafts/how-async-await-works.md
Normal file
884
YaeBlog/source/drafts/how-async-await-works.md
Normal file
@@ -0,0 +1,884 @@
|
|||||||
|
---
|
||||||
|
title: async/await究竟是如何工作的?
|
||||||
|
tags:
|
||||||
|
- dotnet
|
||||||
|
- 技术笔记
|
||||||
|
- 译文
|
||||||
|
---
|
||||||
|
|
||||||
|
### 译者按
|
||||||
|
|
||||||
|
如何正确而快速的编写异步运行的代码一直是软件工程界的难题,而C#提出的`async/await`范式无疑是探索道路上的先行者。本篇文章便是翻译自.NET开发者博客上一篇名为“How async/await really works in C#”的文章,希望能够让读者在阅读之后明白`async/await`编程范式的前世今生和`.NET`实现方式。另外,.Net开发者中文博客也翻译了[这篇文章](https://devblogs.microsoft.com/dotnet-ch/async-await%e5%9c%a8-c%e8%af%ad%e8%a8%80%e4%b8%ad%e6%98%af%e5%a6%82%e4%bd%95%e5%b7%a5%e4%bd%9c%e7%9a%84/),一并供读者参考。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
数周前,[.NET开发者博客](https://devblogs.microsoft.com/dotnet/)发布了一篇题为[什么是.NET,为什么你应该选择.NET](https://devblogs.microsoft.com/dotnet/why-dotnet/)的文章。文章中从宏观上概览了整个`dotnet`生态系统,总结了系统中的各个部分和其中的设计决定;文章还承诺在未来推出一系列的深度文章介绍涉及到的方方面面。这篇文章便是这系列文章中的第一篇,深入介绍C#和.NET中`async/await`的历史、设计决定和实现细节。
|
||||||
|
|
||||||
|
对于`async/await`的支持大约在十年前就提供了。在这段时间里,`async/await`语法大幅改变了编写可扩展.NET代码的方式,同时该语法使得在不了解`async/await`工作原理的情况下使用它提供的功能编写异步代码也是十分容易和常见的。以下面的**同步**方法为例:(因为这个方法的调用者在整个操作完成之前、将控制权返回给它之前都不能进行任何操作,所以这个方法被称为**同步**)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 将数据同步地从源复制到目的地
|
||||||
|
public void CopyStreamToStream(Stream source, Stream destination)
|
||||||
|
{
|
||||||
|
var buffer = new byte[0x1000];
|
||||||
|
int numRead;
|
||||||
|
while ((numRead = source.Read(buffer, 0, buffer.Length)) != 0)
|
||||||
|
{
|
||||||
|
destination.Write(buffer, 0, numRead);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
在这个方法的基础上,你只需要修改几个关键词、改变几个方法的名称,就可以得到一个**异步**的方法(因为这个方法将很快,往往实在所有的工作完成之前,就会将控制权返回给它的调用者,所以被称作异步方法)。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 将数据异步地从源复制到目的地
|
||||||
|
public async Task CopyStreamToStreamAsync(Stream source, Stream destination)
|
||||||
|
{
|
||||||
|
var buffer = new byte[0x1000];
|
||||||
|
int numRead;
|
||||||
|
while ((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) != 0)
|
||||||
|
{
|
||||||
|
await destination.WriteAsync(buffer, 0, numRead);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
有着几乎相同的语法,类似的控制流结构,但是现在这个方法在执行过程中不会阻塞,有着完全不同的底层执行模型,而且C#编译器和核心库帮你完成所有这些复杂的工作。
|
||||||
|
|
||||||
|
尽管在不了解底层原理的基础上使用这类技术是十分普遍的,但是我们坚持认为了解这些事务的运行原理将会帮助我们更好的利用它们。之于`async/await`,了解这些原理将在你需要深入探究时十分有用,例如当你需要调试一段错误的代码或者优化某段正确运行代码的运行效率时。在这篇文章中,我们将深入了解`async/await`具体如何在语言、编译器和库层面运行,然后你将更好地利用这些优秀的设计。
|
||||||
|
|
||||||
|
为了更好的理解这一切,我们将回到没有`async/await`的时代,看看在没有它们的情况下最优秀的异步代码是如何编写的。平心而论,这些代码看上去并不好。
|
||||||
|
|
||||||
|
### 原初的历史
|
||||||
|
|
||||||
|
回到.NET框架1.0时代,当时流行的异步编程范式是**异步编程模型**,“Asynchronous Programming Model”,也被称作`APM`范式、`Being/End`范式或者`IAsyncResult`范式。从宏观上来看,这种范式是相当简单的。例如对于一个同步操作`DoStuff`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
class Handler
|
||||||
|
{
|
||||||
|
public int DoStuff(string arg);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
在这种编程模型下会存在两个相关的方法:一个`BeginStuff`方法和一个`EndStuff`方法:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
class Handler
|
||||||
|
{
|
||||||
|
public int DoStuff(string arg);
|
||||||
|
|
||||||
|
public IAsyncResult BeginDoStuff(string arg, AsyncCallback? callback, object? state);
|
||||||
|
public int EndDoStuff(IAsyncResult asyncResult);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`BeginStuff`方法首先会接受所有`DoStuff`方法会接受的参数,同时其会接受一个`AsyncCallback`回调和一个**不透明**的状态对象`state`,而且这两个参数都可以为空。这个“开始”方法将负责异步操作的初始化,而且如果提供了回调函数,这个函数还会负责在异步操作完成之后调用这个回调函数,因此这个回调函数也常常被称为初始化操作的“下一步”。开始方法还会负责构建一个实现了`IAsyncResult`接口的对象,这个对象中的`AsyncState`属性由可选的`state`参数提供:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
namespace System
|
||||||
|
{
|
||||||
|
public interface IAsyncResult
|
||||||
|
{
|
||||||
|
object? AsyncState { get; }
|
||||||
|
WaitHandle AsyncWaitHandle { get; }
|
||||||
|
bool IsCompleted { get; }
|
||||||
|
bool CompletedSynchronously { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate void AsyncCallback(IAsyncResult ar);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这个`IAsynResult`实例将会被开始方法返回,在调用`AsyncCallback`时这个实例也会被传递过去。当准备好使用该异步操作的结果时,调用者也会将这个`IAsyncResult`实例传递给结束方法,同时结束方法也会负责保证这个异步操作完成,如果没有完成该方法就会阻塞代码的运行直到完成。结束方法会返回异步操作的结果,异步操作过程中引发的各种错误和异常也会通过该方法传递出来。因此,对于下面这种同步的操作:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int i = handler.DoStuff(arg);
|
||||||
|
Use(i);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
... // 在这里处理DoStuff方法和Use方法中引发的各种异常
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
可以使用开始/结束方法改写为异步运行的形式:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
try
|
||||||
|
{
|
||||||
|
handler.BeginDoStuff(arg, iar =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Handler handler = (Handler)iar.AsyncState!;
|
||||||
|
int i = handler.EndDoStuff(iar);
|
||||||
|
Use(i);
|
||||||
|
}
|
||||||
|
catch (Exception e2)
|
||||||
|
{
|
||||||
|
... // 处理从EndDoStuff方法和Use方法中引发的各种异常
|
||||||
|
}
|
||||||
|
}, handler);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
... // 处理从同步调用BeginDoStuff方法引发的各种异常
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
对于熟悉使用含有回调`API`语言的开发者来说,这样的代码应该会显得相当眼熟。
|
||||||
|
|
||||||
|
但是事情在这里变得更加复杂了。例如,这段代码存在“栈堆积”`stack dive`的问题。栈堆积就是代码在重复的调用方法中使得栈越来越深,直到发生栈溢出的现象。如果“异步”操作同步完成,开始方法将会使同步的调用回调方法,这就意味着对于开始方法的调用就会直接调用回调方法。同时考虑到“异步”方法同步完成却是一种非常常见的现象,它们只是承诺会异步的完成操作而不是只被允许异步的完成。例如一个对于某个网络操作的异步操作,比如读取一个套接字,如果你只需要从一次操作中读取少量的数据,例如在一次回答中只需要读取少量响应头的数据,你可能会直接读取大量数据存储在缓冲区中。相比于每次使用都使用系统调用但是只读取少量的数据,你一次读取了大量数据在缓冲区中,并在缓冲区失效之前都是从缓冲区中读取,这样就减少了需要调用昂贵的系统调用来和套接字交互的次数。像这样的缓冲区可能在你进行任何异步调用之后存在,例如第一次操作异步的完成对于缓冲区的填充,之后的若干次“异步”操作都不需要同I/O进行任何交互而直接通过与缓冲区的同步交互完成,直到缓冲区失效之后再次异步的填充缓冲区。因此当开始方法进行上述的一次调用时,开始方法会发现操作同步地完成了,因此开始方法同步地调用回调方法。此时,你有一个调用了开始方法的栈帧和一个调用了回调方法的栈帧。想想看如果回调方法再次调用了开始方法会发生什么?如果开始方法和回调方法都是被同步调用的,现在你就会在站上得到多个重复的栈帧,如此重复下去直到将栈上的空间耗尽。
|
||||||
|
|
||||||
|
这并不是杞人忧天,使用下面这段代码就可以很容易的复现这个问题:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
|
||||||
|
using Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||||
|
listener.Bind(new IPEndPoint(IPAddress.Loopback, 0));
|
||||||
|
listener.Listen();
|
||||||
|
|
||||||
|
using Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||||
|
client.Connect(listener.LocalEndPoint!);
|
||||||
|
|
||||||
|
using Socket server = listener.Accept();
|
||||||
|
_ = server.SendAsync(new byte[100_000]);
|
||||||
|
|
||||||
|
var mres = new ManualResetEventSlim();
|
||||||
|
byte[] buffer = new byte[1];
|
||||||
|
|
||||||
|
var stream = new NetworkStream(client);
|
||||||
|
|
||||||
|
void ReadAgain()
|
||||||
|
{
|
||||||
|
stream.BeginRead(buffer, 0, 1, iar =>
|
||||||
|
{
|
||||||
|
if (stream.EndRead(iar) != 0)
|
||||||
|
{
|
||||||
|
ReadAgain(); // uh oh!
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mres.Set();
|
||||||
|
}
|
||||||
|
}, null);
|
||||||
|
};
|
||||||
|
ReadAgain();
|
||||||
|
|
||||||
|
mres.Wait();
|
||||||
|
```
|
||||||
|
|
||||||
|
在代码中我们建立一个简单的客户端套接字和一个简单的服务端套接字并让它们连接。服务端会向客户端发送十万字节的信息,而客户端会使用开始/结束方法尝试去“异步的”接收这些信息(需要注意这样做是十分低效的,在教学实例之外的地方都不应该这样编写代码)。传递给`BeingRead`的回调函数通过调用`EndRead`方法停止读取,如果在读取过程中读取到数据(意味着还没有读取完成),就通过对于本地方法`ReadAgain`的递归调用来再次调用`BeingRead`方法继续读取。值得指出的是,在.NET Core中套接字操作比原来在.NET Framework中的版本快上许多,同时如果操作系统可以同步的完成这些操作,那么.NET Core中的操作也会同步完成(需要注意操作系统内核也有一个缓冲区来完成套接字接收操作)。因此,运行这段代码就会出现栈溢出。
|
||||||
|
|
||||||
|
鉴于这个问题非常容易出现,因此`APM`模型中内建了缓解这个问题的方法。容易想到有两种方法可以缓解这个问题:
|
||||||
|
|
||||||
|
1. 不允许`AsyncCallback`被同步调用。如果该回调方法始终都是被异步调用的,即使操作是异步完成的,栈堆叠的方法也就不存在了。但是这样做会降低性能,因为同步完成的操作(或者快到难以注意到的操作)是相当的常见的,强制这些操作的回调排队完成会增加相当可观的开销。
|
||||||
|
2. 引入一个机制让调用者而不是回调函数在工作异步完成时完成剩余的工作。在这种情况下,我们就避免了引入额外的栈帧,在不增加栈深度的情况下完成了余下的工作。
|
||||||
|
|
||||||
|
`APM`模型使用了第二种方法。为了实现这个方法,`IAsyncResult`接口提供了另外两个成员:`IsCompleted`和`CompletedSynchronusly`。`IsCompeleted`成员告诉我们操作是否完成,在程序中可以反复检查这个成员直到它从`false`变成`true`。相对的,`CompletedSynchronously`在运行过程中不会变化,(或者它存在一个还未被发现的`bug`会导致这个值变化,笑),这个值的主要作用是判断后续的工作是应该由开始方法的调用者还是`AsyncCallback`来进行。如果`CompletedSynchronously`的值是`false`,说明这个操作是异步进行的,所有后续的工作应该由回调函数来进行处理;毕竟,如果工作是异步完成的,开始方法的调用者不能知道工作是何时完成的(如果开始方法的调用者调用了结束方法,那么结束方法就会阻塞直到工作完成)。反之,如果`CompletedSynchronously`的值是`true`,如果此时使用回调方法处理后续的工作就会引发栈堆叠问题,因为此时回调方法会在栈上比开始它更低的位置上进行后续的操作。因此任何在意栈堆叠问题的实现需要关注`CompletedSynchronously`的值,当为真的时候,让开始方法的调用者处理后续的工作,而回调方法在此时不应处理任何工作。这也是为什么`CompletedSynchronously`的值不能改变——开始方法的调用者和回调方法需要相同的值来保证后续工作在任何情况下都进行且只进行一次。
|
||||||
|
|
||||||
|
因此我们之前的`DoStuff`实例就需要被修改为:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IAsyncResult ar = handler.BeginDoStuff(arg, iar =>
|
||||||
|
{
|
||||||
|
if (!iar.CompletedSynchronously)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Handler handler = (Handler)iar.AsyncState!;
|
||||||
|
int i = handler.EndDoStuff(iar);
|
||||||
|
Use(i);
|
||||||
|
}
|
||||||
|
catch (Exception e2)
|
||||||
|
{
|
||||||
|
... // handle exceptions from EndDoStuff and Use
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, handler);
|
||||||
|
if (ar.CompletedSynchronously)
|
||||||
|
{
|
||||||
|
int i = handler.EndDoStuff(ar);
|
||||||
|
Use(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
... // handle exceptions that emerge synchronously from BeginDoStuff and possibly EndDoStuff/Use
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这里的代码已经~~显得冗长~~,而且我们还只研究了如何使用这种范式,还没有涉及如何实现这种范式。尽管大部分的开发者并不需要在这些子调用(例如实现`Socket.BeginReceive/EndReceive`这些方法去和操作系统交互),但是很多开发者需要组合这些操作(从一个“较大的”的异步操作调用多个异步操作),而这不仅需要使用其他的开始/结束方法,还需要自行实现你自己的开始/结束方法,这样你才能在其他的地方使用这个操作。同时,你还会注意到在上述的`DoStuff`范例中没有任何的控制流代码。如果需要引入一些控制流代码——即使是一个简单的循环——这也会立刻变成~~抖M才会编写的代码~~,同时也给无数的博客作者提供水`CSDN`的好题材。
|
||||||
|
|
||||||
|
所以让我们现在就来写一篇`CSDN`,给出一个完成的实例。在文章的开头我展示了一个`CopyStreamToStream`方法,这个方式会将一个流中的数据复制到另外一个流中(就是`Stream.CopyTo`方法所完成的工作,但是为了说明,让我们假设这个方法并不存在):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public void CopyStreamToStream(Stream source, Stream destination)
|
||||||
|
{
|
||||||
|
var buffer = new byte[0x1000];
|
||||||
|
int numRead;
|
||||||
|
while ((numRead = source.Read(buffer, 0, buffer.Length)) != 0)
|
||||||
|
{
|
||||||
|
destination.Write(buffer, 0, numRead);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
直白的说,我们只需要不停的从一个流中读取数据然后写入到另外一个流中,直到我们没法从第一个流中读取到任何数据。现在让我们使用`APM`模型使用这个操作的异步模式吧:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public IAsyncResult BeginCopyStreamToStream(
|
||||||
|
Stream source, Stream destination,
|
||||||
|
AsyncCallback callback, object state)
|
||||||
|
{
|
||||||
|
var ar = new MyAsyncResult(state);
|
||||||
|
var buffer = new byte[0x1000];
|
||||||
|
|
||||||
|
Action<IAsyncResult?> readWriteLoop = null!;
|
||||||
|
readWriteLoop = iar =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (bool isRead = iar == null; ; isRead = !isRead)
|
||||||
|
{
|
||||||
|
if (isRead)
|
||||||
|
{
|
||||||
|
iar = source.BeginRead(buffer, 0, buffer.Length, static readResult =>
|
||||||
|
{
|
||||||
|
if (!readResult.CompletedSynchronously)
|
||||||
|
{
|
||||||
|
((Action<IAsyncResult?>)readResult.AsyncState!)(readResult);
|
||||||
|
}
|
||||||
|
}, readWriteLoop);
|
||||||
|
|
||||||
|
if (!iar.CompletedSynchronously)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int numRead = source.EndRead(iar!);
|
||||||
|
if (numRead == 0)
|
||||||
|
{
|
||||||
|
ar.Complete(null);
|
||||||
|
callback?.Invoke(ar);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
iar = destination.BeginWrite(buffer, 0, numRead, writeResult =>
|
||||||
|
{
|
||||||
|
if (!writeResult.CompletedSynchronously)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
destination.EndWrite(writeResult);
|
||||||
|
readWriteLoop(null);
|
||||||
|
}
|
||||||
|
catch (Exception e2)
|
||||||
|
{
|
||||||
|
ar.Complete(e);
|
||||||
|
callback?.Invoke(ar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, null);
|
||||||
|
|
||||||
|
if (!iar.CompletedSynchronously)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
destination.EndWrite(iar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
ar.Complete(e);
|
||||||
|
callback?.Invoke(ar);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
readWriteLoop(null);
|
||||||
|
|
||||||
|
return ar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EndCopyStreamToStream(IAsyncResult asyncResult)
|
||||||
|
{
|
||||||
|
if (asyncResult is not MyAsyncResult ar)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(null, nameof(asyncResult));
|
||||||
|
}
|
||||||
|
|
||||||
|
ar.Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class MyAsyncResult : IAsyncResult
|
||||||
|
{
|
||||||
|
private bool _completed;
|
||||||
|
private int _completedSynchronously;
|
||||||
|
private ManualResetEvent? _event;
|
||||||
|
private Exception? _error;
|
||||||
|
|
||||||
|
public MyAsyncResult(object? state) => AsyncState = state;
|
||||||
|
|
||||||
|
public object? AsyncState { get; }
|
||||||
|
|
||||||
|
public void Complete(Exception? error)
|
||||||
|
{
|
||||||
|
lock (this)
|
||||||
|
{
|
||||||
|
_completed = true;
|
||||||
|
_error = error;
|
||||||
|
_event?.Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Wait()
|
||||||
|
{
|
||||||
|
WaitHandle? h = null;
|
||||||
|
lock (this)
|
||||||
|
{
|
||||||
|
if (_completed)
|
||||||
|
{
|
||||||
|
if (_error is not null)
|
||||||
|
{
|
||||||
|
throw _error;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
h = _event ??= new ManualResetEvent(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
h.WaitOne();
|
||||||
|
if (_error is not null)
|
||||||
|
{
|
||||||
|
throw _error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public WaitHandle AsyncWaitHandle
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (this)
|
||||||
|
{
|
||||||
|
return _event ??= new ManualResetEvent(_completed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CompletedSynchronously
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (this)
|
||||||
|
{
|
||||||
|
if (_completedSynchronously == 0)
|
||||||
|
{
|
||||||
|
_completedSynchronously = _completed ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _completedSynchronously == 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsCompleted
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (this)
|
||||||
|
{
|
||||||
|
return _completed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
~~Yowsers~~。即使写完了这些繁文缛节,这实际上仍然不是一个完美的实现。例如,`IAsyncResult`的实现会在每次操作时上锁,而不是在任何可能的时候都使用无锁的实现;异常也是以原始的模型存储,如果使用`ExceptionDispatchInfo`可以让异常在传播的过程中含有调用栈的信息,在每次操作中都分配了大量的空间来存储变量(例如在每次`BeingWrite`调用时都会分配一片空间来存储委托),如此等等。现在想象这就是你每次编写方法时需要做的工作,每次当你需要编写一个可重用的异步方法来使用另外一个异步方法时,你需要自己完成上述所有的工作。而且如果你需要编写使用多个不同的`IAsyncResult`的可重用代码——就像在`async/await`范式中`Task.WhenAll`所完成的那样,难度又上升了一个等级;每个不同操作都会实现并暴露针对相关的`API`,这让编写一套逻辑代码并简单的复用它们也变得不可能(尽管一些库作者可能会通过提供一层针对回调方法的新抽象来方便开发者编写需要访问暴露`API`的回调方法)。
|
||||||
|
|
||||||
|
上述这些复杂性也说明只有很少的一部分人尝试过这样编写代码,而且对于这些人来说,`bug`也往往如影随形。而且这并不是一个`APM`范式的黑点,这是所有使用基于回调的异步方法都具有的缺点。我们已经十分习惯现代语言都有的控制流结构所带来的强大和便利,因此使用会破坏这种结构的基于回调的异步方式会带来大量的复杂性也是可以理解的。同时,也没有任何主流的语言提供了更好的替代。
|
||||||
|
|
||||||
|
我们需要一种更好的办法,一个既继承了我们在`APM`范式中所学习到所有经验也规避了其所有的各种缺点的方式。一个有趣的点是,`APM`范式只是一种编程范式,运行时、核心库和编译器在使用或者实现这种范式的过程中没有提供任何协助。
|
||||||
|
|
||||||
|
### 基于事件的异步范式
|
||||||
|
|
||||||
|
在.NET Framework 2.0中提供了一系列的`API`来实现一种不同的异步编程范式,当时设想这种范式的主要应用场景是客户端应用程序。这种基于事件的异步范式,也被称作`EAP`范式,也是以提供一系列成员的方式提供的,包含一个用于初始化异步操作的方式和一个监听异步操作是否完成的事件。因此上述的`DoStuff`示例可能会暴露如下的一系列成员:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
class Handler
|
||||||
|
{
|
||||||
|
public int DoStuff(string arg);
|
||||||
|
|
||||||
|
public void DoStuffAsync(string arg, object? userToken);
|
||||||
|
public event DoStuffEventHandler? DoStuffCompleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate void DoStuffEventHandler(object sender, DoStuffEventArgs e);
|
||||||
|
|
||||||
|
public class DoStuffEventArgs : AsyncCompletedEventArgs
|
||||||
|
{
|
||||||
|
public DoStuffEventArgs(int result, Exception? error, bool canceled, object? userToken) :
|
||||||
|
base(error, canceled, usertoken) => Result = result;
|
||||||
|
|
||||||
|
public int Result { get; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
首先通过`DoStuffCompleted`事件注册需要在完成异步操作时进行的工作然后调用`DoStuff`方法,这个方法将初始化异步操作,一旦异步操作完成,`DoStuffCompleted`事件将会被调用者引发。已经注册的回调方法可以运行剩余的工作,例如验证提供的`userToken`是否是期望的`userToken`,同时我们可以注册多个回调方法在异步操作完成的时候运行。
|
||||||
|
|
||||||
|
这个范式确实让一系列用例的编写更好编写,同时也让一系列用例变得更加复杂(例如上述的`CopyStreamToStream`例子)。这种范式的影响范围并不大,只在一次.NET Framework的更新中引入便匆匆地消失了,除了留下了一系列为了支持这种范式而实现的`API`,例如:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
class Handler
|
||||||
|
{
|
||||||
|
public int DoStuff(string arg);
|
||||||
|
|
||||||
|
public void DoStuffAsync(string arg, object? userToken);
|
||||||
|
public event DoStuffEventHandler? DoStuffCompleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate void DoStuffEventHandler(object sender, DoStuffEventArgs e);
|
||||||
|
|
||||||
|
public class DoStuffEventArgs : AsyncCompletedEventArgs
|
||||||
|
{
|
||||||
|
public DoStuffEventArgs(int result, Exception? error, bool canceled, object? userToken) :
|
||||||
|
base(error, canceled, usertoken) => Result = result;
|
||||||
|
|
||||||
|
public int Result { get; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
但是这种编程范式确实在`APM`范式所没有注意到的地方前进了一大步,并且这一点还保留到了我们今天所介绍的模型中:[同步上下文](https://github.com/dotnet/runtime/blob/967a59712996c2cdb8ce2f65fb3167afbd8b01f3/src/libraries/System.Private.CoreLib/src/System/Threading/SynchronizationContext.cs#L6) (`SynchronizationContext`)。
|
||||||
|
|
||||||
|
同步上下文作为一个对于通用调度器的实现,也是在.NET Framework中引入的。在实践中,同步上下文最常用的方法是`Post`,这个方法将一个工作实现传递给上下文所代表的一种调度器。举例来说,一个基础的同步上下文实现是一个线程池`ThreadPool`,因此`Post`方法的典型实现就是`ThreadPool.QueueUserWorkItem`方法,这个方法将让线程池在池中任意的线程上以指定的状态调用指定的委托。然而,同步上下文的巧妙之处不仅在于提供了对于不同调度器的支持,而是提供了一种针对不同的应用模型使用不同调度方法的抽象能力。
|
||||||
|
|
||||||
|
考虑像Windows Forms之类的`UI`框架。对于大多数工作在Windows上的`UI`框架来说,控件往往关联到一个特定的线程,这个线程负责运行一个消息管理中心,这个中心用来运行那些需要同控件交互的工作:只有这个控件有能力来修改控件,任何其他试图同控件进行交互的线程都需要发送消息到这个消息控制中心。Windows Forms通过一系列方法来实现这一点,例如`Control.BeingInvoke`,这类方法将会把提供的委托和参数传递给同这个控件相关联的线程来运行。你可以写出如下的代码:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private void button1_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
ThreadPool.QueueUserWorkItem(_ =>
|
||||||
|
{
|
||||||
|
string message = ComputeMessage();
|
||||||
|
button1.BeginInvoke(() =>
|
||||||
|
{
|
||||||
|
button1.Text = message;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这段代码首先将`ComputeMessage`方法交给线程池中的一个线程运行(这样可以保证该方法在运行时`UI`界面不会卡死),当上述工作完成之后,再将一个更新`button1`标签的委托传递给关联到`button1`的线程运行。简单而易于理解。在`WPF`框架中也是类似的逻辑,使用一个被称为`Dispatcher`的类型:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private void button1_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
ThreadPool.QueueUserWorkItem(_ =>
|
||||||
|
{
|
||||||
|
string message = ComputeMessage();
|
||||||
|
button1.Dispatcher.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
button1.Content = message;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`.NET MAUI`亦然。但是如果我想将这部分的逻辑封装到一个独立的辅助函数中,例如下面这种:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 调用ComputeMessage然后触发更新逻辑
|
||||||
|
internal static void ComputeMessageAndInvokeUpdate(Action<string> update) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
这样我就可以直接:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private void button1_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
ComputeMessageAndInvokeUpdate(message => button1.Text = message);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
但是`ComputerMessageAndInvokeUpdate`应该如何实现才能适配各种类型的应用程序呢?难道需要硬编码所有可能涉及的`UI`框架吗?这就是`SynchronizationContext`大显神威的地方,我们可以这样实现这个方法:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
internal static void ComputeMessageAndInvokeUpdate(Action<string> update)
|
||||||
|
{
|
||||||
|
SynchronizationContext? sc = SynchronizationContext.Current;
|
||||||
|
ThreadPool.QueueUserWorkItem(_ =>
|
||||||
|
{
|
||||||
|
string message = ComputeMessage();
|
||||||
|
if (sc is not null)
|
||||||
|
{
|
||||||
|
sc.Post(_ => update(message), null);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
update(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
在这个实现中将`SynchronizationContext`作为同`UI`进行交互的调度器之抽象。任何应用程序模型都需要保证在`SynchronizationContext.Current`属性上注册一个继承了`SynchronizationContext`的类,这个就会完成调度相关的工作。例如在`Windows Forms`中:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class WindowsFormsSynchronizationContext : SynchronizationContext, IDisposable
|
||||||
|
{
|
||||||
|
public override void Post(SendOrPostCallback d, object? state) =>
|
||||||
|
_controlToSendTo?.BeginInvoke(d, new object?[] { state });
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
在`WPF`中有:
|
||||||
|
|
||||||
|
```
|
||||||
|
public sealed class DispatcherSynchronizationContext : SynchronizationContext
|
||||||
|
{
|
||||||
|
public override void Post(SendOrPostCallback d, Object state) =>
|
||||||
|
_dispatcher.BeginInvoke(_priority, d, state);
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`ASP.NET`*曾经*也有过一个实现,尽管Web框架实际上并不关心是哪个线程在运行指定的工作,但是非常关心指定工作和那个请求相关,因此该实现主要负责保证多个线程不会在同时访问同一个`HttpContext`。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
internal sealed class AspNetSynchronizationContext : AspNetSynchronizationContextBase
|
||||||
|
{
|
||||||
|
public override void Post(SendOrPostCallback callback, Object state) =>
|
||||||
|
_state.Helper.QueueAsynchronous(() => callback(state));
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这个概念也并不局限于像上面的主流应用程序模型。例如在[xunit](https://github.com/xunit/xunit),一个流行的单元测试框架(`.NET`核心代码仓库也使用了)中也实现了需要自定义的`SynchronizationContext`。例如限制同步运行单元测试时同时运行单元测试数量就可以用`SynchroniaztionContext`实现:
|
||||||
|
|
||||||
|
```
|
||||||
|
public class MaxConcurrencySyncContext : SynchronizationContext, IDisposable
|
||||||
|
{
|
||||||
|
public override void Post(SendOrPostCallback d, object? state)
|
||||||
|
{
|
||||||
|
var context = ExecutionContext.Capture();
|
||||||
|
workQueue.Enqueue((d, state, context));
|
||||||
|
workReady.Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`MaxConcurrentSyncContext`中的`Post`方法只是将需要完成的工作压入其内部的工作队列中,这样就能够控制同时多少工作能够并行的运行。
|
||||||
|
|
||||||
|
那么同步上下文这个概念时如何同基于事件的异步范式关联起来的呢?`EAP`范式和同步上下文都是在同一时间引入的,而`EAP`范式要求当异步操作启动的时候,完成事件需要由当前`SynchronizationContext`进行调度。为了简化这个过程(可能反而引入多余的复杂性),在`System.ComponentModel`命名控件中引入了一些帮助程序,具体来说是`AsyncOperation`和`AsyncOperationManager`。其中前者是一个由用户提供的状态对象和捕获到的`SynchronizationContext`组成的元组,后者是一个捕获`SynchronizationContext`和创建`AsyncOperation`对象的工厂类。`EAP`范式会在实现中使用上述帮助类,例如`Ping.SendAsync`会首先调用`AsyncOperationManager.CreateOperationi`来捕获同步上下文,然后当异步操作完成的时候调用`AsyncOperation.PostOperationCompleted`方法来调用捕获到的`SynchronizationContext.Post`方法。
|
||||||
|
|
||||||
|
`SynchronizationContext`还提供了其他一些后面会用到的小工具。这个类暴露了`OperationStarted`和`OperationCompleted`两个方法。这个虚方法在基类中的实现都是空的,并不完成任何工作。但是继承其的实现可能会重载这些来了解运行中的操作。`EAP`的实现就会在每个操作开始和结束的时候调用`OperationStarted`和`OperationCompleted`,来方便可能存在的同步上下文跟踪工作的进度。鉴于在`EAP`范式中启动异步操作的方法往往不会返回任何东西,不能指望可以获得任何帮助你跟踪工作进度的东西,因而可能获得工作进度的同步上下文就显得很有价值了。
|
||||||
|
|
||||||
|
综上所说,我们需要一些比`APM`编程范式更好的东西,而`EAP`范式引入了一些新的东西,但是没有解决我们面对的核心问题,我们仍然需要一些更好的东西。
|
||||||
|
|
||||||
|
### 进入Task时代
|
||||||
|
|
||||||
|
在.NET Framework 4.0中引入了`System.Threading.Tasks.Task`类型。当时`Task`类型还只代表某些异步操作的最终完成(在其他编程框架中可能成称为`promise`或者`future`)。当一个操作开始时,创建一个`Task`来表示这个操作,当这个操作完成之后,操作的结果就会被保存在这个`Task`中。简单而明确。但是`Task`相较于`IAsyncResult`提供的重要特点是其蕴含了一个任务在持续运行的状态。这个特点让你能够随意找到一个`Task`,让它在异步操作完成的时候异步的通知你,而不用你关注任务当前是处在已经完成、没有完成、正在完成等各种状态。为什么这点非常重要?首先想想`APM`范式中存在的两个主要问题:
|
||||||
|
|
||||||
|
1. 你需要对每个操作实现一个自定义的`IAsycResult`实现:库中没有任何内置开箱即用的`IAsycResult`实现。
|
||||||
|
2. 你需要在调用开始方法之前就知道在操作结束的时候需要做什么。这让编写使用任意异步操作的组合代码或者通用运行时非常困难。
|
||||||
|
|
||||||
|
相对的,`Task`提供了一个通用的接口让你在启动一个异步操作之后“接触”这个操作,还提供了针对“持续”的抽象,这样你就不需要为启动异步操作的方法提供一个持续性。任何需要进行异步操作的人都可以产生一个`Task`,任何人需要使用异步操作的人都可以使用一个`Task`,在这个过程中不用自定义任何东西,`Task`成为了沟通异步操作的生产者和消费者之间最重要的桥梁。这一点大大改变了.NET框架。
|
||||||
|
|
||||||
|
现在让我们深入理解`Task`所带来的重要意义。与其直接去研究错综复杂的`Task`源代码,我们将尝试去实现一个`Task`的简单版本。这不会是一个完善的实现,只会完成基础的功能来让我们更好的理解什么是`Task`,即一个负责协调设置和存储完成信号的数据结构。
|
||||||
|
|
||||||
|
开始时`Task`中只有很少的字段:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
class MyTask
|
||||||
|
{
|
||||||
|
private bool _completed;
|
||||||
|
private Exception? _error;
|
||||||
|
private Action<MyTask>? _continuation;
|
||||||
|
private ExecutionContext? _ec;
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
我们首先需要一个字段告诉我们任务是否完成`_completed`,一个字段存储造成任务执行失败的错误`_error`;如果我们需要实现一个泛型的`MyTask<TResult>`,还需要一个`private TResult _result`字段来存储操作运行完成之后的结果。到目前为止的实现和`IAsyncResult`相关的实现非常类似(当然这不是一个巧合)。`_continuation`字段时实现中最重要的字段。在这个简单的实现中,我们只支持一个简单的后续过程,在真正的`Task`实现中是一个`object`类型的字段,这样既可以是一个独立的后续过程,也可以是一个后续过程的列表。这个委托会在任务完成的时候调用。
|
||||||
|
|
||||||
|
让我们继续深入。如上所述,`Task`相较于之前的异步执行模型一个基础的优势是在异步操作开始之后再提供后续需要完成的工作。因此我们需要一个方法来实现这个功能:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public void ContinueWith(Action<MyTask> action)
|
||||||
|
{
|
||||||
|
lock (this)
|
||||||
|
{
|
||||||
|
if (_completed)
|
||||||
|
{
|
||||||
|
ThreadPool.QueueUserWorkItem(_ => action(this));
|
||||||
|
}
|
||||||
|
else if (_continuation is not null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Unlike Task, this implementation only supports a single continuation.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_continuation = action;
|
||||||
|
_ec = ExecutionContext.Capture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
如果在调用`ContinueWith`的时候异步操作已经完成,那么就直接将该委托的执行加入执行队列。反之,这个方法就会将存储这个委托,当异步任务完成的时候进行执行(这个方法同时也存储一个被称为`ExecutionContext`的对象,会在后续调用委托的涉及到,我们后续会继续介绍)。
|
||||||
|
|
||||||
|
然后我们需要能够在异步过程完成的时候标记任务已经完成。我们将添加两个方法,一个负责标记任务成功完成,一个负责标记任务报错退出。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public void SetResult() => Complete(null);
|
||||||
|
|
||||||
|
public void SetException(Exception error) => Complete(error);
|
||||||
|
|
||||||
|
private void Complete(Exception? error)
|
||||||
|
{
|
||||||
|
lock (this)
|
||||||
|
{
|
||||||
|
if (_completed)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Already completed");
|
||||||
|
}
|
||||||
|
|
||||||
|
_error = error;
|
||||||
|
_completed = true;
|
||||||
|
|
||||||
|
if (_continuation is not null)
|
||||||
|
{
|
||||||
|
ThreadPool.QueueUserWorkItem(_ =>
|
||||||
|
{
|
||||||
|
if (_ec is not null)
|
||||||
|
{
|
||||||
|
ExecutionContext.Run(_ec, _ => _continuation(this), null);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_continuation(this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
我们会存储任何的错误、标记任务已经完成,如果已经注册的任何的后续过程,我们也会引发其进行执行。
|
||||||
|
|
||||||
|
最后我们还需要一个方法将在工作中发生的任何传递出来,(如果是泛型类型,还需要将执行结果返回),为了方便某些特定的场景,我们将允许这个方法阻塞直到异步操作完成(通过调用`ContinueWith`注册一个`ManualResetEventSlim`实现)。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public void Wait()
|
||||||
|
{
|
||||||
|
ManualResetEventSlim? mres = null;
|
||||||
|
lock (this)
|
||||||
|
{
|
||||||
|
if (!_completed)
|
||||||
|
{
|
||||||
|
mres = new ManualResetEventSlim();
|
||||||
|
ContinueWith(_ => mres.Set());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mres?.Wait();
|
||||||
|
if (_error is not null)
|
||||||
|
{
|
||||||
|
ExceptionDispatchInfo.Throw(_error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这就是一个基础的`Task`实现。当然需要指出的是实际的`Task`会复杂很多:
|
||||||
|
|
||||||
|
- 支持设置任意数量的后续工作;
|
||||||
|
- 支持配置其的工作行为(例如配置后续工作是应该进入工作队列等待执行还是作为任务完成的一部分同步被调用);
|
||||||
|
- 支持存储多个错误;
|
||||||
|
- 支持取消异步操作;
|
||||||
|
- 一系列的帮助函数(例如`Task.Run`创建一个代表在线程池上运行委托的`Task`)。
|
||||||
|
|
||||||
|
但是这些内容中没有什么奥秘,核心工作原理和我们自行实现的是一样的。
|
||||||
|
|
||||||
|
你可以会注意到我们自行实现的`MyTask`直接公开了`SetResult/SetException`方法,而`Task`没有;这是因为`Task`是以`internal`声明了上述两个方法,同时`System.Threading.Tasks.TaskCompletionSource`类型负责作为一个独立的`Task`生产者和管理任务的完成。这样做的目的并不是出于技术目的,只是将负责控制完成的方法从消费`Task`的方法中分离出来。这样你就可以通过保留`TaskCompletionSource`对象来控制`Task`的完成,不必担心你创建的`Task`在你不知道的地方被完成。(`CancellationToken`和`CanellationTokenSource`也是处于同样的设计考虑,`CancellationToken`是一个包装`CancellationTokenSource`的结构,只暴露了和接受消费信号相关的结构而缺少产生一个取消信号的能力,这样就限制只有`CancellationToeknSource`可以产生取消信号。)
|
||||||
|
|
||||||
|
当前我们也可以像`Task`一样为我们自己的`MyTask`添加各种工具函数。例如我们添加一个`MyTask.WhenAll`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static MyTask WhenAll(MyTask t1, MyTask t2)
|
||||||
|
{
|
||||||
|
var t = new MyTask();
|
||||||
|
|
||||||
|
int remaining = 2;
|
||||||
|
Exception? e = null;
|
||||||
|
|
||||||
|
Action<MyTask> continuation = completed =>
|
||||||
|
{
|
||||||
|
e ??= completed._error; // just store a single exception for simplicity
|
||||||
|
if (Interlocked.Decrement(ref remaining) == 0)
|
||||||
|
{
|
||||||
|
if (e is not null) t.SetException(e);
|
||||||
|
else t.SetResult();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
t1.ContinueWith(continuation);
|
||||||
|
t2.ContinueWith(continuation);
|
||||||
|
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
然后是一个`MyTask.Run`的示例:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static MyTask Run(Action action)
|
||||||
|
{
|
||||||
|
var t = new MyTask();
|
||||||
|
|
||||||
|
ThreadPool.QueueUserWorkItem(_ =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
t.SetResult();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
t.SetException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
还有一个简单的`MyTask.Delay`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static MyTask Delay(TimeSpan delay)
|
||||||
|
{
|
||||||
|
var t = new MyTask();
|
||||||
|
|
||||||
|
var timer = new Timer(_ => t.SetResult());
|
||||||
|
timer.Change(delay, Timeout.InfiniteTimeSpan);
|
||||||
|
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
在`Task`横空出世之后,之前的所有异步编程范式都成为了过去式。任何使用过去的编程范式暴露的异步`API`,现在都提供了返回`Task`的方法。
|
||||||
|
|
||||||
|
### 添加Value Task
|
||||||
|
|
||||||
|
直到现在,`Task`都是.NET异步编程中的主力军,在每次新版本发布或者社区发布的新`API`都会返回`Task`或者`Task<TResult>`。但是,`Task`是一个类,而每次创建一个类是都需要分配一次内存。在大多数情况下,为一个会长期存在的异步操作进行一次内存分配时无关紧要的,并不会操作明显的性能影响。但是正如之前所说的,同步完成的异步操作十分创建。例如,`Stream.ReadAsync`会返回一个`Task<int>`,但是如果是在一个类似与`BufferedStream`的实现上调用该方法,那么你的调用由很大概率就会是同步完成的,因为大多数读取只需要从内存中的缓冲区中读取数据而不需要通过系统调用访问`I/O`。在这种情况下还需要分配一个额外的对象显然是不划算的(而且在`APM`范式中也存在这个问题)。对于返回非泛型类型的方法来说,还可以通过返回一个预先分配的已完成单例来缓解这个问题,而且`Task`也提供了一个`Task.CompletedTask`。但是对于泛型的`Task<TResult>`则不行,因为不可能针对每个不同的`TResult`都创建一个对应的单例。那么我们可以如何让这个同步操作更快呢?
|
||||||
|
|
||||||
|
我们可以试图缓存一个常见的`Task<TResult>`。例如`Task<bool>`就非常的常见,而且也只存在两种需要缓存的情况:当结果为真时的一个对象和结果为假时的一个对象。同样的,尽管我们可能不想尝试(也不太可能)去缓存数亿个`Task<int>`对象以覆盖所有可能出现的值,但是鉴于很小的`Int32`值时非常常见的,我们可以尝试去缓存给一些较小的结果,例如从-1到8的结果。 而且对于其他任意的类型来说,`default`就是一个常常出现的值,因此缓存一个结果是`default(TResult)`的`Task`。而且 在最近的.NET版本中添加了一个称作`Task.FromResult`辅助函数,该函数就会完成与上述类似的工作,如果存在可以重复使用的`Task<Result>`单例就返回该单例,反之再创建一个新的`Task`对象。对于其他常常出现的值也也可以设计方法进行缓存。还是以`Stream.ReadAsync`为例子,这个方法常常会在同一个流上调用多次,而且每次读取的值都是允许读取的字节数量`count`。再考虑到使用者往往只需要读取到这个`count`值,因此`Stream.ReadAsync`操作常常会重复返回有着相同`int`值的`Task`对象。为了避免在这种情况下重复的内存分配,许多`Stream`的实现(例如`MemoryStream`)会缓存上一次成功缓存的`Task<int>`对象,如果下一次读取仍然是同步返回的且返回了相同的数值,该方法就会返回上一次读取创建的`Task<int>`对象。但是仍然会存在许多无法覆盖的其他情况,能不能找到一种更加优雅的解决方案来来避免在异步操作同步完成的时候避免创建新的对象,尤其是在性能非常重要的场景下。
|
||||||
|
|
||||||
|
这就是`ValueTask<TResult>`诞生的背景([这篇博客](https://devblogs.microsoft.com/dotnet/understanding-the-whys-whats-and-whens-of-valuetask/)详细测试了`ValueTask<TResult>`的性能)。`ValueTask<TResult>`在诞生之初是`TResult`和`Task<TResult>`的歧视性联合。在这些争论尘埃落定之后,`ValueTask<TResult>`便不是一个立刻可以返回的结果就是一个对未来结果的承诺:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public readonly struct ValueTask<TResult>
|
||||||
|
{
|
||||||
|
private readonly Task<TResult>? _task;
|
||||||
|
private readonly TResult _result;
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
一个方法可以通过返回`ValueTask<TResult>`来避免在`TResult`已知的情况下创建新的`Task<Result>`对象,当然返回的类型会更大、返回的结果更加不直接。
|
||||||
|
|
||||||
|
当然,实际应用中也存在对性能需求相当高的场合,甚至你会想在操作异步完成的时候也避免`Task<TResult>`对象的分配。例如`Socket`作为整个网络栈的最底层,对于网络中的大多数服务来说`SendAsync`和`ReceiveAsync`都是绝对的热点代码路径,不论是同步操作还是异步操作都是非常常见的(鉴于内核中的缓存,大多数发送请求都会同步完成,部分接受请求会同步完成)。因此对于像`Socket`这类的工具,如果我们可以在异步我弄成和同步完成的情况下都实现无内存分配的调用是十分有意义的。
|
||||||
|
|
||||||
|
这就是`System.Threading.Tasks.Sources.IValueTaskSource<TResult>`产生的背景:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IValueTaskSource<out TResult>
|
||||||
|
{
|
||||||
|
ValueTaskSourceStatus GetStatus(short token);
|
||||||
|
void OnCompleted(Action<object?> continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags);
|
||||||
|
TResult GetResult(short token);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
该接口允许自行为`ValueTask<TResult>`实现一个“背后“的对象,并且让这个对象提供了获得操作结构的`GetResult`方法和设置操作后续工作的`OnCompleted`。在这个接口出现之后,`ValueTask<TResult>`也小小修改了定义,`Task<TResult>? _task`字段被一个`object? _obj`字段替换了:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public readonly struct ValueTask<TResult>
|
||||||
|
{
|
||||||
|
private readonly object? _obj;
|
||||||
|
private readonly TResult _result;
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
现在`_obj`字段就可以存储一个`IValueTaskSource<TReuslt>`对象了。而且相较于`Task<TResult>`在完成之后就只能保持完成的状态,不能变回未完成的状态,`IValueTaskSource<TResult>`的实现有着完全的控制权,可以在已完成和未完成的状态之间双向变化。但是`ValueTask<TResult>`要求一个特定的实例只能被使用一次,不能观察到这个实例在使用之后的任何变化,这也是分析规则[CA2012](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2012)存在的意义。这就让让类似于`Socket`的工具为重复的调用建立一个`IValueTaskSource<TResult>`对象池。从实现上来说,`Socket`会至多缓存两个类似的实例,一个用于读取操作一个用于写入操作,因为在99.999%的情况下同时只会有一个发送请求和一个接受请求。
|
||||||
|
|
||||||
|
值得说明的是我只提到了`ValueTask<TResult>`却没有提到`ValueTask`。因为如果只是为了在操作同步完成的时候避免内存分配,非泛型类型的`ValueTask`指挥提供很少的性能提升,因为在同样的条件下可以使用`Task.CompletedTask`。但是如果要考虑在异步完成的时候通过缓存对象避免内存分配,非泛型类型也有作用。因而,在引入`IValueTaskSource<TResult>`的同时,`IValueTaskSource`和`ValueTask`也被引入了。
|
||||||
|
|
||||||
|
到目前我们,我们已经可以利用`Task`,`Task<TResult>`,`ValueTask`,`ValueTask<TResult>`表示各种各样的异步操作,并注册在操作完成之前和之后注册后续的操作。
|
||||||
|
|
||||||
|
但是这些后续操作仍然是回调方法,我们仍然陷入了基于回调的异步控制流程。该怎么办?
|
||||||
|
|
||||||
|
### 迭代器成为大救星
|
||||||
|
|
||||||
|
解决方案的先声实际上在`Task`诞生之前就出现了,在C# 2.0引入迭代器语法的时候。
|
||||||
|
|
||||||
|
你可能会问,迭代器就是`IEnumerable<T>`吗?这是其中的一个。迭代器是一个让编译器将你编写的方法自动实现`IEnumerable<T>`或者`IEnumertor<T>`的语法。例如我可以用迭代器语法编写一个产生斐波那契数列的可遍历对象:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static IEnumerable<int> Fib()
|
||||||
|
{
|
||||||
|
int prev = 0, next = 1;
|
||||||
|
yield return prev;
|
||||||
|
yield return next;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
int sum = prev + next;
|
||||||
|
yield return sum;
|
||||||
|
prev = next;
|
||||||
|
next = sum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这个方法可以直接用`foreach`遍历,也可以和`System.Linq.Enumerable`中提供的各种方法组合,也可以直接用一个`IEnumerator<T>`对象遍历。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
foreach (int i in Fib())
|
||||||
|
{
|
||||||
|
if (i > 100) break;
|
||||||
|
Console.Write($"{i} ");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
foreach (int i in Fib().Take(12))
|
||||||
|
{
|
||||||
|
Console.Write($"{i} ");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using IEnumerator<int> e = Fib().GetEnumerator();
|
||||||
|
while (e.MoveNext())
|
||||||
|
{
|
||||||
|
int i = e.Current;
|
||||||
|
if (i > 100) break;
|
||||||
|
Console.Write($"{i} ");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
@@ -146,6 +146,10 @@ process {
|
|||||||
dotnet run -- serve
|
dotnet run -- serve
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
"list" {
|
||||||
|
dotnet run -- list
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,174 +0,0 @@
|
|||||||
---
|
|
||||||
title: 2025年终总结
|
|
||||||
date: 2026-05-08T01:22:18.6904350+08:00
|
|
||||||
updateTime: 2026-05-08T01:22:18.8859180+08:00
|
|
||||||
tags:
|
|
||||||
- 杂谈
|
|
||||||
- 年终总结
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
越来越晚的年终总结是本站不得不品的一大特色,在这样下去2026年的年终总结要到2028年才能面世了(绝望)。不过2025年确实是非常丰富多彩的一年,这一年的经历是如此的多样,以至于真正需要提笔写下来的时候反而不知道可以写什么。
|
|
||||||
|
|
||||||
<!--more-->
|
|
||||||
|
|
||||||
### 毕业
|
|
||||||
|
|
||||||
在2025年的6月,我从明光村幼儿园附属大学的括号学院毕业了。
|
|
||||||
|
|
||||||
毕业本身似乎并不是什么特别需要记录的事情,不过是一篇论文、两张证书和几次合照,现在回想起来只有一种如堕梦中的感觉。
|
|
||||||
|
|
||||||
虽然对于其他的事情都已经几乎淡忘,但是因为我提前自愿(?)选修了研究生课程《高性能计算》,而这门课的期末考试日期甚至在毕业典礼之后。这不得不使得我在毕业季各种事务缠身的情况下还得抽出时间准备考试,而这个《高性能计算》课程的内容又多又杂,实在是又难学又难背。这使得我对于这个毕业季影响最深刻的事情不是什么毕业合照,而是SMP和CUDA!
|
|
||||||
|
|
||||||
不过说都说到这里了,顺便回忆一下我的四年大学生活吧。
|
|
||||||
|
|
||||||
### 我的大学
|
|
||||||
|
|
||||||
还记得2021年的暑假,我还在和一个名叫Microsoft Visual Studio的神秘软件搏斗,并不知道这个软件中某个叫作.NET SDK的东西会成为这四年中的一个重要组成部分,当时还创作了本博客的[第一篇文章](https://rrricardo.top/blog/essays/question-in-install-vs-2019)。
|
|
||||||
|
|
||||||
2021年的9月第一次来到校园,不得不说该附属大学的偏远校区还是很符合我对于大学校园的刻板印象的。标准的四人间、高大的图书馆,迷宫一样的教学楼设计充分满足了我对于大学生活的一切想象,甚至到主校区需要坐一个半小时地铁也是刻板印象的一部分。
|
|
||||||
|
|
||||||
不过这样的“幸福大学生活”只持续了四个月。在2022年9月份就回到了宇宙中心的南边,中国最宽公路的东边,我们伟大的明光村。很高情商地说,在这个校区中学习和生活,可以**随时随地**地品味到这个学校深厚的历史底蕴。例如我现在居住的宿舍,在20年前也居住过我的导师。课间在窗边踱步,你可以欣赏到墙上的照片里,脚下的教学楼在1956年落成时的雄姿。
|
|
||||||
|
|
||||||
还是谈谈在这四年中我所修过的那些课程吧。本来在2025年6月毕业之后,我打算写一篇文章,结合我四年中的经历,详细分析一下我所经历的计算机科学本科教学。但是迟迟没有时间落笔,就在这里简单评述一下。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
首先是培养方案中归类为“数学与自然科学”的课程:《高等数学A》和《线性代数》是必修课,其中《高等数学A》因为代课老师江彦是一个**认真负责**的好老师,这里给到一个夯。《线性代数》则是因为课程设计和老师只能给到一个NPC,说实话本来想给到一个拉完了,但是考虑到这门课算是计算机科学比较重要的数学基础,还是给到NPC。
|
|
||||||
|
|
||||||
两门概率论课程我选的是《概率论与随机过程》,同样因为代课老师鞠红杰给到一个顶级,而且随机过程本来就是一个非常有趣的研究课题,这门课的课程论文就是博客中的[原神抽卡研究](https://rrricardo.top/blog/essays/genshin-gacha-1)。至于《组合数学》、《运筹学》、《数学建模与模拟》和《矩阵理论与方法》四选一,我选择的是《数学建模与模拟》,这个课程只能给到NPC,听了和没听一样,老师脾气还不太好。《大学物理C》同样只能给到一个NPC,虽然我对于物理还是挺感兴趣的~~还参加了大学生物理竞赛~~,但是这实在和计算机学科关系不太大,安排这个不如多安排一些通信和电子的内容。
|
|
||||||
|
|
||||||
然后是培养方案中“学科基础”大类课程,这个课程组中的课程都是必修课程。《计算导论与程序设计》是大一上学期的唯一一门专业课,也是计算机科学的入门课,我会说它很好的完成了这个任务,虽然还有不少的优化空间,这里给到一个顶级。
|
|
||||||
|
|
||||||
《电路与电子学基础》我直接给到一个顶级,老师非常风趣幽默,教学内容不能说是和计算机科学直接相关,但也算是计算机的基石了。《离散数学》这里是一个拉完了,从理论上来说离散数学的教学内容可以算是实际上的计算机科学核心,但是正是这种极端重要性更加凸显出了这个课程拉完了的程度,而且还是中英文混合教学,我的评价是没有金刚钻就别揽瓷器活。
|
|
||||||
|
|
||||||
《数字逻辑与数字系统》,还可以,归类到人上人,课程内容本身比较重要,算是之后的计算机组成原理的先修课程。《形式语言与自动机》,这门课的代课老师石川老师算是北邮的风云人物,其他的东西不评价,但是他在这门课程上的表现还是值得肯定的,这里归到人上人。
|
|
||||||
|
|
||||||
下面正式进入计算机科学中的真·核心课程,即培养方案中的“专业基础”课程组。首先是《数据结构》,因为考试要求手写代码直接给到一个拉完了,我得承认这个有我自己的一些偏见。《算法设计与分析》本来想以同样的理由给到一个拉完了,~~但是考虑到我的成绩比《数据结构》高~~,这里给到一个NPC。《计算机系统基础》课程,使用的教材是大名鼎鼎的*Computer System: A Programmer Perspective* ,虽然课程并没有完全覆盖CSAPP中所有内容,但是还是给到一个夯!
|
|
||||||
|
|
||||||
《操作系统》,按道理讲也是计算机科学中的核心课程,但是课程内容我现在就记得一个信号量控制抢水果和挑水,老师也非常莫名其妙,直接给到拉完了,正所谓学完这门课你也不懂操作系统。然后是《编译原理与技术》,可以算是计算机科学中最难的课程之一,但是因为课程核心都放在了编译器的前端部分,教学语言还是老掉牙的Pascal,只能给到一个人上人。
|
|
||||||
|
|
||||||
《计算机组成原理》,考虑老师直接在课程推销王道考研,直接给到拉完了。《计算机系统结构》,算是《计算机组成原理》的进阶课,老师讲得也非常不错,和之前的《计算机组成原理》相比简直是天差地别,直接给到一个顶级,如果实验部分改进一下,和《计算机组成原理课程设计》结合一下,就可以直接给到夯了。《计算机网络》直接给到夯,算是课程与现实结合最紧密的课程,和《离散数学》同样都是中英文混合教学,只能说人与人之间亦有差距。《数据库系统原理》获得夯的理由和《计算机网络》比较一致,都是同现实紧密关联的课程,任课老师教学非常不错,也是中英文混合教学。
|
|
||||||
|
|
||||||
《软件工程》和《现代交换原理》则是难兄难弟,两门都是拉完了:《软件工程》被称为计算机科学中的政治,我认为与其死记硬背一些非常抽象的概念、参加两次考试不如多写几行代码来得体会深刻。而且这门课的实践部分也非常抽象,要求几个小组联合验收:而且这些小组的前后端之间需要可以随机组合。虽然这个要求可以说是非常的软件工程,但是和你合作的哥们可能并不是特别拟人,只能唉唉唉。至于《现代交换原理》,第一次看到这门课的时候我很怀疑这是否是打印错误,虽然代课老师挺不错的,但是它唯一的问题就是不该出现在计算机科学的培养方案上。退一步说就算学校设计培养方案的时候希望计算机科学的同学也懂一点通信,也是应该设计简单一点的通信原理而不是设计一门不知所云的“古代交换原理”课程(毕竟课程的主体部分是电话交换机)。
|
|
||||||
|
|
||||||
然后是专业课选修课组部分。在“网络&开发技术模块”,我选修的课程是《下一代Internet技术与协议》、《移动互联网技术及应用》和《Python程序设计》三门课。其中《下一代Internet技术与协议》是中规中矩的选修课程,只能归到NPC一档,课程的主要内容就是讲IPv6协议栈,考虑到现在IPv6协议栈已经在大规模的普及推进,建议把课程名称中的“下一代”去掉。《移动互联网技术及应用》就是教写Android应用,考虑到这门课的老师是北邮少数敢于在课程上打开IDE写代码的老师,出于对这位老师的敬佩,这里给到人上人。《Python程序设计》也是一门平平无奇的选修课,同时是出于对踢球骨折了还拄着拐杖来上课的老师之敬佩,这里给到人上人。
|
|
||||||
|
|
||||||
在“大数据技术模块”,我选修的课程是《大数据技术基础》和《信息与知识获取》。其中的《大数据技术基础》的实验部分非常抽象,要求使用容器技术模拟出多个节点来搭建大数据系统,比如Hadoop和Spark,还需要在华为的ARM云服务器上进行实验,与其叫作是《大数据技术基础》不如改名为《Linux系统运维基础》。不过考虑到这是我本科期间唯一一门愿意给100分的课程,这门还是给到人上人。《信息与知识获取》则是“大海呀,你全部都是水~”,这里给到NPC。
|
|
||||||
|
|
||||||
在“技术拓展模块”,我选修的课程是《人工智能原理》和《程序设计实践》。这两门都给到NPC,《人工智能原理》是因为它讲授的原理有点太古老了,连深度神经网络都没有涉及,还需要考试,只能给到NPC。《程序设计实践》则是普通的水课,项目开源在[github](https://github.com/jackfiled/Katheryne)上,看上去这个题目也是祖传题目了。
|
|
||||||
|
|
||||||
然后我们可以来谈论一下最激动人心的实践课课组。首先是《物理实验A》,由于是线上教学做实验,直接给到一个拉完了。《计算导论与程序设计课程设计》(不太可能会有“普通人”在大一的下学期就选择《程序设计竞赛基础》吧)是大一下学期开设,课程设计的题目是公交车的调度,对大一新人来说还是挺合适的,这里给到一个人上人。面向对象的程序设计实践我选择的是Java,这个只能说拉完了,Java感觉没学到什么有用的,最后交作业的时候还要求提交一大堆软件工程的UML图。
|
|
||||||
|
|
||||||
《计算机组成原理课程设计》和《数字逻辑与数字系统课程设计》二选一我选择是《计算机组成原理课程设计》,题目是设计一个单周期的CPU,非常好课程直接给到夯!唯一的问题似乎因为小学期时间安排的问题,导致最后只有三四天的时间来完成整个作业,这种课程就应该设计为一个必修实验,而且最好需要设计一个完整的五段流水线CPU。
|
|
||||||
|
|
||||||
《操作系统课程设计》和《编译原理课程设计》二选一我选择了《编译原理课程设计》,这更多是一个历史和“人民”的选择,当时我们课程班100多个人,最后只有一个小组大概8个人选择了《操作系统课程设计》。不过《编译原理课程设计》确实可以给到夯!首先是老师非常不错,然后课程设计的内容也确实可以说是循序渐进。唯一的问题是设计的编译器是从Pascal翻译到C,和编译原理课程本身的衔接也不是特别紧密,尤其是考虑到编译原理课程本身自带的两个实验就是词法分析和语法分析,课程设计还把重点放在前端上就有点不太好了。最好是把题目修改到C到某门汇编语言,例如RISC-V,然后和《编译原理》课程本身的实验衔接设计,课程设计的重点就可以放在代码生成和代码优化上面了。
|
|
||||||
|
|
||||||
至于最后的三选二选修课,在《数据结构课程设计》、《计算机网络课程设计》和《数据库系统原理课程设计》三门中我选择的前两门。《数据结构课程设计》有一个非常奇葩的要求,不能使用自带的数据结构实现(比如说标准库中的列表、哈希表等),而是要求自行实现,但是在最后验收时并没有突出这一点,这里只能给到一个人上人,算是实践课程中比较低的评价了。《计算机网络课程设计》要求设计一个DNS Relay服务器,比较典型的计算机网络课程设计的要求,唯一的问题是要求必须要用C语言完成,只能给到一个顶级。这里我感觉老师做出这个限制的主要理由是希望大家多钻研一下和网络相关的高并发设计,但是直接限制到C语言级别有点强人所难了。
|
|
||||||
|
|
||||||
最后锐评一下我选过的公选课和体育课,在我的培养方案中,我需要选修两门以上的公选课(人文艺术类型)和四门体育课(其中一门必修的《体育基础》)。我为了满足培养方案选修的公选课是《人工智能与社会发展》和《显示技术发展与游戏应用》两门,普通的水课没什么好说的,给到NPC。不过在培养方案之外我还选修了一门《基于Arduino的开源手机设计》,非常好的选修课,课程内容是基于ESP8266设计一个支持2G和Wi-Fi的按键手机。所有上课的同学都可以免费获得一个板子,值得选修,这里给到一个夯。
|
|
||||||
|
|
||||||
至于体育课,因为我本人体育苦手,评价略有失真之处,仅供参考。首先是在**线上**进行的《体育基础》课,直接给到NPC,至于《健美》和《乒乓球》,考虑到我学得不好应该是我的问题,给到一个顶级。至于最后的《太极拳》因为体育苦手的缘故,简直就是我等的福音,给到夯!
|
|
||||||
|
|
||||||
### 实习与工作
|
|
||||||
|
|
||||||
在毕业之后不能躲过的话题自然就是工作,虽然正式的找工作离我还有~~三年~~两年半的时间。尤其是考虑到我们伟大的学校并不打算为保研的同学提供宿舍,在6月被赶出校门之后,一个比较正确的选择就是找一份实习。
|
|
||||||
|
|
||||||
大概从2025年的5月份我就开始在BOSS直聘上找实习,首选的工作意向就是和我现在研究方向相关联的编译器、高性能计算和AI Infra方向,考虑到这些方向的工作岗位数量如同食堂番茄蛋花汤中的蛋花,我也填上了后端方向作为备选。不过找工作真的好痛苦啊:从事后统计来看,在那半个月的时间里我大概投出去了60多份简历,其中只有个位数的HR回复了消息,只约上了一场面试。这便是今天的主角——理想汽车。
|
|
||||||
|
|
||||||
说起来也奇怪,招聘的这个岗位,理想汽车的图编译器开发,并不是我自己找上门去的,而是对方的HR主动要走了我的简历。下面就简单记录一下理想汽车的图编译器开发实习生的面经吧,不过说实话这已经是接近一年之前的事情了,如果有错漏之处还请谅解。
|
|
||||||
|
|
||||||
上来还是自我介绍起手,并介绍自己简历上的一些项目。因为我当时没有任何的实习经历,简历上主要的项目其实是本科的毕业设计。不过因为这个毕业设计题目取得非常高大上,但是实现上非常一坨,所以拷打的过程个人感觉漏洞百出。
|
|
||||||
|
|
||||||
然后是一些基础的编译器知识,比如说你是否知道什么是静态单赋值形式,MLIR中的IR是否是SSA等等,还结合MLIR问了很多MLIR中的细节开发问题,如果你是否定义过Dialect,是否写过operation的parser和printer等等。还有MLIR中的pass分为什么,是否了解这些pass是如何运行的,有一些非常细节的问题我当时就直接回答不知道了(捂脸)。
|
|
||||||
|
|
||||||
中间还拷打了一些`C++`开发的知识,比如说CRTP这种在MLIR中非常常用的模板范式,不过我当时因为对`C++`开发还不是特别熟悉,将中间的`static_cast`说成了`dynamic_cast`,非常的尴尬。面试官还问了问我研究生和本科的主修课程。
|
|
||||||
|
|
||||||
编程的题目是手撕堆排序,但是面试官找了半天都没有找到测试的题目,只让我口述了算法的过程,不过我当时过度紧张(毕竟是人生的第一场面试),什么归并快排桶排堆排的都丢到九霄云外去了,只能阿巴阿巴地说一些建堆、排序之内的车轱辘话,幸好面试官也没有特别纠结这一块。反问的环节我是询问了一下我们工作的对象,理想汽车自研汽车芯片的情况,也就是现在即将发布的马赫100芯片。
|
|
||||||
|
|
||||||
幸好最终的结果还是比较好的,顺利拿到了offer并入职理想。不过当时暑假期间的工资是按照本科生算的,只有230一天,考虑到当时我在北京还需要租房居住,差点付费上班(幸好还有第二份兼职)。而且当时部门正处于芯片即将上车的集中开发阶段,8月份还把我们一起打包送到杭州去上班,血亏一个月房租,~~不过住一个月的酒店还是蛮爽的,谢谢想哥~~。
|
|
||||||
|
|
||||||
不过说来我还是非常感谢在理想的这段实习经历的。在进入理想实习之前,虽然我已经在导师的手下干了一年,但是感觉对于编译器,尤其是这种面向NPU的AI编译器,总感觉还是有一种雾里看花的感觉。但是在实习过程中,实际接触了编译器的开发和优化流程,瞬间感觉过程论文和课本中的编译器活了过来。同样也是在这段经历中,我对于MLIR的了解和认识也是飞速增长。
|
|
||||||
|
|
||||||
在理想的实习经历之外,~~为了赚房租~~为了进一步提高自己的能力,同时在为我在开源世界中开辟一块立锥之地,我报名并参加了2025年的开源之夏(OSPP)项目,中选的题目是[RustSBI原型系统引导生态完善](https://summer-ospp.ac.cn/org/prodetail/256590172?lang=zh&list=pro)。不过这次OSPP的经历我打算单出一篇文章来分享(挖坑x1),这里就不多赘述了。
|
|
||||||
|
|
||||||
### 研究生
|
|
||||||
|
|
||||||
讲道理,在9月份开学之后我就回到明光村幼儿园附属大学继续攻读我的硕士学位了,这段研究生时光也占据了2025年度三分之一的长度。但是我在撰写本年终总结的时候却对于这段时光一点回忆也没有,真是令人感到好奇!
|
|
||||||
|
|
||||||
### 第一台NAS
|
|
||||||
|
|
||||||
上面拖拖拉拉地讲了一堆令人悲伤的话题,还是一转聊一聊一些开心的话题吧。
|
|
||||||
|
|
||||||
经过若干个月的精心筹备,在2025年的一季度,我终于组建了我的个人NAS,虽然这台NAS里面目前只有1块4T 3.5英寸硬盘和几块从笔记本上拆下来的2.5英寸硬盘,显得我精心挑选的8 STAT接口主板和8个3.5英寸硬盘位的机箱显得很呆。不过还是很开心,谁叫组装好硬件就遇上了AI导致的硬件大涨价呢。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
这里也是简单列一下硬件配置:
|
|
||||||
|
|
||||||
1. CPU:Intel Xeon E3-1245v5
|
|
||||||
2. 内存:镁光DDR4 ECC 16G 2400 x2
|
|
||||||
3. 主板:超微X11SSH-F
|
|
||||||
4. 电源:全汉 蓝暴经典PLUS 450W
|
|
||||||
5. 机箱:联宇 见方L
|
|
||||||
6. CPU散热器:利民AXP90-X36
|
|
||||||
|
|
||||||
这里我也打算后面单独出一篇文章分享设计和配置这个NAS的点点滴滴(挖坑x2),这里就简单吐槽一件令我非常绷不住的事。这里选择E3-1245v5这颗带有核显的CPU简直就是我最大的错误,我设计的时候考虑的是这颗核显可以极大地加快NAS上各种媒体的解码速度。但是E3-1245v5这颗CPU带的核显简直就是垃圾中的垃圾:它不支持H265/HEVC 10 bit的解码,简直就是屁用没用!
|
|
||||||
|
|
||||||
而且,这颗CPU在秋季开学之后就挂掉了,还贡献了我的第一张Linux蓝屏~~扫码即可查看当时的Kernel日志~~:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
我不得不购入了第二块CPU,也就是现在机器上使用的Intel Xeon E3-1240v5。谢天谢地,这颗CPU到目前还工作正常,而它比它带核显的兄弟足足便宜了三分之二。
|
|
||||||
|
|
||||||
### 第一台台式机
|
|
||||||
|
|
||||||
除了这台,我还在暑假的时候组装了我人生的第一台台式机:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
这里也简单列举一下这台主机的配置:
|
|
||||||
|
|
||||||
- CPU: AMD 锐龙 5 9600X
|
|
||||||
- 内存:光威龙武弈 DDR5 长鑫颗粒 6000 CL36 16G x2
|
|
||||||
- 主板:华硕TX B650EM WIFI W
|
|
||||||
- GPU:ONIX Intel ARC B580 LUMI 12G
|
|
||||||
- 电源:鑫谷 GM650W 金牌全模组
|
|
||||||
- CPU散热器:九州风神 玄冰400V5 晴雪白
|
|
||||||
- 机箱散热器:酷冷至尊莫比乌斯120白色 x2
|
|
||||||
- 立牌:psplive 2025校园主题亚克力立牌-[李豆沙_Channel](https://space.bilibili.com/1703797642)
|
|
||||||
|
|
||||||
组装这台主机的时间非常巧妙,我是在7月初的时候下定决心组装这台台式机,这几乎是在电子产品大涨价之前最后一个上车的时间窗口了。
|
|
||||||
|
|
||||||
关于我选择的这些配置我也想简单讨论一下。主要的争议点可能是我选择的显卡,当时我的决赛圈里面几乎就是了两张显卡:NVIDIA的RTX 5060和Intel的Arc B580。最终驱动我做出决定的因素主要是预算,RTX 5060的价格基本上都在2500元以上,而我拿下的这块Intel Arc B580只花了我1800元。虽然当时网上对于Intel显卡驱动的批评甚嚣尘上,但是我决定对Intel这家老牌的半导体厂商保持基本的信任(至少到目前为止,Intel还没有辜负我的信任)。而且考虑到我实际上也是一个Linux用户,NVIDIA的Linux驱动是个什么样子是不言自明的,至少Intel的Linux支持情况要好得多。
|
|
||||||
|
|
||||||
而且在我目前的日常游玩的所有游戏中这块显卡都表现正常,例如《三角洲行动》在我目前的2K分辨率下可以取得90~110FPS的水平。《原神》和《戴森球计划》这种对于显卡需求比较低的游戏更是轻松拿下。
|
|
||||||
|
|
||||||
### 玫瑰色生活
|
|
||||||
|
|
||||||
2025年也是一个旅行颇多的年份。
|
|
||||||
|
|
||||||
首先是趁着毕业的时候,去北京一个著名的小众冷门(这是否矛盾?)景点——中央礼品文物管理中心参观,算是一个非常有首都特色的景点了。然后是忙碌的8月份,在北京和杭州之间飞来飞去,很幸运地是在其中的一次坐到了国产大飞机C919前往杭州:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
还前往了内蒙古的鄂尔多斯参加全国高性能计算学术年会CCF HPC China 2025。学术上的东西在下人微言轻不做评价,不过至少HPC China在茶歇上比之前去参加了CNCC 2024要慷慨的多。其他的内容可以参见同行者的[文章](https://www.chenxutalk.top/posts/life/travel/neimenggu/)。
|
|
||||||
|
|
||||||
正所谓“读万卷书,行万里路”。在2025年还读了下面这几本书:
|
|
||||||
|
|
||||||
- [*GOSICK*](https://mzh.moegirl.org.cn/GOSICK),樱庭一树著,不算是一般意义上的轻小说,而是比较严肃的推理小说,好看!
|
|
||||||
- 《浪潮之巅》,吴军创作的科技产业发展史的书籍,对于计算机相关领域从业者来说可以说是必读的经典书籍了。
|
|
||||||
|
|
||||||
还看了不少的电影:一部是看上去比较冷门的电影《商海通牒》,原名*Margin Call*,记录的是2008年到2009年发生金融危机时一家华尔街投行中发生的故事。另一部则是学校组织放映的《窗外是蓝星》,记录的是神舟十三号乘组首次在中国空间站执行在轨驻留六个月任务的故事。岁月如梭啊,现在已经是神舟二十一号乘组了。
|
|
||||||
|
|
||||||
本来在年终总结的末尾,按照传统还是应该放一下B站的观看时长,但是在撰写本文时B站似乎已经下架了2025年度报告的查看页面了(悲)。
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: 使用System.Text.Json序列化和反序列化JSON
|
title: 使用System.Text.Json序列化和反序列化JSON
|
||||||
date: 2026-01-21T22:07:38.4297603+08:00
|
date: 2026-01-21T22:07:38.4297603+08:00
|
||||||
updateTime: 2026-04-03T17:16:16.0831040+08:00
|
updateTime: 2026-01-21T22:07:38.4370636+08:00
|
||||||
tags:
|
tags:
|
||||||
- 技术笔记
|
- 技术笔记
|
||||||
- dotnet
|
- dotnet
|
||||||
@@ -9,7 +9,6 @@ tags:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
如何使用`System.Text.Json`高效地序列化和反序列化JSON?
|
如何使用`System.Text.Json`高效地序列化和反序列化JSON?
|
||||||
|
|
||||||
<!--more-->
|
<!--more-->
|
||||||
@@ -218,14 +217,6 @@ record MyPoco(
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
但是在实际的生产和生活中,并不会有人将可选的属性序列化为`null` 之后再返回,而是直接忽略这个属性。这就让“可为空”这个属性显得有点鸡肋,因此可为空的属性一般也需要提供了一个可选的构造函数参数:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
record MyPocp(string RequiredField, string? NotRequiredField = null)
|
|
||||||
```
|
|
||||||
|
|
||||||
更本质的说,这是因为Javascript中提供了两种空`undefined`和`null`,然而在C#中并没有提供`undefined`对应的语言构造,因此只能通过这种默认值为`null`的构造函数参数来模拟。
|
|
||||||
|
|
||||||
#### 反序列化为结构
|
#### 反序列化为结构
|
||||||
|
|
||||||
结构作为值类型,虽然在函数之间传递时需要被拷贝而带来了额外的性能开销,但是也因为这一点而可以被直接分配在栈上,给GC带来的压力较小。因此在部分需要极端性能优化的场景可以直接针对`struct`进行反序列化。
|
结构作为值类型,虽然在函数之间传递时需要被拷贝而带来了额外的性能开销,但是也因为这一点而可以被直接分配在栈上,给GC带来的压力较小。因此在部分需要极端性能优化的场景可以直接针对`struct`进行反序列化。
|
||||||
|
|||||||
@@ -1,215 +0,0 @@
|
|||||||
---
|
|
||||||
title: Tarjan算法与实现
|
|
||||||
date: 2026-03-28T21:53:45.1681856+08:00
|
|
||||||
updateTime: 2026-03-28T21:53:45.1733146+08:00
|
|
||||||
tags:
|
|
||||||
- 技术笔记
|
|
||||||
- 算法
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
Tarjan算法是一类用于无向图中割边和割点的算法。
|
|
||||||
|
|
||||||
<!--more-->
|
|
||||||
|
|
||||||
## Tarjan算法
|
|
||||||
|
|
||||||
Tarjan算法是图论中非常常用的一种算法,基于深度优先搜索(DFS),基础版本的Tarjan算法用于求解无向图中的割点和桥。基于此可以求解图论中的一系列问题,例如无向图的双连通分量、有向图的强连通分量等问题。
|
|
||||||
|
|
||||||
Tarjan算法由计算机科学家Robert Tarjan在1972年于论文*Depth-First Search And Linear Graph Algorithms*中提出。Robert Tarjan是一位著名的计算机科学家,解决了图论中的一系列重大问题,同时也是斐波那契堆(Fibonacci Heap)和伸展树(Splay Tree)的开发者之一。他于1986年获得了图灵奖,目前仍在普林斯顿大学担任教职。
|
|
||||||
|
|
||||||
## 无向图的割点与桥
|
|
||||||
|
|
||||||
如果一个图中所有的边都是无向边,则称之为无向图。
|
|
||||||
|
|
||||||
### 割点
|
|
||||||
|
|
||||||
如果从无向图中删除节点x和所有与节点x关联的边之后,图将会被分成两个或者两个以上不相连的子图,那么节点x就是这个图的割点。下图中标注为红色的点就是该图的割点。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 桥
|
|
||||||
|
|
||||||
如果从图中删除边e之后,图将分裂为两个不相连的子图,那么就称e是图的桥,或者割边。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
图中被标注为红色的边就是该图的桥。
|
|
||||||
|
|
||||||
## 求解图中的割点
|
|
||||||
|
|
||||||
Tarjan算法中为了求解桥和割点,首先定义了如下几个概念。
|
|
||||||
|
|
||||||
### 时间戳
|
|
||||||
|
|
||||||
时间戳用来标记图中每个节点在进行深度优先搜索的过程中被访问的时间顺序,这个概念起始也就是在遍历的时候给每个节点编号。
|
|
||||||
|
|
||||||
这个编号用`search_number[x]`来表示,其中的x是节点。
|
|
||||||
|
|
||||||
### 搜索树
|
|
||||||
|
|
||||||
在图中,如果从一个节点x出发进行深度优先的搜索,在搜索的过程中每个节点只能访问一次,所有被访问的节点可以构成一棵树,这棵树就被称为无向连通图的搜索树。
|
|
||||||
|
|
||||||
### 追溯值
|
|
||||||
|
|
||||||
追溯值的定义和计算是Tarjan算法的核心。
|
|
||||||
|
|
||||||
追溯值被定义为,从当前节点x作为搜索树的根节点出现,能够访问到的所有节点中,时间戳的最小值,被记为`low[x]`。
|
|
||||||
|
|
||||||
定义中主要的限定条件是“能够访问到的所有节点”,主要考虑的是如下两种访问方式:
|
|
||||||
|
|
||||||
- 这个节点在以x为根的搜索树上
|
|
||||||
- 通过一条不属于搜索树的边,可以到达搜索树的节点。
|
|
||||||
|
|
||||||
例如上图的例子中,考虑直接从节点1出发开始深度优先的遍历,此时使用的遍历顺序是节点1、节点2、节点3、节点4、节点5。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
当遍历到节点5时,考虑以节点5为根的搜索树(可以认为此时的搜索树中只有节点5一个节点),可以发现有两条不属于搜索树的边(2, 5)和(1, 5),使得节点1和节点2成为了上述“可以访问到的节点”,因此将节点5的追溯值更新为1。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
此时,算法按照深度优先搜索的顺序开始回溯,在回溯的过程中逐步更新当前节点的追溯值,此时就是按照上面“可以访问的所有节点”中的搜索树情形工作了。例如当回溯到节点3时,可以认为存在以节点3为根节点的搜索树{3, 4, 5},其中追溯值的最小值为1, 将节点3的追溯值更新为1。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 桥的判定法则
|
|
||||||
|
|
||||||
在无向图中,对于一条边`e = (u ,v)`,如果满足`search_number[u] < low[v]`,那么该边就是图中的一个桥。
|
|
||||||
|
|
||||||
这个条件所蕴含的意思是,节点u被访问的时间,要小于(优先于)以下所有这些节点被访问的时间:
|
|
||||||
|
|
||||||
- 以节点v为根节点的搜索树中的所有节点
|
|
||||||
- 通过一条非搜索树上的边,能否到达搜索树的所有节点。
|
|
||||||
|
|
||||||
## 实现
|
|
||||||
|
|
||||||
以下以[1192. 查找集群内的关键连接 - 力扣(LeetCode)](https://leetcode.cn/problems/critical-connections-in-a-network/description/)为例,给出Tarjan算法的实现。
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
namespace {
|
|
||||||
/// Graph structure.
|
|
||||||
/// Store the graph using linked forwarded stars.
|
|
||||||
/// The linked forwarded stars store the graph using linked list.
|
|
||||||
///
|
|
||||||
/// To accelerate the loading and storing, use array to simulate the linked
|
|
||||||
/// list.
|
|
||||||
struct Graph {
|
|
||||||
explicit Graph(const size_t nodeCount, const size_t edgeCount) {
|
|
||||||
// The edgeID starts from 2, as 0 is used as null value.
|
|
||||||
// And to find the reverse edge by i ^ 1, so 0 and 1 are both skipped.
|
|
||||||
endNodes = vector<size_t>(edgeCount + 2, 0);
|
|
||||||
nextEdges = vector<size_t>(edgeCount + 2, 0);
|
|
||||||
headEdges = vector<size_t>(nodeCount, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void addEdge(const size_t x, const size_t y) {
|
|
||||||
endNodes[edgeID] = y;
|
|
||||||
nextEdges[edgeID] = headEdges[x];
|
|
||||||
headEdges[x] = edgeID;
|
|
||||||
|
|
||||||
edgeID += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
vector<bool> calculateBridges() {
|
|
||||||
// Initialize values used by tarjan algorithm.
|
|
||||||
const auto nodeCount = headEdges.size();
|
|
||||||
bridges = vector(endNodes.size(), false);
|
|
||||||
nodeIDs = vector<size_t>(nodeCount, 0);
|
|
||||||
lowValues = vector<size_t>(nodeCount, 0);
|
|
||||||
|
|
||||||
number = 1;
|
|
||||||
|
|
||||||
for (auto i = 0; i < nodeCount; i++) {
|
|
||||||
if (nodeIDs[i] == 0) {
|
|
||||||
tarjan(i, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return bridges;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
size_t edgeID = 2;
|
|
||||||
|
|
||||||
/// Represent the end node of edge i.
|
|
||||||
vector<size_t> endNodes;
|
|
||||||
|
|
||||||
/// Represent the next edge of edge i.
|
|
||||||
vector<size_t> nextEdges;
|
|
||||||
|
|
||||||
/// Represent the head edge of node i.
|
|
||||||
/// Also, head of simulated linked list.
|
|
||||||
vector<size_t> headEdges;
|
|
||||||
|
|
||||||
vector<bool> bridges;
|
|
||||||
|
|
||||||
/// Represent timestamp of node i, 0 is used as unvisited.
|
|
||||||
vector<size_t> nodeIDs;
|
|
||||||
|
|
||||||
vector<size_t> lowValues;
|
|
||||||
|
|
||||||
size_t number = 1;
|
|
||||||
|
|
||||||
void tarjan(const size_t node, const size_t inEdge) {
|
|
||||||
nodeIDs[node] = lowValues[node] = number;
|
|
||||||
number += 1;
|
|
||||||
|
|
||||||
for (auto i = headEdges[node]; i != 0; i = nextEdges[i]) {
|
|
||||||
|
|
||||||
// If the next node is not visited.
|
|
||||||
if (const auto end = endNodes[i]; nodeIDs[end] == 0) {
|
|
||||||
tarjan(end, i);
|
|
||||||
|
|
||||||
lowValues[node] = min(lowValues[node], lowValues[end]);
|
|
||||||
if (lowValues[end] > nodeIDs[node]) {
|
|
||||||
// Subtract 2 as the edge ID starts from 2.
|
|
||||||
bridges[i - 2] = true;
|
|
||||||
bridges[(i ^ 1) - 2] = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If edge i is visited and edge i is not the coming edge.
|
|
||||||
if (i != (inEdge ^ 1)) {
|
|
||||||
lowValues[node] = min(lowValues[node], nodeIDs[end]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
class Solution {
|
|
||||||
public:
|
|
||||||
vector<vector<int>> criticalConnections(int n,
|
|
||||||
vector<vector<int>> &connections) {
|
|
||||||
// To store the undirected graph, double the edge count.
|
|
||||||
auto g = Graph{static_cast<size_t>(n), connections.size() * 2};
|
|
||||||
|
|
||||||
for (const auto &edge : connections) {
|
|
||||||
g.addEdge(edge[0], edge[1]);
|
|
||||||
g.addEdge(edge[1], edge[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto bridges = g.calculateBridges();
|
|
||||||
vector<vector<int>> result;
|
|
||||||
for (auto i = 0; i < bridges.size(); i = i + 2) {
|
|
||||||
if (bridges[i]) {
|
|
||||||
const auto &edge = connections[i / 2];
|
|
||||||
result.push_back(edge);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### 链式前向星
|
|
||||||
|
|
||||||
在上面的实现中使用了一种较为高效的图存储方法-链式前向星(Linked Forward Star)。
|
|
||||||
|
|
||||||
链式前向星是一种类似于邻接表的图存储方法,提供了较为高效的边遍历方法。这种方法的本质上是按节点聚合的边链表,不过在上面的实现中使用了数组来存储链表的头结点和每个节点的下一个节点指针。
|
|
||||||
|
|
||||||
同时这种存储方法还提供了一种非常方便的反向边查找方法:考虑在存储无向图中的边时,将一条边成对的存储在数组中,由此针对任意一条边i,`i ^ 1`就是这条边的反向边。
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,9 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
using DotNext;
|
using DotNext;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Moq;
|
using Moq;
|
||||||
using YaeBlog.Abstractions.Models;
|
using YaeBlog.Models;
|
||||||
using YaeBlog.Services;
|
using YaeBlog.Services;
|
||||||
|
|
||||||
namespace YaeBlog.Tests;
|
namespace YaeBlog.Tests;
|
||||||
@@ -10,7 +9,6 @@ namespace YaeBlog.Tests;
|
|||||||
public sealed class GiteaFetchServiceTests
|
public sealed class GiteaFetchServiceTests
|
||||||
{
|
{
|
||||||
private static readonly Mock<IOptions<GiteaOptions>> s_giteaOptionsMock = new();
|
private static readonly Mock<IOptions<GiteaOptions>> s_giteaOptionsMock = new();
|
||||||
private static readonly Mock<ILogger<GiteaFetchService>> s_logger = new();
|
|
||||||
private readonly GiteaFetchService _giteaFetchService;
|
private readonly GiteaFetchService _giteaFetchService;
|
||||||
|
|
||||||
public GiteaFetchServiceTests()
|
public GiteaFetchServiceTests()
|
||||||
@@ -18,10 +16,12 @@ public sealed class GiteaFetchServiceTests
|
|||||||
s_giteaOptionsMock.SetupGet(o => o.Value)
|
s_giteaOptionsMock.SetupGet(o => o.Value)
|
||||||
.Returns(new GiteaOptions
|
.Returns(new GiteaOptions
|
||||||
{
|
{
|
||||||
BaseAddress = "https://git.rrricardo.top/api/v1/", HeatMapUsername = "jackfiled"
|
BaseAddress = "https://git.rrricardo.top/api/v1/",
|
||||||
|
ApiKey = "7e33617e5d084199332fceec3e0cb04c6ddced55",
|
||||||
|
HeatMapUsername = "jackfiled"
|
||||||
});
|
});
|
||||||
|
|
||||||
_giteaFetchService = new GiteaFetchService(s_giteaOptionsMock.Object, new HttpClient(), s_logger.Object);
|
_giteaFetchService = new GiteaFetchService(s_giteaOptionsMock.Object, new HttpClient());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Moq;
|
using Moq;
|
||||||
using YaeBlog.Abstractions.Models;
|
using YaeBlog.Models;
|
||||||
|
|
||||||
namespace YaeBlog.Tests;
|
namespace YaeBlog.Tests;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using YaeBlog.Abstractions.Models;
|
using YaeBlog.Models;
|
||||||
|
|
||||||
namespace YaeBlog.Abstractions;
|
namespace YaeBlog.Abstraction;
|
||||||
|
|
||||||
public interface IEssayContentService
|
public interface IEssayContentService
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using YaeBlog.Abstractions.Models;
|
using YaeBlog.Models;
|
||||||
|
|
||||||
namespace YaeBlog.Abstractions;
|
namespace YaeBlog.Abstraction;
|
||||||
|
|
||||||
public interface IEssayScanService
|
public interface IEssayScanService
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using YaeBlog.Abstractions.Models;
|
using YaeBlog.Models;
|
||||||
|
|
||||||
namespace YaeBlog.Abstractions;
|
namespace YaeBlog.Abstraction;
|
||||||
|
|
||||||
public interface IPostRenderProcessor
|
public interface IPostRenderProcessor
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using YaeBlog.Abstractions.Models;
|
using YaeBlog.Models;
|
||||||
|
|
||||||
namespace YaeBlog.Abstractions;
|
namespace YaeBlog.Abstraction;
|
||||||
|
|
||||||
public interface IPreRenderProcessor
|
public interface IPreRenderProcessor
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
@using Microsoft.Extensions.Options
|
@using Microsoft.Extensions.Options
|
||||||
@using YaeBlog.Abstractions
|
@using YaeBlog.Abstraction
|
||||||
@using YaeBlog.Abstractions.Models
|
@using YaeBlog.Models
|
||||||
|
|
||||||
@inject IEssayContentService Contents
|
@inject IEssayContentService Contents
|
||||||
@inject IOptions<BlogOptions> Options
|
@inject IOptions<BlogOptions> Options
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@using System.Text.Encodings.Web
|
@using System.Text.Encodings.Web
|
||||||
@using YaeBlog.Abstractions.Models
|
@using YaeBlog.Models
|
||||||
|
|
||||||
<div class="flex flex-col p-3">
|
<div class="flex flex-col p-3">
|
||||||
<div class="text-3xl font-bold py-2">
|
<div class="text-3xl font-bold py-2">
|
||||||
|
|||||||
@@ -7,13 +7,10 @@
|
|||||||
<Anchor Address="https://dotnet.microsoft.com" Text="@DotnetVersion"/>
|
<Anchor Address="https://dotnet.microsoft.com" Text="@DotnetVersion"/>
|
||||||
驱动。
|
驱动。
|
||||||
</p>
|
</p>
|
||||||
@if (!string.IsNullOrEmpty(BuildCommitId))
|
<p class="text-md">
|
||||||
{
|
Build Commit #
|
||||||
<p class="text-md">
|
<Anchor Address="@BuildCommitUrl" Text="@BuildCommitId"/>
|
||||||
Build Commit #
|
</p>
|
||||||
<Anchor Address="@BuildCommitUrl" Text="@BuildCommitId" NewPage="true"/>
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -27,7 +24,7 @@
|
|||||||
{
|
{
|
||||||
private static string DotnetVersion => $".NET {Environment.Version}";
|
private static string DotnetVersion => $".NET {Environment.Version}";
|
||||||
|
|
||||||
private static string? BuildCommitId => Environment.GetEnvironmentVariable("COMMIT_ID");
|
private static string BuildCommitId => Environment.GetEnvironmentVariable("COMMIT_ID") ?? "local_build";
|
||||||
|
|
||||||
private static string BuildCommitUrl => $"https://git.rrricardo.top/jackfiled/YaeBlog/commit/{BuildCommitId}";
|
private static string BuildCommitUrl => $"https://git.rrricardo.top/jackfiled/YaeBlog/commit/{BuildCommitId}";
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@using YaeBlog.Abstractions.Models
|
@using YaeBlog.Models
|
||||||
@using YaeBlog.Services
|
@using YaeBlog.Services
|
||||||
@inject GitHeapMapService GitHeapMapInstance
|
@inject GitHeapMapService GitHeapMapInstance
|
||||||
|
|
||||||
@@ -7,13 +7,13 @@
|
|||||||
<SvgGroup Transform="@GlobalMonthTransform">
|
<SvgGroup Transform="@GlobalMonthTransform">
|
||||||
@foreach ((int i, string text) in _monthIndices)
|
@foreach ((int i, string text) in _monthIndices)
|
||||||
{
|
{
|
||||||
<SvgText Content="@text" Transform="@(MonthTextTransform(i))" Class="text-[8px] font-light"/>
|
<SvgText Content="@text" Transform="@(MonthTextTransform(i))" Class="text-[10px]"/>
|
||||||
}
|
}
|
||||||
</SvgGroup>
|
</SvgGroup>
|
||||||
<SvgGroup Transform="@GlobalWeekTransform">
|
<SvgGroup Transform="@GlobalWeekTransform">
|
||||||
@foreach ((int i, string text) in Weekdays.Index())
|
@foreach ((int i, string text) in Weekdays.Index())
|
||||||
{
|
{
|
||||||
<SvgText Content="@text" Transform="@(DayTextTransform(i))" Class="text-[8px] font-light"/>
|
<SvgText Content="@text" Transform="@(DayTextTransform(i))" Class="text-[10px]"/>
|
||||||
}
|
}
|
||||||
</SvgGroup>
|
</SvgGroup>
|
||||||
<SvgGroup Transform="@GlobalMapTransform">
|
<SvgGroup Transform="@GlobalMapTransform">
|
||||||
@@ -23,8 +23,7 @@
|
|||||||
@foreach ((int j, GitContributionItem item) in contribution.Contributions.Index())
|
@foreach ((int j, GitContributionItem item) in contribution.Contributions.Index())
|
||||||
{
|
{
|
||||||
<Rectangle Width="@Width" Height="@Width" Transform="@(WeekdayGridTransform(j))"
|
<Rectangle Width="@Width" Height="@Width" Transform="@(WeekdayGridTransform(j))"
|
||||||
Class="@(GetColorByContribution(item.ContributionCount))"
|
Class="@(GetColorByContribution(item.ContributionCount))"/>
|
||||||
Id="@(item.ItemId)"/>
|
|
||||||
}
|
}
|
||||||
</SvgGroup>
|
</SvgGroup>
|
||||||
}
|
}
|
||||||
@@ -119,4 +118,5 @@
|
|||||||
_ => "fill-blue-800"
|
_ => "fill-blue-800"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,5 +24,5 @@
|
|||||||
@Body
|
@Body
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer/>
|
<Foonter/>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -32,5 +32,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer/>
|
<Foonter/>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-4xl page-starter">关于</h1>
|
<h1 class="text-4xl">关于</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="py-4">
|
<div class="py-4">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@page "/blog/archives"
|
@page "/blog/archives"
|
||||||
@using YaeBlog.Abstractions
|
@using YaeBlog.Abstraction
|
||||||
@using YaeBlog.Abstractions.Models
|
@using YaeBlog.Models
|
||||||
|
|
||||||
@inject IEssayContentService Contents
|
@inject IEssayContentService Contents
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-4xl page-starter">归档</h1>
|
<h1 class="text-4xl">归档</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="py-4">
|
<div class="py-4">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@page "/blog"
|
@page "/blog"
|
||||||
@using YaeBlog.Abstractions
|
@using YaeBlog.Abstraction
|
||||||
@using YaeBlog.Abstractions.Models
|
@using YaeBlog.Models
|
||||||
|
|
||||||
@inject IEssayContentService Contents
|
@inject IEssayContentService Contents
|
||||||
@inject NavigationManager NavigationInstance
|
@inject NavigationManager NavigationInstance
|
||||||
@@ -10,7 +10,6 @@
|
|||||||
</PageTitle>
|
</PageTitle>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="page-starter"></div>
|
|
||||||
<div class="grid grid-cols-4">
|
<div class="grid grid-cols-4">
|
||||||
<div class="col-span-4 md:col-span-3">
|
<div class="col-span-4 md:col-span-3">
|
||||||
@foreach (BlogEssay essay in _essays)
|
@foreach (BlogEssay essay in _essays)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@page "/blog/essays/{BlogKey}"
|
@page "/blog/essays/{BlogKey}"
|
||||||
@using System.Text.Encodings.Web
|
@using System.Text.Encodings.Web
|
||||||
@using YaeBlog.Abstractions
|
@using YaeBlog.Abstraction
|
||||||
@using YaeBlog.Abstractions.Models
|
@using YaeBlog.Models
|
||||||
|
|
||||||
@inject IEssayContentService Contents
|
@inject IEssayContentService Contents
|
||||||
@inject NavigationManager NavigationInstance
|
@inject NavigationManager NavigationInstance
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<div>
|
<div>
|
||||||
<h1 id="title" class="text-4xl page-starter">@(_essay!.Title)</h1>
|
<h1 id="title" class="text-4xl">@(_essay!.Title)</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-row gap-4 py-2">
|
<div class="flex flex-row gap-4 py-2">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@page "/friends"
|
@page "/friends"
|
||||||
@using Microsoft.Extensions.Options
|
@using Microsoft.Extensions.Options
|
||||||
@using YaeBlog.Abstractions.Models
|
@using YaeBlog.Models
|
||||||
@inject IOptions<BlogOptions> BlogOptionInstance
|
@inject IOptions<BlogOptions> BlogOptionInstance
|
||||||
|
|
||||||
<PageTitle>
|
<PageTitle>
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-4xl page-starter">
|
<h1 class="text-4xl">
|
||||||
友链
|
友链
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
@using YaeBlog.Abstractions
|
@using YaeBlog.Abstraction
|
||||||
@using YaeBlog.Abstractions.Models
|
@using YaeBlog.Models
|
||||||
@inject IEssayContentService EssayContentInstance
|
@inject IEssayContentService EssayContentInstance
|
||||||
|
|
||||||
<PageTitle>
|
<PageTitle>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<div class="col-span-3 md:col-span-2">
|
<div class="col-span-3 md:col-span-2">
|
||||||
<div class="flex flex-col gap-y-3 items-center md:items-start md:px-6">
|
<div class="flex flex-col gap-y-3 items-center md:items-start md:px-6">
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="text-3xl font-bold page-starter">初冬的朝阳</div>
|
<div class="text-3xl font-bold">初冬的朝阳</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="">
|
<div class="">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
</PageTitle>
|
</PageTitle>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-3xl page-starter">NotFound!</h3>
|
<h3 class="text-3xl">NotFound!</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@page "/blog/tags/"
|
@page "/blog/tags/"
|
||||||
@using System.Text.Encodings.Web
|
@using System.Text.Encodings.Web
|
||||||
@using YaeBlog.Abstractions
|
@using YaeBlog.Abstraction
|
||||||
@using YaeBlog.Abstractions.Models
|
@using YaeBlog.Models
|
||||||
|
|
||||||
@inject IEssayContentService Contents
|
@inject IEssayContentService Contents
|
||||||
@inject NavigationManager NavigationInstance
|
@inject NavigationManager NavigationInstance
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
</PageTitle>
|
</PageTitle>
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="page-starter">
|
<div>
|
||||||
@if (TagName is null)
|
@if (TagName is null)
|
||||||
{
|
{
|
||||||
<h1 class="text-4xl">标签</h1>
|
<h1 class="text-4xl">标签</h1>
|
||||||
|
|||||||
@@ -9,6 +9,6 @@
|
|||||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)"></RouteView>
|
<RouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)"></RouteView>
|
||||||
}
|
}
|
||||||
|
|
||||||
<FocusOnNavigate RouteData="routeData" Selector="page-starter"/>
|
<FocusOnNavigate RouteData="routeData" Selector="h1"/>
|
||||||
</Found>
|
</Found>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@@ -18,17 +18,5 @@ public static class DateOnlyExtensions
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public int DayNumberOfWeek
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return date.DayOfWeek switch
|
|
||||||
{
|
|
||||||
DayOfWeek.Sunday => 7,
|
|
||||||
_ => (int)date.DayOfWeek + 1
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,73 +1,26 @@
|
|||||||
using AngleSharp;
|
using AngleSharp;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using YaeBlog.Abstractions;
|
using YaeBlog.Abstraction;
|
||||||
using YaeBlog.Services;
|
using YaeBlog.Services;
|
||||||
using YaeBlog.Abstractions.Models;
|
using YaeBlog.Models;
|
||||||
using YaeBlog.Processors;
|
using YaeBlog.Processors;
|
||||||
|
|
||||||
namespace YaeBlog.Extensions;
|
namespace YaeBlog.Extensions;
|
||||||
|
|
||||||
public static class HostApplicationBuilderExtensions
|
public static class WebApplicationBuilderExtensions
|
||||||
{
|
{
|
||||||
extension(IHostApplicationBuilder builder)
|
extension(WebApplicationBuilder builder)
|
||||||
{
|
{
|
||||||
public ConsoleInfoService AddYaeCommand(string[] arguments)
|
public WebApplicationBuilder AddYaeBlog()
|
||||||
{
|
{
|
||||||
builder.AddCommonServices();
|
builder.ConfigureOptions<BlogOptions>(BlogOptions.OptionName)
|
||||||
|
.ConfigureOptions<GiteaOptions>(GiteaOptions.OptionName);
|
||||||
|
|
||||||
builder.Logging.AddFilter("Microsoft.Hosting.Lifetime", LogLevel.Warning);
|
|
||||||
|
|
||||||
builder.Services.AddTransient<ImageCompressService>();
|
|
||||||
builder.Services.AddHostedService<YaeCommandService>(provider =>
|
|
||||||
{
|
|
||||||
IEssayScanService essayScanService = provider.GetRequiredService<IEssayScanService>();
|
|
||||||
ImageCompressService imageCompressService = provider.GetRequiredService<ImageCompressService>();
|
|
||||||
ConsoleInfoService consoleInfoService = provider.GetRequiredService<ConsoleInfoService>();
|
|
||||||
IOptions<BlogOptions> blogOptions = provider.GetRequiredService<IOptions<BlogOptions>>();
|
|
||||||
ILogger<YaeCommandService> logger = provider.GetRequiredService<ILogger<YaeCommandService>>();
|
|
||||||
IHostApplicationLifetime hostApplicationLifetime =
|
|
||||||
provider.GetRequiredService<IHostApplicationLifetime>();
|
|
||||||
|
|
||||||
return new YaeCommandService(arguments, essayScanService, imageCompressService, consoleInfoService,
|
|
||||||
hostApplicationLifetime, blogOptions, logger);
|
|
||||||
});
|
|
||||||
|
|
||||||
ConsoleInfoService infoService = new();
|
|
||||||
builder.Services.AddSingleton<ConsoleInfoService>(_ => infoService);
|
|
||||||
|
|
||||||
return infoService;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddCommonServices()
|
|
||||||
{
|
|
||||||
builder.Services.AddHttpClient()
|
builder.Services.AddHttpClient()
|
||||||
.AddMarkdig()
|
.AddMarkdig()
|
||||||
.AddYamlParser();
|
.AddYamlParser();
|
||||||
|
|
||||||
builder.ConfigureOptions<BlogOptions>(BlogOptions.OptionName)
|
|
||||||
.ConfigureOptions<GiteaOptions>(GiteaOptions.OptionName);
|
|
||||||
|
|
||||||
builder.Services.AddSingleton<IEssayScanService, EssayScanService>();
|
|
||||||
}
|
|
||||||
|
|
||||||
private IHostApplicationBuilder ConfigureOptions<T>(string optionSectionName) where T : class
|
|
||||||
{
|
|
||||||
builder.Services
|
|
||||||
.AddOptions<T>()
|
|
||||||
.Bind(builder.Configuration.GetSection(optionSectionName))
|
|
||||||
.ValidateDataAnnotations();
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension(WebApplicationBuilder builder)
|
|
||||||
{
|
|
||||||
public WebApplicationBuilder AddYaeServer(ConsoleInfoService consoleInfoService)
|
|
||||||
{
|
|
||||||
builder.AddCommonServices();
|
|
||||||
|
|
||||||
builder.Services.AddSingleton<AngleSharp.IConfiguration>(_ => Configuration.Default)
|
builder.Services.AddSingleton<AngleSharp.IConfiguration>(_ => Configuration.Default)
|
||||||
.AddSingleton<ConsoleInfoService>(_ => consoleInfoService)
|
|
||||||
.AddSingleton<IEssayScanService, EssayScanService>()
|
.AddSingleton<IEssayScanService, EssayScanService>()
|
||||||
.AddSingleton<RendererService>()
|
.AddSingleton<RendererService>()
|
||||||
.AddSingleton<IEssayContentService, EssayContentService>()
|
.AddSingleton<IEssayContentService, EssayContentService>()
|
||||||
@@ -79,8 +32,31 @@ public static class HostApplicationBuilderExtensions
|
|||||||
.AddTransient<BlogHotReloadService>()
|
.AddTransient<BlogHotReloadService>()
|
||||||
.AddSingleton<GitHeapMapService>();
|
.AddSingleton<GitHeapMapService>();
|
||||||
|
|
||||||
builder.Services.AddHostedService<StartServerService>();
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WebApplicationBuilder AddYaeCommand(string[] arguments)
|
||||||
|
{
|
||||||
|
builder.Services.AddHostedService<YaeCommandService>(provider =>
|
||||||
|
{
|
||||||
|
IEssayScanService essayScanService = provider.GetRequiredService<IEssayScanService>();
|
||||||
|
IOptions<BlogOptions> blogOptions = provider.GetRequiredService<IOptions<BlogOptions>>();
|
||||||
|
ILogger<YaeCommandService> logger = provider.GetRequiredService<ILogger<YaeCommandService>>();
|
||||||
|
IHostApplicationLifetime applicationLifetime = provider.GetRequiredService<IHostApplicationLifetime>();
|
||||||
|
|
||||||
|
return new YaeCommandService(arguments, essayScanService, provider, blogOptions, logger,
|
||||||
|
applicationLifetime);
|
||||||
|
});
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebApplicationBuilder ConfigureOptions<T>(string optionSectionName) where T : class
|
||||||
|
{
|
||||||
|
builder.Services
|
||||||
|
.AddOptions<T>()
|
||||||
|
.Bind(builder.Configuration.GetSection(optionSectionName))
|
||||||
|
.ValidateDataAnnotations();
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using YaeBlog.Abstractions;
|
using YaeBlog.Abstraction;
|
||||||
using YaeBlog.Processors;
|
using YaeBlog.Processors;
|
||||||
using YaeBlog.Services;
|
using YaeBlog.Services;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace YaeBlog.Abstractions.Models;
|
namespace YaeBlog.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 单个博客文件的所有数据和元数据
|
/// 单个博客文件的所有数据和元数据
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
namespace YaeBlog.Abstractions.Models;
|
namespace YaeBlog.Models;
|
||||||
|
|
||||||
public record BlogContents(ConcurrentBag<BlogContent> Drafts, ConcurrentBag<BlogContent> Posts)
|
public record BlogContents(ConcurrentBag<BlogContent> Drafts, ConcurrentBag<BlogContent> Posts)
|
||||||
: IEnumerable<BlogContent>
|
: IEnumerable<BlogContent>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace YaeBlog.Abstractions.Models;
|
namespace YaeBlog.Models;
|
||||||
|
|
||||||
public record BlogEssay(
|
public record BlogEssay(
|
||||||
string Title,
|
string Title,
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace YaeBlog.Abstractions.Models;
|
namespace YaeBlog.Models;
|
||||||
|
|
||||||
public class BlogHeadline(string title, string selectorId)
|
public class BlogHeadline(string title, string selectorId)
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace YaeBlog.Abstractions.Models;
|
namespace YaeBlog.Models;
|
||||||
|
|
||||||
public record BlogImageInfo(FileInfo File, long Width, long Height, string MineType, byte[] Content, bool IsUsed)
|
public record BlogImageInfo(FileInfo File, long Width, long Height, string MineType, byte[] Content, bool IsUsed)
|
||||||
: IComparable<BlogImageInfo>
|
: IComparable<BlogImageInfo>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace YaeBlog.Abstractions.Models;
|
namespace YaeBlog.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 友链模型类
|
/// 友链模型类
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
|
|
||||||
namespace YaeBlog.Abstractions.Models;
|
namespace YaeBlog.Models;
|
||||||
|
|
||||||
public class EssayTag(string tagName) : IEquatable<EssayTag>
|
public class EssayTag(string tagName) : IEquatable<EssayTag>
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace YaeBlog.Abstractions.Models;
|
namespace YaeBlog.Models;
|
||||||
|
|
||||||
public class GiteaOptions
|
public class GiteaOptions
|
||||||
{
|
{
|
||||||
@@ -8,7 +8,7 @@ public class GiteaOptions
|
|||||||
|
|
||||||
[Required] public required string BaseAddress { get; init; }
|
[Required] public required string BaseAddress { get; init; }
|
||||||
|
|
||||||
public string? ApiKey { get; init; }
|
[Required] public required string ApiKey { get; init; }
|
||||||
|
|
||||||
[Required] public required string HeatMapUsername { get; init; }
|
[Required] public required string HeatMapUsername { get; init; }
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
namespace YaeBlog.Abstractions.Models;
|
namespace YaeBlog.Models;
|
||||||
|
|
||||||
public record GitContributionItem(DateOnly Time, long ContributionCount)
|
public record GitContributionItem(DateOnly Time, long ContributionCount);
|
||||||
{
|
|
||||||
public string ItemId => $"item-{Time:yyyy-MM-dd}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public record GitContributionGroupedByWeek(DateOnly Monday, List<GitContributionItem> Contributions);
|
public record GitContributionGroupedByWeek(DateOnly Monday, List<GitContributionItem> Contributions);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace YaeBlog.Abstractions.Models;
|
namespace YaeBlog.Models;
|
||||||
|
|
||||||
public class MarkdownMetadata
|
public class MarkdownMetadata
|
||||||
{
|
{
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
using AngleSharp;
|
using AngleSharp;
|
||||||
using AngleSharp.Dom;
|
using AngleSharp.Dom;
|
||||||
using YaeBlog.Abstractions;
|
using YaeBlog.Abstraction;
|
||||||
using YaeBlog.Extensions;
|
using YaeBlog.Extensions;
|
||||||
using YaeBlog.Abstractions.Models;
|
using YaeBlog.Models;
|
||||||
|
|
||||||
namespace YaeBlog.Processors;
|
namespace YaeBlog.Processors;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using AngleSharp;
|
using AngleSharp;
|
||||||
using AngleSharp.Dom;
|
using AngleSharp.Dom;
|
||||||
using YaeBlog.Abstractions;
|
using YaeBlog.Abstraction;
|
||||||
using YaeBlog.Abstractions.Models;
|
using YaeBlog.Models;
|
||||||
|
|
||||||
namespace YaeBlog.Processors;
|
namespace YaeBlog.Processors;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
using AngleSharp;
|
using AngleSharp;
|
||||||
using AngleSharp.Dom;
|
using AngleSharp.Dom;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using YaeBlog.Abstractions;
|
using YaeBlog.Abstraction;
|
||||||
using YaeBlog.Core.Exceptions;
|
using YaeBlog.Core.Exceptions;
|
||||||
using YaeBlog.Abstractions.Models;
|
using YaeBlog.Models;
|
||||||
|
|
||||||
namespace YaeBlog.Processors;
|
namespace YaeBlog.Processors;
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,13 @@
|
|||||||
using YaeBlog.Components;
|
using YaeBlog.Components;
|
||||||
using YaeBlog.Extensions;
|
using YaeBlog.Extensions;
|
||||||
using YaeBlog.Services;
|
|
||||||
|
|
||||||
HostApplicationBuilder consoleBuilder = Host.CreateApplicationBuilder(args);
|
|
||||||
|
|
||||||
ConsoleInfoService consoleInfoService = consoleBuilder.AddYaeCommand(args);
|
|
||||||
|
|
||||||
IHost consoleApp = consoleBuilder.Build();
|
|
||||||
await consoleApp.RunAsync();
|
|
||||||
|
|
||||||
if (consoleInfoService.IsOneShotCommand)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
builder.Services.AddRazorComponents()
|
builder.Services.AddRazorComponents()
|
||||||
.AddInteractiveServerComponents();
|
.AddInteractiveServerComponents();
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
builder.AddYaeServer(consoleInfoService);
|
builder.AddYaeBlog();
|
||||||
|
builder.AddYaeCommand(args);
|
||||||
|
|
||||||
WebApplication application = builder.Build();
|
WebApplication application = builder.Build();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using YaeBlog.Abstractions.Models;
|
using YaeBlog.Models;
|
||||||
|
|
||||||
namespace YaeBlog.Services;
|
namespace YaeBlog.Services;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using YaeBlog.Abstractions;
|
using YaeBlog.Abstraction;
|
||||||
|
|
||||||
namespace YaeBlog.Services;
|
namespace YaeBlog.Services;
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
namespace YaeBlog.Services;
|
|
||||||
|
|
||||||
public enum ServerCommand
|
|
||||||
{
|
|
||||||
Serve,
|
|
||||||
Watch
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ConsoleInfoService
|
|
||||||
{
|
|
||||||
public bool IsOneShotCommand { get; set; }
|
|
||||||
|
|
||||||
public ServerCommand Command { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using YaeBlog.Abstractions;
|
using YaeBlog.Abstraction;
|
||||||
using YaeBlog.Abstractions.Models;
|
using YaeBlog.Models;
|
||||||
|
|
||||||
namespace YaeBlog.Services;
|
namespace YaeBlog.Services;
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ using System.Text.RegularExpressions;
|
|||||||
using Imageflow.Bindings;
|
using Imageflow.Bindings;
|
||||||
using Imageflow.Fluent;
|
using Imageflow.Fluent;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using YaeBlog.Abstractions;
|
using YaeBlog.Abstraction;
|
||||||
using YaeBlog.Core.Exceptions;
|
using YaeBlog.Core.Exceptions;
|
||||||
using YaeBlog.Abstractions.Models;
|
using YaeBlog.Models;
|
||||||
using YamlDotNet.Core;
|
using YamlDotNet.Core;
|
||||||
using YamlDotNet.Serialization;
|
using YamlDotNet.Serialization;
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
using DotNext;
|
using DotNext;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using YaeBlog.Extensions;
|
using YaeBlog.Extensions;
|
||||||
using YaeBlog.Abstractions.Models;
|
using YaeBlog.Models;
|
||||||
|
|
||||||
namespace YaeBlog.Services;
|
namespace YaeBlog.Services;
|
||||||
|
|
||||||
public sealed class GitHeapMapService(
|
public sealed class GitHeapMapService(IServiceProvider serviceProvider, IOptions<GiteaOptions> giteaOptions,
|
||||||
IServiceProvider serviceProvider,
|
|
||||||
IOptions<GiteaOptions> giteaOptions,
|
|
||||||
ILogger<GitHeapMapService> logger)
|
ILogger<GitHeapMapService> logger)
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -85,24 +83,7 @@ public sealed class GitHeapMapService(
|
|||||||
groupedContribution.Contributions.Add(new GitContributionItem(date, contributions));
|
groupedContribution.Contributions.Add(new GitContributionItem(date, contributions));
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the last contributing day is not today, fill the spacing.
|
// Not fill the last item and add directly.
|
||||||
// But be careful here! If the last grouped contribution is current week, just fill the spacing until today.
|
|
||||||
// If the last grouped contribution is before current week, first fill the blank week then fill until today.
|
|
||||||
while (groupedContribution.Monday < today.LastMonday)
|
|
||||||
{
|
|
||||||
FillSpacing(groupedContribution, today);
|
|
||||||
result.Add(groupedContribution);
|
|
||||||
groupedContribution = new GitContributionGroupedByWeek(groupedContribution.Monday.AddDays(7), []);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Currently the grouped contribution must be current week.
|
|
||||||
for (DateOnly date = groupedContribution.Monday.AddDays(groupedContribution.Contributions.Count);
|
|
||||||
date <= today;
|
|
||||||
date = date.AddDays(1))
|
|
||||||
{
|
|
||||||
groupedContribution.Contributions.Add(new GitContributionItem(date, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Add(groupedContribution);
|
result.Add(groupedContribution);
|
||||||
|
|
||||||
_gitContributionsGroupedByWeek = result;
|
_gitContributionsGroupedByWeek = result;
|
||||||
|
|||||||
@@ -3,45 +3,34 @@ using System.Text.Json;
|
|||||||
using DotNext;
|
using DotNext;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using YaeBlog.Core.Exceptions;
|
using YaeBlog.Core.Exceptions;
|
||||||
using YaeBlog.Abstractions.Models;
|
using YaeBlog.Models;
|
||||||
|
|
||||||
namespace YaeBlog.Services;
|
namespace YaeBlog.Services;
|
||||||
|
|
||||||
public sealed class GiteaFetchService
|
public sealed class GiteaFetchService
|
||||||
{
|
{
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly ILogger<GiteaFetchService> _logger;
|
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions s_serializerOptions = new()
|
private static readonly JsonSerializerOptions s_serializerOptions = new()
|
||||||
{
|
{
|
||||||
PropertyNameCaseInsensitive = true,
|
PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
RespectRequiredConstructorParameters = true, RespectNullableAnnotations = true
|
||||||
RespectRequiredConstructorParameters = true,
|
|
||||||
RespectNullableAnnotations = true
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// For test only.
|
/// For test only.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal GiteaFetchService(IOptions<GiteaOptions> giteaOptions, HttpClient httpClient,
|
internal GiteaFetchService(IOptions<GiteaOptions> giteaOptions, HttpClient httpClient)
|
||||||
ILogger<GiteaFetchService> logger)
|
|
||||||
{
|
{
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
_logger = logger;
|
|
||||||
|
|
||||||
_httpClient.BaseAddress = new Uri(giteaOptions.Value.BaseAddress);
|
_httpClient.BaseAddress = new Uri(giteaOptions.Value.BaseAddress);
|
||||||
if (string.IsNullOrWhiteSpace(giteaOptions.Value.ApiKey))
|
_httpClient.DefaultRequestHeaders.Authorization =
|
||||||
{
|
new AuthenticationHeaderValue("Bearer", giteaOptions.Value.ApiKey);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.LogInformation("Api Token is set.");
|
|
||||||
httpClient.DefaultRequestHeaders.Authorization =
|
|
||||||
new AuthenticationHeaderValue("token", giteaOptions.Value.ApiKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public GiteaFetchService(IOptions<GiteaOptions> giteaOptions, IHttpClientFactory httpClientFactory,
|
public GiteaFetchService(IOptions<GiteaOptions> giteaOptions, IHttpClientFactory httpClientFactory) : this(
|
||||||
ILogger<GiteaFetchService> logger) : this(giteaOptions, httpClientFactory.CreateClient(), logger)
|
giteaOptions, httpClientFactory.CreateClient())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +50,6 @@ public sealed class GiteaFetchService
|
|||||||
new GiteaFetchException("Failed to fetch valid data."));
|
new GiteaFetchException("Failed to fetch valid data."));
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Fetch new user heat map data.");
|
|
||||||
return Result.FromValue(data.Select(i =>
|
return Result.FromValue(data.Select(i =>
|
||||||
new GitContributionItem(DateOnly.FromDateTime(DateTimeOffset.FromUnixTimeSeconds(i.Timestamp).DateTime),
|
new GitContributionItem(DateOnly.FromDateTime(DateTimeOffset.FromUnixTimeSeconds(i.Timestamp).DateTime),
|
||||||
i.Contributions)).ToList());
|
i.Contributions)).ToList());
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using Imageflow.Fluent;
|
using Imageflow.Fluent;
|
||||||
using YaeBlog.Abstractions;
|
using YaeBlog.Abstraction;
|
||||||
using YaeBlog.Core.Exceptions;
|
using YaeBlog.Core.Exceptions;
|
||||||
using YaeBlog.Abstractions.Models;
|
using YaeBlog.Models;
|
||||||
|
|
||||||
namespace YaeBlog.Services;
|
namespace YaeBlog.Services;
|
||||||
|
|
||||||
@@ -34,7 +34,6 @@ public sealed class ImageCompressService(IEssayScanService essayScanService, ILo
|
|||||||
|
|
||||||
if (needCompressContents.Count == 0)
|
if (needCompressContents.Count == 0)
|
||||||
{
|
{
|
||||||
logger.LogInformation("No candidates found to be compressed.");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +51,7 @@ public sealed class ImageCompressService(IEssayScanService essayScanService, ILo
|
|||||||
|
|
||||||
foreach (BlogImageInfo image in uncompressedImages)
|
foreach (BlogImageInfo image in uncompressedImages)
|
||||||
{
|
{
|
||||||
logger.LogInformation("Uncompressed image: {filename} belonging to blog {blog}.", image.File.Name,
|
logger.LogInformation("Uncompressed image: {} belonging to blog {}.", image.File.Name,
|
||||||
content.BlogName);
|
content.BlogName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +82,7 @@ public sealed class ImageCompressService(IEssayScanService essayScanService, ILo
|
|||||||
|
|
||||||
logger.LogInformation("Compression ratio: {}%.", (double)compressedSize / uncompressedSize * 100.0);
|
logger.LogInformation("Compression ratio: {}%.", (double)compressedSize / uncompressedSize * 100.0);
|
||||||
|
|
||||||
if (!dryRun)
|
if (dryRun is false)
|
||||||
{
|
{
|
||||||
await Task.WhenAll(from content in compressedContent
|
await Task.WhenAll(from content in compressedContent
|
||||||
select essayScanService.SaveBlogContent(content, content.IsDraft));
|
select essayScanService.SaveBlogContent(content, content.IsDraft));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using YaeBlog.Extensions;
|
using YaeBlog.Extensions;
|
||||||
using YaeBlog.Abstractions.Models;
|
using YaeBlog.Models;
|
||||||
|
|
||||||
namespace YaeBlog.Services
|
namespace YaeBlog.Services
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ using System.Diagnostics;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Markdig;
|
using Markdig;
|
||||||
using YaeBlog.Abstractions;
|
using YaeBlog.Abstraction;
|
||||||
using YaeBlog.Core.Exceptions;
|
using YaeBlog.Core.Exceptions;
|
||||||
using YaeBlog.Abstractions.Models;
|
using YaeBlog.Models;
|
||||||
|
|
||||||
namespace YaeBlog.Services;
|
namespace YaeBlog.Services;
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
namespace YaeBlog.Services;
|
|
||||||
|
|
||||||
public sealed class StartServerService(ConsoleInfoService consoleInfoService,
|
|
||||||
RendererService rendererService,
|
|
||||||
BlogHotReloadService blogHotReloadService) : IHostedService
|
|
||||||
{
|
|
||||||
public async Task StartAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
switch (consoleInfoService.Command)
|
|
||||||
{
|
|
||||||
case ServerCommand.Serve:
|
|
||||||
{
|
|
||||||
await rendererService.RenderAsync();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ServerCommand.Watch:
|
|
||||||
{
|
|
||||||
await blogHotReloadService.StartAsync(cancellationToken);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw new InvalidOperationException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
|
||||||
}
|
|
||||||
@@ -2,31 +2,30 @@
|
|||||||
using System.CommandLine.Invocation;
|
using System.CommandLine.Invocation;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using YaeBlog.Abstractions;
|
using YaeBlog.Abstraction;
|
||||||
using YaeBlog.Core.Exceptions;
|
using YaeBlog.Core.Exceptions;
|
||||||
using YaeBlog.Abstractions.Models;
|
using YaeBlog.Models;
|
||||||
|
|
||||||
namespace YaeBlog.Services;
|
namespace YaeBlog.Services;
|
||||||
|
|
||||||
public sealed class YaeCommandService(
|
public class YaeCommandService(
|
||||||
string[] arguments,
|
string[] arguments,
|
||||||
IEssayScanService essayScanService,
|
IEssayScanService essayScanService,
|
||||||
ImageCompressService imageCompressService,
|
IServiceProvider serviceProvider,
|
||||||
ConsoleInfoService consoleInfoService,
|
|
||||||
IHostApplicationLifetime hostApplicationLifetime,
|
|
||||||
IOptions<BlogOptions> blogOptions,
|
IOptions<BlogOptions> blogOptions,
|
||||||
ILogger<YaeCommandService> logger)
|
ILogger<YaeCommandService> logger,
|
||||||
: BackgroundService
|
IHostApplicationLifetime applicationLifetime)
|
||||||
|
: IHostedService
|
||||||
{
|
{
|
||||||
private readonly BlogOptions _blogOptions = blogOptions.Value;
|
private readonly BlogOptions _blogOptions = blogOptions.Value;
|
||||||
private bool _oneShotCommandFlag = true;
|
private bool _oneShotCommandFlag = true;
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
public async Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
RootCommand rootCommand = new("YaeBlog CLI");
|
RootCommand rootCommand = new("YaeBlog CLI");
|
||||||
|
|
||||||
RegisterServeCommand(rootCommand);
|
RegisterServeCommand(rootCommand);
|
||||||
RegisterWatchCommand(rootCommand);
|
RegisterWatchCommand(rootCommand, cancellationToken);
|
||||||
|
|
||||||
RegisterNewCommand(rootCommand);
|
RegisterNewCommand(rootCommand);
|
||||||
RegisterUpdateCommand(rootCommand);
|
RegisterUpdateCommand(rootCommand);
|
||||||
@@ -34,10 +33,6 @@ public sealed class YaeCommandService(
|
|||||||
RegisterPublishCommand(rootCommand);
|
RegisterPublishCommand(rootCommand);
|
||||||
RegisterCompressCommand(rootCommand);
|
RegisterCompressCommand(rootCommand);
|
||||||
|
|
||||||
// Shit code: wait for the application starting.
|
|
||||||
// If the command service finished early before the application starting, there will be an ugly exception.
|
|
||||||
await Task.Delay(500, stoppingToken);
|
|
||||||
logger.LogInformation("Running YaeBlog Command.");
|
|
||||||
int exitCode = await rootCommand.InvokeAsync(arguments);
|
int exitCode = await rootCommand.InvokeAsync(arguments);
|
||||||
|
|
||||||
if (exitCode != 0)
|
if (exitCode != 0)
|
||||||
@@ -45,15 +40,14 @@ public sealed class YaeCommandService(
|
|||||||
throw new BlogCommandException($"YaeBlog command exited with no-zero code {exitCode}");
|
throw new BlogCommandException($"YaeBlog command exited with no-zero code {exitCode}");
|
||||||
}
|
}
|
||||||
|
|
||||||
consoleInfoService.IsOneShotCommand = _oneShotCommandFlag;
|
if (_oneShotCommandFlag)
|
||||||
|
|
||||||
if (!consoleInfoService.IsOneShotCommand)
|
|
||||||
{
|
{
|
||||||
logger.LogInformation("Start YaeBlog command: {}", consoleInfoService.Command);
|
applicationLifetime.StopApplication();
|
||||||
}
|
}
|
||||||
hostApplicationLifetime.StopApplication();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|
||||||
private void RegisterServeCommand(RootCommand rootCommand)
|
private void RegisterServeCommand(RootCommand rootCommand)
|
||||||
{
|
{
|
||||||
Command command = new("serve", "Start http server.");
|
Command command = new("serve", "Start http server.");
|
||||||
@@ -65,23 +59,27 @@ public sealed class YaeCommandService(
|
|||||||
rootCommand.SetHandler(HandleServeCommand);
|
rootCommand.SetHandler(HandleServeCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task HandleServeCommand(InvocationContext context)
|
private async Task HandleServeCommand(InvocationContext context)
|
||||||
{
|
{
|
||||||
_oneShotCommandFlag = false;
|
_oneShotCommandFlag = false;
|
||||||
consoleInfoService.Command = ServerCommand.Serve;
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
logger.LogInformation("Failed to load cache, re-render essays.");
|
||||||
|
RendererService rendererService = serviceProvider.GetRequiredService<RendererService>();
|
||||||
|
await rendererService.RenderAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RegisterWatchCommand(RootCommand rootCommand)
|
private void RegisterWatchCommand(RootCommand rootCommand, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Command command = new("watch", "Start a blog watcher that re-render when file changes.");
|
Command command = new("watch", "Start a blog watcher that re-render when file changes.");
|
||||||
rootCommand.AddCommand(command);
|
rootCommand.AddCommand(command);
|
||||||
|
|
||||||
command.SetHandler(_ =>
|
command.SetHandler(async _ =>
|
||||||
{
|
{
|
||||||
_oneShotCommandFlag = false;
|
_oneShotCommandFlag = false;
|
||||||
consoleInfoService.Command = ServerCommand.Watch;
|
|
||||||
|
// BlogHotReloadService is derived from BackgroundService, but we do not let framework trigger it.
|
||||||
|
BlogHotReloadService blogHotReloadService = serviceProvider.GetRequiredService<BlogHotReloadService>();
|
||||||
|
await blogHotReloadService.StartAsync(cancellationToken);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,6 +270,12 @@ public sealed class YaeCommandService(
|
|||||||
getDefaultValue: () => false);
|
getDefaultValue: () => false);
|
||||||
command.AddOption(dryRunOption);
|
command.AddOption(dryRunOption);
|
||||||
|
|
||||||
command.SetHandler(async dryRun => { await imageCompressService.Compress(dryRun); }, dryRunOption);
|
command.SetHandler(HandleCompressCommand, dryRunOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleCompressCommand(bool dryRun)
|
||||||
|
{
|
||||||
|
ImageCompressService imageCompressService = serviceProvider.GetRequiredService<ImageCompressService>();
|
||||||
|
await imageCompressService.Compress(dryRun);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="../../third-party/BlazorSvgComponents/src/BlazorSvgComponents/BlazorSvgComponents.csproj" />
|
<ProjectReference Include="../../third-party/BlazorSvgComponents/src/BlazorSvgComponents/BlazorSvgComponents.csproj" />
|
||||||
<ProjectReference Include="..\YaeBlog.Abstractions\YaeBlog.Abstractions.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
},
|
},
|
||||||
"Gitea": {
|
"Gitea": {
|
||||||
"BaseAddress": "https://git.rrricardo.top/api/v1/",
|
"BaseAddress": "https://git.rrricardo.top/api/v1/",
|
||||||
|
"ApiKey": "7e33617e5d084199332fceec3e0cb04c6ddced55",
|
||||||
"HeatMapUsername": "jackfiled"
|
"HeatMapUsername": "jackfiled"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user