Compare commits

1 Commits

Author SHA1 Message Date
67c72284d5 dev: rubbish 2025-01-16 17:30:34 +08:00
61 changed files with 794 additions and 1623 deletions

View File

@@ -5,9 +5,7 @@ namespace YaeBlog.Abstraction;
public interface IEssayContentService
{
public IEnumerable<BlogEssay> Essays { get; }
public int Count { get; }
public IReadOnlyDictionary<string, BlogEssay> Essays { get; }
public IReadOnlyDictionary<EssayTag, List<BlogEssay>> Tags { get; }
@@ -18,8 +16,6 @@ public interface IEssayContentService
public bool TryAdd(BlogEssay essay);
public bool TryGetEssay(string filename, [NotNullWhen(true)] out BlogEssay? essay);
public void RefreshTags();
public void Clear();

View File

@@ -70,6 +70,7 @@ public sealed class YaeBlogCommand
builder.Services.AddControllers();
builder.AddYaeBlog();
builder.AddWatcher();
builder.AddTailwindWatcher();
WebApplication application = builder.Build();

View File

@@ -1,9 +0,0 @@
<a href="@Address" class="text-blue-600" target="@(NewPage ? "_blank" : "_self")">@Text</a>
@code {
[Parameter] public string? Address { get; set; }
[Parameter] public string? Text { get; set; }
[Parameter] public bool NewPage { get; set; }
}

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="zh">
<html lang="en">
<head>
<meta charset="utf-8"/>
@@ -13,8 +13,13 @@
</head>
<body>
<Routes/>
<script src="_framework/blazor.web.js"></script>
<Routes/>
<script src="_framework/blazor.web.js"></script>
<script src="clipboard.min.js"></script>
<script>
const clipboard = new ClipboardJS('.btn');
</script>
</body>
</html>

View File

@@ -4,46 +4,54 @@
@inject IEssayContentService Contents
@inject BlogOptions Options
<div class="flex flex-col">
<div class="p-10">
<img src="images/avatar.png" alt="Ricardo's Avatar" class="h-auto max-w-full"/>
<div class="container">
<div class="row justify-content-center">
<div class="col-auto p-4">
@* <Image Src="images/avatar.png" Alt="Ricardo's avatar"/> *@
</div>
</div>
<div class="px-10 py-2 text-xl">
<div class="row justify-content-center p-3">
<div class="col-auto fs-4">
“奇奇怪怪东西的聚合地”
</div>
</div>
<div class="flex flex-row justify-between px-6 py-2 text-xl">
<div>
<div class="row justify-content-between px-2 py-1 fs-5">
<div class="col-auto">
文章
</div>
<a href="/blog/archives/">
<div>
@(Contents.Count)
</div>
<div class="col-auto">
<a href="/blog/archives">
@(Contents.Essays.Count)
</a>
</div>
</div>
<div class="flex flex-row justify-between px-6 py-2 text-xl">
<div>
<div class="row justify-content-between px-2 py-1 fs-5">
<div class="col-auto">
标签
</div>
<a href="/blog/tags/">
<div>
<div class="col-auto">
<a href="/blog/tags">
@(Contents.Tags.Count)
</div>
</a>
</div>
<div class="text-xl px-2 py-2">
广而告之
</div>
<div class="px-6">
<p class="text-lg">
<div class="row justify-content-start fs-5" style="padding-top: 2em">
<div class="col-auto">
广而告之
</div>
</div>
<div class="row">
<div class="col">
<p style="text-indent: 2em">
@(Options.Announcement)
</p>
</div>
</div>
</div>

View File

@@ -1,19 +1,19 @@
@using System.Text.Encodings.Web
@using YaeBlog.Models
<div class="flex flex-col p-3">
<div class="text-3xl font-bold py-2">
<div class="container p-3">
<div class="row fs-2 fw-bold py-2 essay-title">
<a href="/blog/essays/@(Essay.FileName)" target="_blank">@(Essay.Title)</a>
</div>
<div class="p-2 flex flex-row justify-content-start gap-2">
<div class="font-light">
<div class="row p-2 justify-content-start">
<div class="col-auto fw-light">
@(Essay.PublishTime.ToString("yyyy-MM-dd"))
</div>
@foreach (string key in Essay.Tags)
{
<div class="text-sky-600">
<div class="col-auto">
<a href="/blog/tags/?tagName=@(UrlEncoder.Default.Encode(key))">
# @key
</a>
@@ -21,11 +21,20 @@
}
</div>
<div class="p-2">
<div class="row p-2">
<div class="col">
@(Essay.Description)
</div>
</div>
<div class="row">
<div class="col border-bottom">
</div>
</div>
</div>
@code {
[Parameter] public required BlogEssay Essay { get; set; }
[Parameter]
public required BlogEssay Essay { get; set; }
}

View File

@@ -0,0 +1,3 @@
.essay-title a {
color: var(--bs-body-color);
}

View File

@@ -1,22 +1,14 @@
<div class="flex flex-col text-center py-2">
<div>
<p class="text-md">
2021 - @(DateTimeOffset.Now.Year) ©
<Anchor Address="https://rrricardot.top" Text="Ricardo Ren"/>
,由
<Anchor Address="https://dotnet.microsoft.com" Text="@DotnetVersion"/>
驱动。
<div class="row align-items-end text-center">
<div class="row">
<p class="fs-6">
2021 - @(DateTimeOffset.Now.Year) © <a href="https://rrricardo.top" target="_blank">Ricardo Ren</a>
由 <a href="https://dotnet.microsoft.com/zh-cn/" target="_blank">.NET @(Environment.Version)</a> 驱动。
</p>
</div>
<div>
<p class="text-md">
<a href="https://beian.miit.gov.cn" target="_blank" class="text-black">蜀ICP备2022004429号-1</a>
<div class="row">
<p class="fs-6">
<a href="https://beian.miit.gov.cn" target="_blank">蜀ICP备2022004429号-1</a>
</p>
</div>
</div>
@code
{
private string DotnetVersion => $".NET {Environment.Version}";
}

View File

View File

@@ -1,33 +1,35 @@
@using YaeBlog.Models
@inject BlogOptions Options
<div class="px-4 py-8 border border-sky-700 rounded-md bg-sky-200">
<div class="flex flex-col gap-3 text-md">
<div>
文章作者:<a href="https://rrricardo.top" target="_blank" class="text-blue-600">Ricardo Ren</a>
<div class="row px-2 py-4 copyright border border-primary rounded-1 bg-primary-subtle">
<div class="col">
<div class="row p-1">
<div class="col">
文章作者:<a href="https://rrricardo.top" target="_blank">Ricardo Ren</a>
</div>
</div>
<div>
<div class="row p-1">
<div class="col">
文章地址:
<a href="/blog/essays/@(EssayFilename)" target="_blank" class="text-blue-600">
@($"https://rrricardo.top/blog/essays/{EssayFilename}")
<a href="/blog/essays/@(EssayAddress)" target="_blank">
@($"https://rrricardo.top/blog/essays/{EssayAddress}")
</a>
</div>
</div>
<div>
<div class="row p-1">
<div class="col">
版权声明:本博客所有文章除特别声明外,均采用
<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank" class="text-blue-600">
CC BY-NC-SA 4.0
</a>
许可协议,诸位读者如有兴趣可任意转载,不必征询许可,但请注明“转载自
<a href="https://rrricardo.top/blog/" target="_blank" class="text-blue-600">
Ricardo's Blog
</a>”。
<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank">CC BY-NC-SA 4.0</a>
许可协议,转载请注明来自
<a href="https://rrricardo.top/blog/" target="_blank">Ricardo's Blog</a>
</div>
</div>
</div>
</div>
@code
{
[Parameter] public string? EssayFilename { get; set; }
[Parameter] public string? EssayAddress { get; set; }
}

View File

@@ -0,0 +1,2 @@
.copyright {
}

View File

@@ -1,22 +0,0 @@
@if (Selected)
{
<div class="border rounded-lg shadow-neutral-500 bg-sky-400 w-8 h-8 inline-block leading-8 text-center">
<span class="text-white">@(Text)</span>
</div>
}
else
{
<a href="@Address">
<div class="border rounded-lg shadow-neutral-500 w-8 h-8 inline-block leading-8 text-center">
<span>@(Text)</span>
</div>
</a>
}
@code {
[Parameter] public string? Address { get; set; }
[Parameter] public string? Text { get; set; }
[Parameter] public bool Selected { get; set; }
}

View File

@@ -1,46 +0,0 @@
<div class="flex flex-row justify-center gap-3">
@if (Page != 1)
{
<PageAnchor Address="@GenerateAddress(Page - 1)" Text="<"/>
}
@if (Page == 1)
{
<PageAnchor Address="@GenerateAddress(1)" Text="1" Selected="@true"/>
<PageAnchor Address="@GenerateAddress(2)" Text="2"/>
<PageAnchor Address="@GenerateAddress(3)" Text="3"/>
}
else if (Page == PageCount)
{
<PageAnchor Address="@GenerateAddress(PageCount - 2)" Text="@($"{PageCount - 2}")"/>
<PageAnchor Address="@GenerateAddress(PageCount - 1)" Text="@($"{PageCount - 1}")"/>
<PageAnchor Address="@GenerateAddress(PageCount)" Text="@($"{PageCount}")" Selected="@true"/>
}
else
{
<PageAnchor Address="@GenerateAddress(Page - 1)" Text="@($"{Page - 1}")"/>
<PageAnchor Address="@GenerateAddress(Page)" Text="@($"{Page}")" Selected="@true"/>
<PageAnchor Address="@GenerateAddress(Page + 1)" Text="@($"{Page + 1}")"/>
}
@if (Page != PageCount)
{
<PageAnchor Address="@GenerateAddress(Page + 1)" Text=">"/>
}
</div>
@code {
[Parameter] public string? BaseUrl { get; set; }
[Parameter] public int PageCount { get; set; }
[Parameter] public int Page { get; set; }
private string GenerateAddress(int page) => $"{BaseUrl}?page={page}";
}

View File

@@ -0,0 +1,12 @@
namespace YaeBlog.Core.Exceptions;
public sealed class ProcessInteropException : Exception
{
public ProcessInteropException(string message) : base(message)
{
}
public ProcessInteropException(string message, Exception innerException) : base(message, innerException)
{
}
}

View File

@@ -22,8 +22,9 @@ public static class WebApplicationBuilderExtensions
builder.Services.AddSingleton<RendererService>();
builder.Services.AddSingleton<IEssayContentService, EssayContentService>();
builder.Services.AddTransient<ImagePostRenderProcessor>();
builder.Services.AddTransient<CodeBlockPostRenderProcessor>();
builder.Services.AddTransient<TablePostRenderProcessor>();
builder.Services.AddTransient<HeadlinePostRenderProcessor>();
builder.Services.AddTransient<EssayStylesPostRenderProcessor>();
builder.Services.AddTransient<BlogOptions>(provider =>
provider.GetRequiredService<IOptions<BlogOptions>>().Value);
@@ -44,4 +45,13 @@ public static class WebApplicationBuilderExtensions
return builder;
}
public static WebApplicationBuilder AddTailwindWatcher(this WebApplicationBuilder builder)
{
builder.Services.AddSingleton<ProcessInteropService>();
builder.Services.Configure<TailwindOptions>(builder.Configuration.GetSection(TailwindOptions.OptionName));
builder.Services.AddHostedService<TailwindRefreshService>();
return builder;
}
}

View File

@@ -9,8 +9,9 @@ public static class WebApplicationExtensions
public static void UseYaeBlog(this WebApplication application)
{
application.UsePostRenderProcessor<ImagePostRenderProcessor>();
application.UsePostRenderProcessor<CodeBlockPostRenderProcessor>();
application.UsePostRenderProcessor<TablePostRenderProcessor>();
application.UsePostRenderProcessor<HeadlinePostRenderProcessor>();
application.UsePostRenderProcessor<EssayStylesPostRenderProcessor>();
}
private static void UsePreRenderProcessor<T>(this WebApplication application) where T : IPreRenderProcessor

View File

@@ -2,41 +2,41 @@
@attribute [StreamRendering]
<main class="container mx-auto flex flex-col min-h-screen">
<main class="container mx-auto">
<div class="grid grid-cols-3 mx-3">
<div class="md:col-span-2 col-span-3 h-20 flex items-center">
<div class="md:col-span-2 col-span-3 my-6">
<a href="/blog/">
<span class="text-blue-600 text-2xl">Ricardo's Blog</span>
</a>
</div>
<div class="md:col-span-1 col-span-3 h-20 flex items-center">
<div class="flex flex-row w-full px-2 gap-3 md:justify-center justify-end">
<div>
<div class="md:col-span-1 col-span-3 my-6">
<div class="flex flex-row">
<div class="px-2 ">
<a href="/blog/">
<span class="text-xl text-blue-600">首页</span>
</a>
</div>
<div class="px-2 ">
<a href="/blog/archives/">
<span class="text-xl text-blue-600">归档</span>
</a>
</div>
<div>
<div class="px-2 ">
<a href="/blog/tags/">
<span class="text-xl text-blue-600">标签</span>
</a>
</div>
<div>
<a href="/about/" target="_blank">
<div class="px-2 ">
<a href="/blog/about/">
<span class="text-xl text-blue-600">关于</span>
</a>
</div>
<div>
<a href="/friends/" target="_blank">
<span class="text-xl text-blue-600">友链</span>
</a>
</div>
</div>
</div>
</div>
<div class="px-4 py-2 flex-grow">
<div class="px-4 py-2">
@Body
</div>

View File

View File

@@ -1,34 +1,22 @@
@inherits LayoutComponentBase
<main class="container mx-auto min-h-screen flex flex-col">
<div class="grid grid-cols-4">
<div class="px-2 md:col-span-3 col-span-4 h-20 flex items-center">
<a href="/" class="text-2xl">
<main class="container mx-auto">
<div class="flex flex-row h-20">
<div class="px-2 basis-3/4">
<a href="/" class="text-2xl p-2">
<h4 class="text-blue-600">Ricardo's Index</h4>
</a>
<h1></h1>
</div>
<div class="md:col-span-1 col-span-4 h-20 flex items-center">
<div class="flex flex-row w-full px-2 md:justify-center justify-end text-xl gap-3">
<Anchor
Address="/blog/"
Text="博客"
NewPage="@(true)"/>
<Anchor
Address="/about/"
Text="关于"
NewPage="@(true)"/>
<Anchor
Address="/friends"
Text="友链"
NewPage="@(true)"/>
</div>
<div class="basis-1/4">
<a href="mailto://shicangjuner@outlook.com" class="p-2 text-xl" target="_blank">
<h5 class="text-blue-600">E-mail</h5>
</a>
</div>
</div>
<div class="px-4 mx-auto flex-grow">
<div class="px-4 mx-auto">
<div class="py-2">
@Body
</div>

View File

@@ -0,0 +1,8 @@
.center {
margin: 0 auto;
max-width: 48em;
min-height: calc(100vh - 80px);
position: relative;
display: flex;
flex-direction: column;
}

View File

@@ -0,0 +1,10 @@
namespace YaeBlog.Models;
public class AboutInfo
{
public required string Introduction { get; set; }
public required string Description { get; set; }
public required string AvatarImage { get; set; }
}

View File

@@ -7,6 +7,4 @@ public class BlogContent
public required MarkdownMetadata Metadata { get; init; }
public required string FileContent { get; set; }
public bool IsDraft { get; set; } = false;
}

View File

@@ -6,8 +6,6 @@ public class BlogEssay : IComparable<BlogEssay>
public required string FileName { get; init; }
public required bool IsDraft { get; init; }
public required DateTime PublishTime { get; init; }
public required string Description { get; init; }
@@ -26,7 +24,6 @@ public class BlogEssay : IComparable<BlogEssay>
{
Title = Title,
FileName = FileName,
IsDraft = IsDraft,
PublishTime = PublishTime,
Description = Description,
WordCount = WordCount,
@@ -42,16 +39,10 @@ public class BlogEssay : IComparable<BlogEssay>
{
if (other is null)
{
return -1;
return 1;
}
// 草稿文章应当排在前面
if (IsDraft != other.IsDraft)
{
return IsDraft ? -1 : 1;
}
return other.PublishTime.CompareTo(PublishTime);
return PublishTime.CompareTo(other.PublishTime);
}
public override string ToString()

View File

@@ -0,0 +1,10 @@
namespace YaeBlog.Models;
public class TailwindOptions
{
public const string OptionName = "Tailwind";
public required string InputFile { get; set; }
public required string OutputFile { get; set; }
}

View File

@@ -1,74 +1,141 @@
@page "/about"
@page "/blog/about"
@using YaeBlog.Models
@inject BlogOptions Options
<PageTitle>
关于
</PageTitle>
<div class="flex flex-col">
<div>
<h1 class="text-4xl">关于</h1>
<div class="container">
<div class="row">
<div class="col">
<h1>关于</h1>
</div>
</div>
<div class="py-4">
<span class="italic">把字刻在石头上!(・’ω’・)</span>
<div class="row">
<div class="col fst-italic py-2">
把字刻在石头上!(・’ω’・)
</div>
</div>
<div class="flex flex-col p-2">
<div class="flex flex-col p-2">
<div class="pb-2">
<h3 class="text-2xl">关于我</h3>
<div class="row p-2">
<div class="col">
<div class="row">
<div class="col">
<h3>关于我</h3>
</div>
</div>
<div class="py-2">
<div class="row py-2">
<div class="col">
计算机科学与技术在读大学生,明光村幼儿园附属大学所属。正处于读书和失业的叠加态。
一般在互联网上使用<span class="italic">初冬的朝阳</span>或者<span class="italic">jackfiled</span>的名字活动。
<span class="line-through">都是ICP备案过的人了网名似乎没有太大的用处</span>
一般在互联网上使用<span class="fst-italic">初冬的朝阳</span>或者<span class="fst-italic">jackfiled</span>的名字活动。
<span class="text-decoration-line-through">都是ICP备案过的人了网名似乎没有太大的用处</span>
</div>
</div>
<div class="py-2">
<div class="row py-2">
<div class="col">
主要是一个C#程序员目前也在尝试写一点Rust。
总体上对于编程语言的态度是“<span>大家都是我的翅膀.jpg</span>”。
前后端分离的项目本当上手。
常常因为现实的压力而写一些C/C++。
<span class="line-through">对于Java和Go的评价很低。</span>
<span class="text-decoration-line-through">对于Java和Go的评价很低。</span>
日常使用ArchLinux。
</div>
<div class="py-2">
100%社恐。日常生活是宅在电脑前面自言自语。
兴趣活动是读书和看番,目前在玩原神和三角洲。
</div>
<div class="py-4">
<div class="row py-2">
<div class="col">
100%社恐。日常生活是宅在电脑前面自言自语。兴趣活动是读书和看番。
</div>
</div>
<div class="row py-2">
<div class="col">
常常被人批评没有梦想,这里就随便瞎编一下。
成为嵌入式工程师,修好桌面上的<a href="https://www.bilibili.com/video/BV1VA411p7MD">HoloCubic</a>。
完成第一个不是课程设计的个人开源项目。
遇到能够搭伙过日子的人也算是一大梦想,虽然社恐人根本不知道从何开始的说,
<span class="line-through">什么时候天上才能掉美少女?</span>
<span class="text-decoration-line-through">什么时候天上才能掉美少女?</span>
</div>
</div>
<div class="py-2">
<div class="row py-2">
<div class="col">
公开的联系渠道是<a href="mailto:shicangjuner@outlook.com">电子邮件</a>。
也可以试试在各大平台搜索上面提到的名字。
</div>
</div>
<div class="flex flex-col p-2">
<div class="pb-2">
<h3 class="text-2xl">关于本站</h3>
</div>
</div>
<div class="py-2">
<div class="row p-2">
<div class="col">
<div class="row">
<div class="col">
<h3>关于本站</h3>
</div>
</div>
<div class="row py-2">
<div class="col">
本站肇始于2021年下半年在开始的两年中个人网站和博客是分别的两个网站个人网站是裸HTML写的博客是用
<a href="https://hexo.io">Hexo</a>渲染的。
</div>
<div class="py-2">
2024年我们决定使用.NET技术完全重构两个网站合二为一。虽然目前这个版本还是一个半成品但是我们一定会努力的~(确信。
</div>
<div class="py-2">
2025年我们将使用的样式库从Bootstrap迁移到Tailwind CSS将现代的前端技术同Blazor结合起来。
<div class="row py-2">
<div class="col">
2024年我们决定使用.NET技术完全重构两个网站合二为一。虽然目前这个版本还是一个半成品但是我们一定会努力的~(确信。
</div>
</div>
</div>
</div>
<div class="row p-2">
<div class="col">
<div class="row">
<div class="col">
<h3>友链</h3>
</div>
</div>
<div class="row py-2">
<div class="col fst-italic">
欢迎所有人联系我添加友链!(´。✪ω✪。`)
</div>
</div>
<div class="row py-2">
@foreach (FriendLink link in Options.Links)
{
<div class="col-sm-12 col-md-4 col-lg-3">
<a href="@(link.Link)" target="_blank" class="m-3">
<div class="row link-item">
<div class="col-4">
@* <Image Src="@(link.AvatarImage)" Alt="@(link.Name)" Style="border-radius: 50%"/> *@
</div>
<div class="col-8">
<div class="row">
<div class="col-auto fs-5">
@(link.Name)
</div>
</div>
<div class="row">
<div class="col-auto fst-italic">
@(link.Description)
</div>
</div>
</div>
</div>
</a>
</div>
}
</div>
</div>
</div>

View File

@@ -0,0 +1,8 @@
.link-item {
padding: 1rem;
border-radius: 4px;
}
.link-item:hover {
background-color: var(--bs-secondary-bg);
}

View File

@@ -8,56 +8,68 @@
归档
</PageTitle>
<div class="flex flex-col">
<div>
<h1 class="text-4xl">归档</h1>
<div class="container">
<div class="row">
<div class="col">
<div class="container">
<div class="row">
<div class="col">
<h1>归档</h1>
</div>
</div>
<div class="py-4">
<span class="italic">
<div class="row">
<div class="col fst-italic py-4">
时光图书馆,黑历史集散地。(๑◔‿◔๑)
</span>
</div>
</div>
</div>
</div>
</div>
@foreach (IGrouping<DateTime, BlogEssay> group in _essays)
@foreach (IGrouping<DateTime, KeyValuePair<string, BlogEssay>> group in _essays)
{
<div class="p-2">
<div class="flex flex-col">
<div>
<h3 class="text-xl">@(group.Key.Year)</h3>
<div class="row">
<div class="col">
<div class="container">
<div class="row">
<div class="col">
<h3>@(group.Key.Year)</h3>
</div>
</div>
<div class="px-4 py-4 flex flex-col">
@foreach (BlogEssay essay in group)
<div class="container px-3 py-2">
@foreach (KeyValuePair<string, BlogEssay> essay in group)
{
<a target="_blank" href="@($"/blog/essays/{essay.FileName}")">
<div class="flex flex-row p-2 mx-1 rounded-lg hover:bg-gray-300">
<div class="w-20">
@(essay.PublishTime.ToString("MM月dd日"))
<div class="row py-1">
<div class="col-auto">
@(essay.Value.PublishTime.ToString("MM-dd"))
</div>
<div>
<span class="text-blue-600">
@(essay.Title)
</span>
</div>
</div>
<div class="col-auto">
<a href="/blog/essays/@(essay.Key)">
@(essay.Value.Title)
</a>
</div>
</div>
}
</div>
</div>
</div>
</div>
}
</div>
@code {
private readonly List<IGrouping<DateTime, BlogEssay>> _essays = [];
private readonly List<IGrouping<DateTime, KeyValuePair<string, BlogEssay>>> _essays = [];
protected override void OnInitialized()
{
base.OnInitialized();
_essays.AddRange(from essay in Contents.Essays
group essay by new DateTime(essay.PublishTime.Year, 1, 1));
orderby essay.Value.PublishTime descending
group essay by new DateTime(essay.Value.PublishTime.Year, 1, 1));
}
}

View File

View File

@@ -9,18 +9,79 @@
Ricardo's Blog
</PageTitle>
<div>
<div class="grid grid-cols-4">
<div class="col-span-4 md:col-span-3">
@foreach (BlogEssay essay in _essays)
<div class="container">
<div class="row">
<div class="col-sm-12 col-md-9">
@foreach (KeyValuePair<string, BlogEssay> pair in _essays)
{
<EssayCard Essay="@(essay)"/>
<EssayCard Essay="@(pair.Value)"/>
}
<Pagination BaseUrl="/blog/" Page="_page" PageCount="_pageCount"/>
<div class="row align-items-center justify-content-center p-3">
@if (_page == 1)
{
<div class="col-auto fw-light">上一页</div>
}
else
{
<div class="col-auto">
<a href="/blog/?page=@(_page - 1)">上一页</a>
</div>
}
@if (_page == 1)
{
<div class="col-auto">
1
</div>
<div class="col-auto">
<a href="/blog/?page=2">2</a>
</div>
<div class="col-auto">
<a href="/blog/?page=3">3</a>
</div>
}
else if (_page == _pageCount)
{
<div class="col-auto">
<a href="/blog/?page=@(_pageCount - 2)">@(_pageCount - 2)</a>
</div>
<div class="col-auto">
<a href="/blog/?page=@(_pageCount - 1)">@(_pageCount - 1)</a>
</div>
<div class="col-auto">
@(_pageCount)
</div>
}
else
{
<div class="col-auto">
<a href="/blog/?page=@(_page - 1)">@(_page - 1)</a>
</div>
<div class="col-auto">
@(_page)
</div>
<div class="col-auto">
<a href="/blog/?page=@(_page + 1)">@(_page + 1)</a>
</div>
}
@if (_page == _pageCount)
{
<div class="col-auto fw-light">
下一页
</div>
}
else
{
<div class="col-auto">
<a href="/blog/?page=@(_page + 1)">下一页</a>
</div>
}
</div>
</div>
<div class="col-span-4 md:col-span-1">
<div class="col-sm-12 col-md-3">
<BlogInformationCard/>
</div>
</div>
@@ -30,7 +91,7 @@
[SupplyParameterFromQuery] private int? Page { get; set; }
private readonly List<BlogEssay> _essays = [];
private readonly List<KeyValuePair<string, BlogEssay>> _essays = [];
private const int EssaysPerPage = 8;
private int _pageCount = 1;
private int _page = 1;
@@ -38,15 +99,16 @@
protected override void OnInitialized()
{
_page = Page ?? 1;
_pageCount = Contents.Count / EssaysPerPage + 1;
_pageCount = Contents.Essays.Count / EssaysPerPage + 1;
if (EssaysPerPage * _page > Contents.Count + EssaysPerPage)
if (EssaysPerPage * _page > Contents.Essays.Count + EssaysPerPage)
{
NavigationInstance.NavigateTo("/NotFount");
return;
}
_essays.AddRange(Contents.Essays
.OrderByDescending(p => p.Value.PublishTime)
.Skip((_page - 1) * EssaysPerPage)
.Take(EssaysPerPage));
}

View File

@@ -0,0 +1,7 @@
.essay-title a {
color: var(--bs-body-color);
}
.read-more a {
color: var(--bs-body-color);
}

View File

@@ -10,78 +10,86 @@
@(_essay!.Title)
</PageTitle>
<div class="flex flex-col py-8">
<div>
<h1 id="title" class="text-4xl">@(_essay!.Title)</h1>
<div class="container py-4">
<div class="row">
<div class="col-auto">
<h1 id="title">@(_essay!.Title)</h1>
</div>
</div>
<div class="px-6 pt-4 pb-2">
<div class="flex flex-row gap-4">
<div class="font-light">
<div class="row px-4 py-1">
<div class="col-auto fw-light">
@(_essay!.PublishTime.ToString("yyyy-MM-dd"))
</div>
@foreach (string tag in _essay!.Tags)
{
<div class="text-sky-500">
<div class="col-auto">
<a href="/blog/tags/?tagName=@(UrlEncoder.Default.Encode(tag))">
# @(tag)
</a>
</div>
}
</div>
</div>
<div class="px-6 pt-2 pb-4">
<div class="font-light">
<div class="row px-4 py-1">
<div class="col-auto fw-light">
总字数:@(_essay!.WordCount)字,预计阅读时间 @(_essay!.ReadTime)。
</div>
</div>
<div class="grid grid-cols-3">
<div class="col-span-3 md:col-span-2 flex flex-col gap-3">
<div>
<div class="row">
<div class="col-lg-8 col-md-12">
@((MarkupString)_essay!.HtmlContent)
<LicenseDisclaimer EssayAddress="@BlogKey"/>
</div>
<div>
<LicenseDisclaimer EssayFilename="@BlogKey"/>
<div class="col-lg-4 col-md-12">
<div class="row sticky-lg-top justify-content-center">
<div class="col-auto">
<div class="row">
<div class="col-auto">
<h3 style="margin-block-start: 1em; margin-block-end: 0.5em">
文章目录
</h3>
</div>
</div>
<div class="col-span-3 md:col-span-1">
<div class="flex flex-col sticky top-0 px-8">
<div>
<h3 class="text-2xl">文章目录</h3>
</div>
<div>
<div class="row" style="padding-left: 10px">
<div class="col-auto">
@foreach (BlogHeadline level2 in _headline!.Children)
{
<div class="py-2 pl-3">
<Anchor Address="@(GenerateSelectorUrl(level2.SelectorId))"
Text="@(level2.Title)"/>
<div class="row py-1">
<div class="col-auto">
<a href="@(GenerateSelectorUrl(level2.SelectorId))">@(level2.Title)</a>
</div>
</div>
@foreach (BlogHeadline level3 in level2.Children)
{
<div class="py-2 pl-6">
<Anchor Address="@(GenerateSelectorUrl(level3.SelectorId))"
Text="@(level3.Title)"/>
<div class="row py-1">
<div class="col-auto">
<a style="padding-left: 20px" href="@GenerateSelectorUrl(level3.SelectorId)">
@(level3.Title)
</a>
</div>
</div>
@foreach (BlogHeadline level4 in level3.Children)
{
<div class="py-2 pl-9">
<Anchor Address="@(GenerateSelectorUrl(level4.SelectorId))"
Text="@(level4.Title)"/>
<div class="row py-1">
<div class="col-auto">
<a style="padding-left: 40px" href="@(GenerateSelectorUrl(level4.SelectorId))">
@(level4.Title)
</a>
</div>
</div>
}
}
}
</div>
</div>
@if (_headline!.Children.Count == 0)
{
@@ -94,6 +102,7 @@
</div>
</div>
</div>
</div>
</div>
@@ -114,7 +123,7 @@
return;
}
if (!Contents.TryGetEssay(BlogKey, out _essay))
if (!Contents.Essays.TryGetValue(BlogKey, out _essay))
{
NavigationInstance.NavigateTo("/NotFound");
}

View File

View File

@@ -1,49 +0,0 @@
@page "/friends"
@using YaeBlog.Models
@inject BlogOptions Options
<PageTitle>
友链
</PageTitle>
<div class="flex flex-col">
<div>
<h1 class="text-4xl">
友链
</h1>
</div>
<div class="py-4">
欢迎所有人联系我添加友链!(´。✪ω✪。`)
</div>
<div class="grid grid-cols-4 g-4 p-2">
@foreach (FriendLink link in Options.Links)
{
<div>
<a href="@(link.Link)" target="_blank" class="mx-5">
<div class="flex flex-row">
<div class="basis-1/3">
<img src="@(link.AvatarImage)" alt="@($"Avatar of {link.Name}")"
class="w-full h-auto rounded-full">
</div>
<div class="flex flex-col basis-2/3 px-2">
<div class="text-lg">
@(link.Name)
</div>
<div class="text-sm italic">
@(link.Description)
</div>
</div>
</div>
</a>
</div>
}
</div>
</div>
@code {
}

View File

@@ -4,28 +4,28 @@
Ricardo's Index
</PageTitle>
<div class="mx-20">
<div class="grid grid-cols-3 py-4">
<div class="col-span-3 md:col-span-1 p-5 p-lg-0">
<img src="images/avatar.png" alt="Ricardo's Avatar" class="h-auto max-w-full">
<div class="container">
<div class="row py-4">
<div class="col-lg-4 col-12 p-5 p-lg-0">
@* <Image Src="images/avatar.png" Alt="Ricardo's Avatar"/> *@
</div>
<div class="col-span-3 md:col-span-2">
<div class="flex flex-col px-3 gap-y-3">
<div class="">
<div class="text-3xl font-bold">初冬的朝阳 (Ricardo Ren)</div>
<div class="col-lg-8 col-12">
<div class="container px-3">
<div class="row">
<h4 class="fw-bold">初冬的朝阳 (Ricardo Ren)</h4>
</div>
<div class="">
<p class="text-lg">a.k.a jackfiled</p>
<div class="row">
<p class="fs-5">a.k.a jackfiled</p>
</div>
<div class="">
<p class="text-lg italic">世界很大,时间很长。</p>
<div class="row">
<p class="fs-5 fst-italic">世界很大,时间很长。</p>
</div>
<div class="">
<p class="text-lg">
<div class="row">
<p class="fs-5">
平平无奇的计算机科学与技术学徒,连微小的贡献都没做。
</p>
</div>
@@ -33,22 +33,20 @@
</div>
</div>
<div class="py-5">
<p class="text-lg">恕我不能亲自为您沏茶(?),还是非常欢迎您能够来到我的主页。</p>
<div class="row" style="padding-top: 80px">
<p class="fs-5">恕我不能亲自为您沏茶(?),还是非常欢迎您能够来到我的主页。</p>
</div>
<div>
<p class="text-lg py-1">
如果您想四处看看,了解一下屏幕对面的人,可以在我的 <Anchor Address="/blog/" Text="博客"/> 看看。
<div class="row">
<p class="fs-5">
如果您想四处看看,了解一下屏幕对面的人,可以在我的 <a href="/blog/">博客</a> 看看。
如果您对于明光村幼儿园某附属技校的计算机教学感兴趣,您可以移步到
<Anchor Address="https://jackfiled.github.io/wiki/" Text="我的学习笔记"/>
<a href="https://jackfiled.github.io/wiki/">我的学习笔记</a>
<span class="fs-5 text-decoration-line-through">虽然这笔记我自己也木有看过。</span>
如果您想批判一下我的代码,在
<Anchor Address="https://github.com/jackfiled/" Text="Github"/> 和
<Anchor Address="https://git.rrricardo.top/jackfiled/" Text="Gitea"/>
都可以找到。
如果您想批判一下我的代码,在 <a href="https://github.com/jackfiled" target="_blank">Github</a> 和
<a href="https://git.rrricardo.top/jackfiled/" target="_blank">Gitea</a> 都可以找到。
</p>
<p class="text-lg py-1">
<p class="fs-5">
如果您真的很闲,也可以四处搜寻一下,也许存在着一些不为人知的彩蛋。
</p>
</div>

View File

View File

@@ -4,8 +4,8 @@
啊~ 页面走丢啦~
</PageTitle>
<div>
<h3 class="text-3xl">NotFound!</h3>
<div class="container">
<h3>NotFound!</h3>
</div>
@code {

View File

View File

@@ -10,22 +10,24 @@
@(TagName ?? "标签")
</PageTitle>
<div class="flex flex-col">
<div>
<div class="container">
<div class="row">
<div class="col">
@if (TagName is null)
{
<h1 class="text-4xl">标签</h1>
<h1>标签</h1>
}
else
{
<h2 class="text-2xl">@(TagName)</h2>
<h2>@(TagName)</h2>
}
</div>
</div>
<div class="py-4">
<span class="italic">
<div class="row">
<div class="col fst-italic py-4">
在野外游荡的指针,走向未知的方向。٩(๑˃̵ᴗ˂̵๑)۶
</span>
</div>
</div>
@if (TagName is null)
@@ -36,17 +38,19 @@
Contents.Tags.OrderByDescending(pair => pair.Value.Count))
{
<li class="p-2">
<div class="flex flex-row">
<a href="/blog/tags/?tagName=@(pair.Key.UrlEncodedTagName)">
<div class="text-sky-600 text-lg">
<div class="container fs-5">
<div class="row">
<div class="col-auto">
# @(pair.Key.TagName)
</div>
</a>
<div class="mx-2 px-1 text-lg bg-gray-300 rounded-lg">
<div class="col-auto tag-count">
@(pair.Value.Count)
</div>
</div>
</div>
</a>
</li>
}
</ul>

View File

@@ -0,0 +1,6 @@
.tag-count {
background: var(--bs-secondary-bg);
border-radius: 5px;
padding: 0 6px;
}

View File

@@ -0,0 +1,29 @@
using AngleSharp;
using AngleSharp.Dom;
using YaeBlog.Abstraction;
using YaeBlog.Models;
namespace YaeBlog.Processors;
public class CodeBlockPostRenderProcessor : IPostRenderProcessor
{
public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
{
BrowsingContext context = new(Configuration.Default);
IDocument document = await context.OpenAsync(
req => req.Content(essay.HtmlContent));
IEnumerable<IElement> preElements = from e in document.All
where e.LocalName == "pre"
select e;
foreach (IElement element in preElements)
{
element.ClassList.Add("p-3 text-bg-secondary rounded-1");
}
return essay.WithNewHtmlContent(document.DocumentElement.OuterHtml);
}
public string Name => nameof(CodeBlockPostRenderProcessor);
}

View File

@@ -1,102 +0,0 @@
using AngleSharp;
using AngleSharp.Dom;
using YaeBlog.Abstraction;
using YaeBlog.Models;
namespace YaeBlog.Processors;
/// <summary>
/// 向渲染的HTML中插入Tailwind CSS的渲染后处理器
/// </summary>
public sealed class EssayStylesPostRenderProcessor : IPostRenderProcessor
{
public string Name => nameof(EssayStylesPostRenderProcessor);
public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
{
BrowsingContext context = new(Configuration.Default);
IDocument document = await context.OpenAsync(
req => req.Content(essay.HtmlContent));
ApplyGlobalCssStyles(document);
BeatifyTable(document);
return essay.WithNewHtmlContent(document.DocumentElement.OuterHtml);
}
private readonly Dictionary<string, string> _globalCssStyles = new()
{
{ "pre", "p-4 bg-slate-300 rounded-sm overflow-x-auto" },
{ "h2", "text-3xl font-bold py-4" },
{ "h3", "text-2xl font-bold py-3" },
{ "h4", "text-xl font-bold py-2" },
{ "h5", "text-lg font-bold py-1" },
{ "p", "p-2" },
{ "img", "w-11/12 block mx-auto my-2 rounded-md shadow-md" },
{ "ul", "list-disc pl-2" }
};
private void ApplyGlobalCssStyles(IDocument document)
{
foreach ((string tag, string style) in _globalCssStyles)
{
foreach (IElement element in document.GetElementsByTagName(tag))
{
element.ClassList.Add(style);
}
}
}
private static void BeatifyTable(IDocument document)
{
foreach (IElement element in from e in document.All
where e.LocalName == "table"
select e)
{
element.ClassList.Add("mx-auto border-collapse table-auto overflow-x-auto");
// thead元素
foreach (IElement headElement in from e in element.Children
where e.LocalName == "thead"
select e)
{
headElement.ClassList.Add("bg-slate-200");
// tr in thead
foreach (IElement trElement in from e in headElement.Children
where e.LocalName == "tr"
select e)
{
trElement.ClassList.Add("border border-slate-300");
// th in tr
foreach (IElement thElement in from e in trElement.Children
where e.LocalName == "th"
select e)
{
thElement.ClassList.Add("px-4 py-1");
}
}
}
// tbody元素
foreach (IElement bodyElement in from e in element.Children
where e.LocalName == "tbody"
select e)
{
// tr in tbody
foreach (IElement trElement in from e in bodyElement.Children
where e.LocalName == "tr"
select e)
{
foreach (IElement tdElement in from e in trElement.Children
where e.LocalName == "td"
select e)
{
tdElement.ClassList.Add("px-4 py-1 border border-slate-300");
}
}
}
}
}
}

View File

@@ -11,11 +11,13 @@ public class ImagePostRenderProcessor(ILogger<ImagePostRenderProcessor> logger,
IOptions<BlogOptions> options)
: IPostRenderProcessor
{
private static readonly AngleSharp.IConfiguration s_configuration = Configuration.Default;
private readonly BlogOptions _options = options.Value;
public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
{
BrowsingContext context = new(Configuration.Default);
BrowsingContext context = new(s_configuration);
IDocument html = await context.OpenAsync(
req => req.Content(essay.HtmlContent));
@@ -31,6 +33,7 @@ public class ImagePostRenderProcessor(ILogger<ImagePostRenderProcessor> logger,
logger.LogDebug("Found image link: '{}'", attr.Value);
attr.Value = GenerateImageLink(attr.Value, essay.FileName);
}
element.ClassList.Add("essay-image");
}
return essay.WithNewHtmlContent(html.DocumentElement.OuterHtml);
}

View File

@@ -0,0 +1,34 @@
using AngleSharp;
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using YaeBlog.Abstraction;
using YaeBlog.Models;
namespace YaeBlog.Processors;
public class TablePostRenderProcessor: IPostRenderProcessor
{
public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
{
BrowsingContext browsingContext = new(Configuration.Default);
IDocument document = await browsingContext.OpenAsync(
req => req.Content(essay.HtmlContent));
IEnumerable<IHtmlTableElement> tableElements = from item in document.All
where item.LocalName == "table"
select item as IHtmlTableElement;
foreach (IHtmlTableElement element in tableElements)
{
IHtmlDivElement divElement = document.CreateElement<IHtmlDivElement>();
divElement.InnerHtml = element.OuterHtml;
divElement.ClassList.Add("py-2", "table-wrapper");
element.Replace(divElement);
}
return essay.WithNewHtmlContent(document.DocumentElement.OuterHtml);
}
public string Name => nameof(TablePostRenderProcessor);
}

View File

@@ -6,12 +6,14 @@ public class BlogHostedService(
{
public async Task StartAsync(CancellationToken cancellationToken)
{
logger.LogInformation("Failed to load cache, render essays.");
logger.LogInformation("Welcome to YaeBlog!");
await rendererService.RenderAsync();
}
public Task StopAsync(CancellationToken cancellationToken)
{
logger.LogInformation("YaeBlog stopped!\nHave a nice day!");
return Task.CompletedTask;
}
}

View File

@@ -10,32 +10,24 @@ public sealed class BlogHotReloadService(
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation("Hot reload is starting...");
logger.LogInformation("Change essays will lead to hot reload!");
logger.LogInformation("HINT: draft essays will be included.");
logger.LogInformation("BlogHotReloadService is starting.");
await rendererService.RenderAsync(true);
await rendererService.RenderAsync();
Task[] reloadTasks = [FileWatchTask(stoppingToken)];
await Task.WhenAll(reloadTasks);
}
private async Task FileWatchTask(CancellationToken token)
while (!stoppingToken.IsCancellationRequested)
{
while (!token.IsCancellationRequested)
{
logger.LogInformation("Watching file changes...");
string? changeFile = await watcher.WaitForChange(token);
logger.LogDebug("Watching file changes...");
string? changFile = await watcher.WaitForChange(stoppingToken);
if (changeFile is null)
if (changFile is null)
{
logger.LogInformation("File watcher is stopping.");
logger.LogInformation("BlogHotReloadService is stopping.");
break;
}
logger.LogInformation("{} changed, re-rendering.", changeFile);
logger.LogInformation("{} changed, re-rendering.", changFile);
essayContentService.Clear();
await rendererService.RenderAsync(true);
await rendererService.RenderAsync();
}
}
}

View File

@@ -9,28 +9,15 @@ public class EssayContentService : IEssayContentService
{
private readonly ConcurrentDictionary<string, BlogEssay> _essays = new();
private readonly List<BlogEssay> _sortedEssays = [];
private readonly Dictionary<EssayTag, List<BlogEssay>> _tags = [];
private readonly ConcurrentDictionary<string, BlogHeadline> _headlines = new();
public bool TryAdd(BlogEssay essay)
{
_sortedEssays.Add(essay);
return _essays.TryAdd(essay.FileName, essay);
}
public bool TryAdd(BlogEssay essay) => _essays.TryAdd(essay.FileName, essay);
public bool TryAddHeadline(string filename, BlogHeadline headline) => _headlines.TryAdd(filename, headline);
public IEnumerable<BlogEssay> Essays => _sortedEssays;
public int Count => _sortedEssays.Count;
public bool TryGetEssay(string filename, [NotNullWhen(true)] out BlogEssay? essay)
{
return _essays.TryGetValue(filename, out essay);
}
public IReadOnlyDictionary<string, BlogEssay> Essays => _essays;
public IReadOnlyDictionary<EssayTag, List<BlogEssay>> Tags => _tags;

View File

@@ -22,8 +22,8 @@ public partial class EssayScanService(
ValidateDirectory(_blogOptions.Root, out DirectoryInfo drafts, out DirectoryInfo posts);
return new BlogContents(
await ScanContentsInternal(drafts, true),
await ScanContentsInternal(posts, false));
await ScanContentsInternal(drafts),
await ScanContentsInternal(posts));
}
public async Task SaveBlogContent(BlogContent content, bool isDraft = true)
@@ -60,7 +60,7 @@ public partial class EssayScanService(
}
}
private async Task<ConcurrentBag<BlogContent>> ScanContentsInternal(DirectoryInfo directory, bool isDraft)
private async Task<ConcurrentBag<BlogContent>> ScanContentsInternal(DirectoryInfo directory)
{
// 扫描以md结果的但是不是隐藏文件的文件
IEnumerable<FileInfo> markdownFiles = from file in directory.EnumerateFiles()
@@ -97,8 +97,7 @@ public partial class EssayScanService(
contents.Add(new BlogContent
{
FileName = filename[..^3], Metadata = metadata, FileContent = content[(endPos + 3)..],
IsDraft = isDraft
FileName = filename[..^3], Metadata = metadata, FileContent = content[(endPos + 3)..]
});
}
catch (YamlException e)

View File

@@ -0,0 +1,65 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using YaeBlog.Core.Exceptions;
namespace YaeBlog.Services;
public class ProcessInteropService(ILogger<ProcessInteropService> logger)
{
public Process StartProcess(string command, string arguments)
{
string commandName;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !command.EndsWith(".exe"))
{
commandName = command + ".exe";
}
else
{
commandName = command;
}
try
{
ProcessStartInfo startInfo = new()
{
FileName = commandName,
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
Process? process = Process.Start(startInfo);
if (process is null)
{
throw new ProcessInteropException(
$"Failed to start process: {commandName}, the return process is null.");
}
process.OutputDataReceived += (_, data) =>
{
if (!string.IsNullOrEmpty(data.Data))
{
logger.LogInformation("Receive output from process '{}': '{}'", commandName, data.Data);
}
};
process.ErrorDataReceived += (_, data) =>
{
if (!string.IsNullOrEmpty(data.Data))
{
logger.LogWarning("Receive error from process '{}': '{}'", commandName, data.Data);
}
};
return process;
}
catch (Exception innerException)
{
throw new ProcessInteropException($"Failed to start process '{command}' with arguments '{arguments}",
innerException);
}
}
}

View File

@@ -21,21 +21,18 @@ public partial class RendererService(
private readonly List<IPostRenderProcessor> _postRenderProcessors = [];
public async Task RenderAsync(bool includeDrafts = false)
public async Task RenderAsync()
{
_stopwatch.Start();
logger.LogInformation("Render essays start.");
BlogContents contents = await essayScanService.ScanContents();
List<BlogContent> posts = contents.Posts.ToList();
if (includeDrafts)
{
posts.AddRange(contents.Drafts);
}
IEnumerable<BlogContent> preProcessedContents = await PreProcess(posts);
List<BlogEssay> essays = [];
await Task.Run(() =>
{
foreach (BlogContent content in preProcessedContents)
{
uint wordCount = GetWordCount(content);
@@ -43,7 +40,6 @@ public partial class RendererService(
{
Title = content.Metadata.Title ?? content.FileName,
FileName = content.FileName,
IsDraft = content.IsDraft,
Description = GetDescription(content),
WordCount = wordCount,
ReadTime = CalculateReadTime(wordCount),
@@ -58,6 +54,7 @@ public partial class RendererService(
essays.Add(essay);
}
});
ConcurrentBag<BlogEssay> postProcessEssays = [];
Parallel.ForEach(essays, essay =>
@@ -69,16 +66,7 @@ public partial class RendererService(
logger.LogDebug("Render markdown file {}.", newEssay);
});
IEnumerable<BlogEssay> postProcessedEssays = await PostProcess(postProcessEssays);
foreach (BlogEssay essay in postProcessedEssays)
{
if (!essayContentService.TryAdd(essay))
{
throw new BlogFileException($"There are at least two essays with filename '{essay.FileName}'.");
}
}
await PostProcess(postProcessEssays);
essayContentService.RefreshTags();
_stopwatch.Stop();
@@ -129,10 +117,8 @@ public partial class RendererService(
return processedContents;
}
private async Task<IEnumerable<BlogEssay>> PostProcess(IEnumerable<BlogEssay> essays)
private async Task PostProcess(IEnumerable<BlogEssay> essays)
{
ConcurrentBag<BlogEssay> processedContents = [];
await Parallel.ForEachAsync(essays, async (essay, _) =>
{
foreach (IPostRenderProcessor processor in _postRenderProcessors)
@@ -140,13 +126,12 @@ public partial class RendererService(
essay = await processor.ProcessAsync(essay);
}
processedContents.Add(essay);
if (!essayContentService.TryAdd(essay))
{
throw new BlogFileException(
$"There are two essays with the same name: '{essay.FileName}'.");
}
});
List<BlogEssay> result = processedContents.ToList();
result.Sort();
return result;
}
[GeneratedRegex(@"(?<!\\)[^\#\*_\-\+\`{}\[\]!~]+")]

View File

@@ -0,0 +1,46 @@
using System.Diagnostics;
using Microsoft.Extensions.Options;
using YaeBlog.Models;
namespace YaeBlog.Services;
/// <summary>
/// 在应用程序运行的过程中启动Tailwind watch
/// 在程序退出时自动结束进程
/// 只在Development模式下启动
/// </summary>
public sealed class TailwindRefreshService(
IOptions<TailwindOptions> options,
ProcessInteropService processInteropService,
IHostEnvironment hostEnvironment,
ILogger<TailwindRefreshService> logger) : IHostedService, IDisposable
{
private Process? _tailwindProcess;
public Task StartAsync(CancellationToken cancellationToken)
{
if (!hostEnvironment.IsDevelopment())
{
return Task.CompletedTask;
}
logger.LogInformation("Try to start tailwind watcher with input {} and output {}", options.Value.InputFile,
options.Value.OutputFile);
_tailwindProcess = processInteropService.StartProcess("pnpm",
$"tailwind -i {options.Value.InputFile} -o {options.Value.OutputFile} --watch");
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_tailwindProcess?.Kill();
return Task.CompletedTask;
}
public void Dispose()
{
_tailwindProcess?.Dispose();
}
}

View File

@@ -7,26 +7,21 @@
<PackageReference Include="YamlDotNet" Version="16.2.1"/>
</ItemGroup>
<ItemGroup>
<Watch Remove="**\*.razor~"/>
</ItemGroup>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<Target Name="EnsurePnpmInstalled" BeforeTargets="Build">
<Message Importance="low" Text="Ensure pnpm is installed..."/>
<Exec Command="pnpm --version" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="ErrorCode"/>
</Exec>
<Error Condition="$(ErrorCode) != 0" Text="Pnpm is not installed which is required for build."/>
<Message Importance="normal" Text="Installing pakages using pnpm..."/>
<Target Name="PnpmInstall">
<Exec Command="pnpm install"/>
</Target>
<Target Name="TailwindGenerate" AfterTargets="EnsurePnpmInstalled">
<Message Importance="normal" Text="Generate css files using tailwind..."/>
<Target Name="TailwindGenerate" DependsOnTargets="PnpmInstall" BeforeTargets="BeforeBuild">
<Exec Command="pnpm tailwind -i wwwroot/input.css -o wwwroot/output.css"/>
</Target>

View File

@@ -1,884 +0,0 @@
---
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} ");
}
```

View File

@@ -1,89 +0,0 @@
---
title: 2024年年终总结
date: 2025-01-16T17:15:05.8634370+08:00
tags:
- 杂谈
- 年终总结
---
欸,年终总结难道不是应该在新年当天发出吗,什么已经是新年第三天了?!
然而年末偶遇流感病毒,头疼脑热强如怪物,拼尽全力也无法战胜。
所以年终总结再次跳票,红豆泥私密马赛!
<!--more-->
### 压力
本年度的第一个关键词,我会选择压力。这一年总是被不同的压力笼罩着,先是有形的压力,然后是无形的压力,在不同的时间阶段有着不同的来源。
1月份起始的两周就是大三学年秋季学期的期末考试周而鄙人在下不才我在本学期面临着计算机科学四幻神的考验——老师不知所云之操作系统、抽象概念无法理解之编译原理、全英语授课之数据库系统原理和智商不够无法战胜之算法导论。挣扎在保研线上的我刚刚被上一学期的离散数学的~~75分~~74分和数据结构的79分拷打面对着如此沉重的考试压力加起来一共12学分呢可耻的失眠了。
过完年回来的三月份就是同论文奋斗的一个月。虽然只是一篇6页的EI检索论文但对于一个**纯洁**的本科生来说还是有点太困难了。这个过程就像是你先拉了一坨大的,然后在上面细细的涂上巧克力,在最后发表的过程中,需要在众人的面前大嚼这一坨东西,并且称赞“真是一道美食啊”!还没有开始的学术生涯就已经留下永恒的污点力(悲)。
搞完论文的四月和五月则是和大作业搏斗的两个月。首先是无法战胜的“编译原理课程设计”内容是设计一个Pascal-S到C语言的源到源编译器。这一大作业的主要压力来源是大作业本身的难度直到最后提交的时候全部95个测试点也没有能够完全通过然而其他人在祖传代码上缝缝补补却过来哭。虽然考虑到我们是全手写的编译器没有使用任何的编译器构建工具提出的解决方案也称不上是墨守成规老师给了我一个还算是可以的分数算是压力中的小小慰藉。
然后是风波不断的软件工程大作业明明只是一个相对简单的Web前后端开发但是我们前后进行了三次验收才通过一直拖到了学期的第16周。老师设计的联合验收制度给我们结结实实的上了一课要求联合验收小组的不同前后端需要能够任意组合使用导致我们为了适配另外一组的逻辑几乎是把核心代码写了两遍。虽然我不喜欢在背后攻击别人但是我不得不说这一年中最有压力的时刻往往不是自己的事情搞不定时而是看着别人搞砸事情你却无能为力的时候。
这两个月还夹杂这一个意义不明的专业实习,明明是计算机科学与技术专业的牛马,为什么会被中兴通讯的老师培训通信项目的项目管理?
应付完上面这些杂七杂八的内容,便是本科生生涯中的最后三场考试:人称计算机领域的政治之《软件工程》,通信领域科普课程之《现代交换原理》和永远的神之《计算机系统结构》。
不得不说《软件工程》,~~或者人们常说的肖概~~确实不愧于计算机领域的政治之称。毕竟政治的主要课题就是研究如何组织和动员人群以完成一个特定的目标,《软件工程》不过是将人员限制为了软件的开发人员,领域限制为了软件开发领域,基本的道理还是相通的。
《现代交换原理》则是一门在现有的课程体系下非常尴尬的一门课程,显然这门课的保留还是为了凸显“计算机+通信”的学科特色,但是大量前置知识的缺失和同其他课程的脱节使得这门课就显得非常的“脱节”。而且相对来说,通信技术的发展速度远远不如互联网的迭代技术,这门课也被同学们戏称为“古代交换原理”。令人最难受的,虽然知识古代,但是却一点都不简单,很多内容只能说是听了个概念,幸好最后的考试不难,靠死记硬背通过了考试。
《计算机系统结构》就是核心课中的核心课了。课程内容和《计算机组成原理》衔接的非常紧密,~~虽然我组成原理就学的很垃圾~~主要围绕着如何最大限度的并行化运行程序进行从指令级的并行一直到多机并行可以说是压力最大的一门考试。在准备的过程中做了很多套往年题博客上也发布了一部分的复习笔记最终幸好低空飞过。唯一的吐槽是实验什么时候可以从MIPS改成为RISC-V呢。
三门课的考试一结束,这些死线明确的、有形的压力便消失了,但是无形的压力——对于是否能保研的焦虑——便笼罩下来。
7月和8月都是在这种不安和恐慌中度过这种氛围在9月份保研名单出炉之前达到了顶峰。保研的流程开始之后则是通知推着人走各种交材料各种准备答辩各种等待公示直到最后的保研名单出炉。
不过现在回想起来,最后名单出炉,获得保研资格,复试通过之后,并没有一种如释重负的感觉,或者说终于实现了既定目标的快感。反而是一种“啊,结束了”的空落感,只想回去睡一觉。
然后新的~~风暴~~压力已经出现,在度过一个短短的国庆假期之后便正式进组,作为一个研究生的社畜生涯就此开始。
### 经历
虽然2024年的第一个关键词已经选择为“压力”但是众所周知高压锅里往往能压出好吃的。人也是这样。所以我将2024年的第二个关键词定为“经历”人生如逆旅我亦是行人各式各样的经历便是风格迥异的景点。
人生第一篇学术论文的撰写和发表无疑是今年最难忘的经历。虽然我在前面称之为“学术生涯上的污点”,但是污点也好过一片空白不是,还非常的引人夺目。而且这是一个完整的撰写-发表流程,从开始的选题、实验、撰写、投稿,到最后的接受、提交、发表、报销等等数个环节我均参加。这个过程不仅让我对于学术论文的诞生流程有力较为清晰的认识,也对学校的各种发表和报销流程有了深入的了解。
两个大作业编译原理课程设计和软件工程大作业也是非常难忘的经历。这两个项目的代码都已经整理好开源在Github上了。前者代表了目前我软件开发的最高水平而后者则是我本科阶段唯一一个差点失败的软件开发项目。这种冰火两重天的对比实在是很难令人忘记。
这两个项目中的收获有非常技术性的。相较于2023年面对各种大作业时的略显底气不足这次我在各种技术栈的选择上更加游刃有余选择了完全倒向.NET和React摈弃了之前的Java和Vue。各类现代软件开发技术也得到了充分的应用例如由Gitea Actions驱动的DevOps实践完全基于合并请求的多人协作流程。事实证明这些协作流程确实在一定程度上加速了项目的开发。
但是,“软件工程里没有银弹”,先进技术的堆叠并不能保证软件项目成功。虽然我这里~~自吹自擂~~有非常多新技术的帮助,软件工程大作业的差点失败的确说明了软件工程实际上还是人的工程,猪队友永远比凶恶的敌人更可怕。当然也不能将所有的锅都扔给别人,我在项目失控的过程中也没有能够采取有力的措施挽救整个项目,~~负有不可推卸的领导责任~~。
今年最后一个难忘的经历便是去横店镇参加CNCC 2024也单独出过[博客](https://rrricardo.top/blog/essays/cncc-2024)。虽然之前学术论文发表的过程中也是在学术会议上做过口头报告,不过是线上参加的,并没有特别的实感。现在线下参加,也不需要自己上去发表,顿感旅游真好玩,~~也有可能是因为CNCC比较水~~。
### 匆匆
2024年的第三个关键词我想定为”匆匆“虽然想找一个更加”有文化“的词汇奈何自己的文化造纸实在不够故定为”匆匆“。
可2024年确实是非常忙碌的一年现在回想起来几乎每一个月都是在为了某一件特定的事情而奔走着。还记得在新年伊始的时间里我还制订了各种各样的读书计划和补番计划现在看来定计划的目的不是为了实现而是为了安心。
不过匆匆之中还是读了几本书。首先是久负盛名的《置身事内——中国政府与经济发展》,这本书的开篇即言:“这本书是写给大学生和对经济话题感兴趣的读者”,细读下来也确实如此。然后是一本我从小便着迷的二战军史相关话题《美国陷阱:橙色计划始末》,其中若干的政治与军事细节之于我不过是走马观花,不过其中表达出的长期战略实在令人敬佩。
至于补番计划我则是表现出了同电子ED一样的症状对于新番没有兴趣对于补早就下载安装好的老番更是兴趣缺缺。反倒是电视剧由于12月韩国的惊天一变我又重新下载了《第五共和国》
不过我的B站观看时长再度增长30%,这好吗,这不好,~~有这么多时间刷B站鬼知道你匆匆在哪了~~。
![image-20250115171809775](./2024-final/image-20250115171809775.png)
### 未来
> 定计划的目的不是为了实现,而是为了安心。
站在年关已经可以预见到2025年将会是更为繁忙的一年从一月份到十月份都已经有了或多或少的安排现在无法多言只能希望都能有良好的结果。
还是多说点可以说的罢。
首先是读书计划。《置身事内——中国政府与经济发展》的每章最后都有一个推荐书目一整本上总结下来也能有超过50本其中不乏超过一千页的大部头说能够一年看完显然是痴人说梦。这里先列两本同我的工作关系密切的书籍
- 陆风,《光变:一个企业及其工业史》
- 吴军,《浪潮之巅》
其次是补番计划,这一年刷到了不少押井守导演的《机动警察》系列,虽然我之前对于人形机器人并不热心,但剧中精细的作画和宏大的背景设定确实非常吸引人,遂决定今年找来看看。

Binary file not shown.

View File

@@ -1,6 +1,6 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["**/*.razor", "**/*.cshtml", "**/*.html", "Processors/EssayStylesPostRenderProcessor.cs"],
content: ["**/*.razor", "**/*.cshtml", "**/*.html"],
theme: {
extend: {},
},

7
YaeBlog/wwwroot/clipboard.min.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
YaeBlog/wwwroot/images/banner.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
YaeBlog/wwwroot/images/blog-icon.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
YaeBlog/wwwroot/images/git-icon.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
YaeBlog/wwwroot/images/wiki-icon.png (Stored with Git LFS) Normal file

Binary file not shown.