Compare commits

14 Commits

Author SHA1 Message Date
8c2cce59e4 Merge branch 'master' into write-async-await 2025-01-24 16:58:18 +08:00
3aae468e65 feat: 从Bootstrap迁移到Tailwind css (#9)
All checks were successful
Build blog docker image / Build-Blog-Image (push) Successful in 1m15s
Reviewed-on: #9
2025-01-24 16:46:56 +08:00
1ceaf30061 blog: 2024-final
All checks were successful
Build blog docker image / Build-Blog-Image (push) Successful in 1m47s
2025-01-16 17:27:11 +08:00
87204dab8e blog: rust-up-trait
All checks were successful
Build blog docker image / Build-Blog-Image (push) Successful in 1m28s
2024-12-15 15:52:58 +08:00
05d40ce3b6 bump: net8.0 -> net9.0
All checks were successful
Build blog docker image / Build-Blog-Image (push) Successful in 3m23s
2024-12-06 15:38:35 +08:00
309db7e5f1 blog: rust-drop-stack-overflow
All checks were successful
Build blog docker image / Build-Blog-Image (push) Successful in 50s
2024-11-06 14:00:46 +08:00
fb71ce64cf blog: 异构并行编程模型的昨天、今天和明天 (#8)
All checks were successful
Build blog docker image / Build-Blog-Image (push) Successful in 2m37s
Reviewed-on: #8
2024-11-04 22:33:42 +08:00
baf50eeab0 Merge branch 'master' into write-async-await 2024-09-08 22:43:46 +08:00
dc1b97fed4 write how async await works 2024-08-25 16:32:50 +08:00
9bfe091024 Merge branch 'master' into write-async-await 2024-08-25 16:30:17 +08:00
3a4ada50c6 Merge branch 'master' into write-async-await 2024-08-23 20:49:21 +08:00
05a22a0b29 Merge branch 'refs/heads/master' into write-async-await
# Conflicts:
#	YaeBlog.Core/Extensions/WebApplicationBuilderExtensions.cs
#	YaeBlog/Components/App.razor
#	YaeBlog/Layout/BlogLayout.razor
#	YaeBlog/Layout/MainLayout.razor
#	YaeBlog/Layout/MainLayout.razor.css
#	YaeBlog/Pages/About.razor
#	YaeBlog/Pages/About.razor.css
#	YaeBlog/Pages/Archives.razor
#	YaeBlog/Pages/Archives.razor.css
#	YaeBlog/Pages/BlogIndex.razor
#	YaeBlog/Pages/BlogIndex.razor.css
#	YaeBlog/Pages/Index.razor
#	YaeBlog/Pages/NotFound.razor
#	YaeBlog/Pages/Tags.razor
#	YaeBlog/Pages/Tags.razor.css
2024-07-30 00:54:02 +08:00
6797028cc1 write 2024-07-20 20:06:43 +08:00
77e52fa11e add: bootstrap in blazor 2024-07-15 21:36:18 +08:00
105 changed files with 3695 additions and 958 deletions

3
.gitignore vendored
View File

@@ -482,3 +482,6 @@ $RECYCLE.BIN/
# Vim temporary swap files
*.swp
# Tailwind auto-generated stylesheet
output.css

View File

@@ -1,8 +0,0 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop

View File

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

View File

@@ -1,29 +0,0 @@
using AngleSharp;
using AngleSharp.Dom;
using YaeBlog.Core.Abstractions;
using YaeBlog.Core.Models;
namespace YaeBlog.Core.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,34 +0,0 @@
using AngleSharp;
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using YaeBlog.Core.Abstractions;
using YaeBlog.Core.Models;
namespace YaeBlog.Core.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

@@ -1,35 +0,0 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using YaeBlog.Core.Abstractions;
namespace YaeBlog.Core.Services;
public sealed class BlogHotReloadService(
RendererService rendererService,
IEssayContentService essayContentService,
BlogChangeWatcher watcher,
ILogger<BlogHotReloadService> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation("BlogHotReloadService is starting.");
await rendererService.RenderAsync();
while (!stoppingToken.IsCancellationRequested)
{
logger.LogDebug("Watching file changes...");
string? changFile = await watcher.WaitForChange(stoppingToken);
if (changFile is null)
{
logger.LogInformation("BlogHotReloadService is stopping.");
break;
}
logger.LogInformation("{} changed, re-rendering.", changFile);
essayContentService.Clear();
await rendererService.RenderAsync();
}
}
}

View File

@@ -1,25 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AngleSharp" Version="1.1.0" />
<PackageReference Include="Markdig" Version="0.34.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.6" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="YamlDotNet" Version="13.7.1" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
</Project>

View File

@@ -3,8 +3,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YaeBlog.Core", "YaeBlog.Core\YaeBlog.Core.csproj", "{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YaeBlog", "YaeBlog\YaeBlog.csproj", "{20438EFD-8DDE-43AF-92E2-76495C29233C}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".gitea", ".gitea", "{9B5AAA29-37D8-454A-8D8F-3E6B6BCF38E6}"
@@ -29,10 +27,6 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}.Release|Any CPU.Build.0 = Release|Any CPU
{20438EFD-8DDE-43AF-92E2-76495C29233C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{20438EFD-8DDE-43AF-92E2-76495C29233C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{20438EFD-8DDE-43AF-92E2-76495C29233C}.Release|Any CPU.ActiveCfg = Release|Any CPU

View File

@@ -1,11 +1,13 @@
using System.Diagnostics.CodeAnalysis;
using YaeBlog.Core.Models;
using YaeBlog.Models;
namespace YaeBlog.Core.Abstractions;
namespace YaeBlog.Abstraction;
public interface IEssayContentService
{
public IReadOnlyDictionary<string, BlogEssay> Essays { get; }
public IEnumerable<BlogEssay> Essays { get; }
public int Count { get; }
public IReadOnlyDictionary<EssayTag, List<BlogEssay>> Tags { get; }
@@ -16,6 +18,8 @@ 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

@@ -1,6 +1,6 @@
using YaeBlog.Core.Models;
using YaeBlog.Models;
namespace YaeBlog.Core.Abstractions;
namespace YaeBlog.Abstraction;
public interface IEssayScanService
{

View File

@@ -1,6 +1,6 @@
using YaeBlog.Core.Models;
using YaeBlog.Models;
namespace YaeBlog.Core.Abstractions;
namespace YaeBlog.Abstraction;
public interface IPostRenderProcessor
{

View File

@@ -1,6 +1,6 @@
using YaeBlog.Core.Models;
using YaeBlog.Models;
namespace YaeBlog.Core.Abstractions;
namespace YaeBlog.Abstraction;
public interface IPreRenderProcessor
{

View File

@@ -1,7 +1,7 @@
using System.CommandLine.Binding;
using System.Text.Json;
using Microsoft.Extensions.Options;
using YaeBlog.Core.Models;
using YaeBlog.Models;
namespace YaeBlog.Commands.Binders;

View File

@@ -1,8 +1,8 @@
using System.CommandLine.Binding;
using Microsoft.Extensions.Options;
using YaeBlog.Core.Abstractions;
using YaeBlog.Core.Models;
using YaeBlog.Core.Services;
using YaeBlog.Abstraction;
using YaeBlog.Models;
using YaeBlog.Services;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;

View File

@@ -1,15 +1,32 @@
using System.CommandLine;
using YaeBlog.Commands.Binders;
using YaeBlog.Components;
using YaeBlog.Core.Extensions;
using YaeBlog.Core.Models;
using YaeBlog.Core.Services;
using YaeBlog.Extensions;
using YaeBlog.Models;
using YaeBlog.Services;
namespace YaeBlog.Commands;
public static class CommandExtensions
public sealed class YaeBlogCommand
{
public static void AddServeCommand(this RootCommand rootCommand)
private readonly RootCommand _rootCommand = new("YaeBlog Cli");
public YaeBlogCommand()
{
AddServeCommand(_rootCommand);
AddWatchCommand(_rootCommand);
AddListCommand(_rootCommand);
AddNewCommand(_rootCommand);
AddPublishCommand(_rootCommand);
AddScanCommand(_rootCommand);
}
public Task<int> RunAsync(string[] args)
{
return _rootCommand.InvokeAsync(args);
}
private static void AddServeCommand(RootCommand rootCommand)
{
Command serveCommand = new("serve", "Start http server.");
rootCommand.AddCommand(serveCommand);
@@ -21,7 +38,6 @@ public static class CommandExtensions
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddControllers();
builder.Services.AddBlazorBootstrap();
builder.AddYaeBlog();
builder.AddServer();
@@ -40,7 +56,7 @@ public static class CommandExtensions
});
}
public static void AddWatchCommand(this RootCommand rootCommand)
private static void AddWatchCommand(RootCommand rootCommand)
{
Command command = new("watch", "Start a blog watcher that re-render when file changes.");
rootCommand.AddCommand(command);
@@ -52,7 +68,6 @@ public static class CommandExtensions
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddControllers();
builder.Services.AddBlazorBootstrap();
builder.AddYaeBlog();
builder.AddWatcher();
@@ -71,7 +86,7 @@ public static class CommandExtensions
});
}
public static void AddNewCommand(this RootCommand rootCommand)
private static void AddNewCommand(RootCommand rootCommand)
{
Command newCommand = new("new", "Create a new blog file and image directory.");
rootCommand.AddCommand(newCommand);
@@ -101,7 +116,7 @@ public static class CommandExtensions
new EssayScanServiceBinder());
}
public static void AddListCommand(this RootCommand rootCommand)
private static void AddListCommand(RootCommand rootCommand)
{
Command command = new("list", "List all blogs");
rootCommand.AddCommand(command);
@@ -124,7 +139,7 @@ public static class CommandExtensions
}, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder());
}
public static void AddScanCommand(this RootCommand rootCommand)
private static void AddScanCommand(RootCommand rootCommand)
{
Command command = new("scan", "Scan unused and not found images.");
rootCommand.AddCommand(command);
@@ -165,7 +180,7 @@ public static class CommandExtensions
}, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder(), removeOption);
}
public static void AddPublishCommand(this RootCommand rootCommand)
private static void AddPublishCommand(RootCommand rootCommand)
{
Command command = new("publish", "Publish a new blog file.");
rootCommand.AddCommand(command);

View File

@@ -0,0 +1,9 @@
<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="en">
<html lang="zh">
<head>
<meta charset="utf-8"/>
@@ -7,22 +7,14 @@
<base href="/"/>
<link rel="stylesheet" href="YaeBlog.styles.css"/>
<link rel="icon" href="images/favicon.ico"/>
<link rel="stylesheet" href="bootstrap.min.css"/>
<link rel="stylesheet" href="bootstrap-icons.min.css"/>
<link rel="stylesheet" href="_content/Blazor.Bootstrap/blazor.bootstrap.css"/>
<link rel="stylesheet" href="globals.css"/>
<link rel="stylesheet" href="output.css"/>
<HeadOutlet/>
</head>
<body>
<Routes/>
<script src="_framework/blazor.web.js"></script>
<script src="bootstrap.bundle.min.js"></script>
<script src="clipboard.min.js"></script>
<script>
const clipboard = new ClipboardJS('.btn');
</script>
<Routes/>
<script src="_framework/blazor.web.js"></script>
</body>
</html>

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,22 @@
<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> 驱动。
<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"/>
驱动。
</p>
</div>
<div class="row">
<p class="fs-6">
<a href="https://beian.miit.gov.cn" target="_blank">蜀ICP备2022004429号-1</a>
<div>
<p class="text-md">
<a href="https://beian.miit.gov.cn" target="_blank" class="text-black">蜀ICP备2022004429号-1</a>
</p>
</div>
</div>
@code
{
private string DotnetVersion => $".NET {Environment.Version}";
}

View File

@@ -1,36 +1,33 @@
@using YaeBlog.Core.Models
@using YaeBlog.Models
@inject BlogOptions Options
<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 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>
<div class="row p-1">
<div class="col">
文章地址:
<a href="/blog/essays/@(EssayAddress)" target="_blank">
@($"https://rrricardo.top/blog/essays/{EssayAddress}")
</a>
</div>
<div>
文章地址:
<a href="/blog/essays/@(EssayFilename)" target="_blank" class="text-blue-600">
@($"https://rrricardo.top/blog/essays/{EssayFilename}")
</a>
</div>
<div class="row p-1">
<div class="col">
版权声明:本博客所有文章除特别声明外,均采用
<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>
版权声明:本博客所有文章除特别声明外,均采用
<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>”。
</div>
</div>
</div>
@code
{
[Parameter] public string? EssayAddress { get; set; }
[Parameter] public string? EssayFilename { get; set; }
}

View File

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

View File

@@ -0,0 +1,22 @@
@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

@@ -0,0 +1,46 @@
<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

@@ -1,7 +1,7 @@
FROM mcr.microsoft.com/dotnet/aspnet:8.0
FROM mcr.microsoft.com/dotnet/aspnet:9.0
WORKDIR /app
COPY bin/Release/net8.0/publish/ ./
COPY bin/Release/net9.0/publish/ ./
COPY source/ ./source/
COPY appsettings.json .

View File

@@ -1,9 +1,8 @@
using Markdig;
using Microsoft.Extensions.DependencyInjection;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace YaeBlog.Core.Extensions;
namespace YaeBlog.Extensions;
public static class ServiceCollectionExtensions
{

View File

@@ -1,13 +1,11 @@
using AngleSharp;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using YaeBlog.Core.Abstractions;
using YaeBlog.Core.Models;
using YaeBlog.Core.Processors;
using YaeBlog.Core.Services;
using YaeBlog.Abstraction;
using YaeBlog.Services;
using YaeBlog.Models;
using YaeBlog.Processors;
namespace YaeBlog.Core.Extensions;
namespace YaeBlog.Extensions;
public static class WebApplicationBuilderExtensions
{
@@ -19,14 +17,13 @@ public static class WebApplicationBuilderExtensions
builder.Services.AddMarkdig();
builder.Services.AddYamlParser();
builder.Services.AddSingleton<IConfiguration>(_ => Configuration.Default);
builder.Services.AddSingleton<AngleSharp.IConfiguration>(_ => Configuration.Default);
builder.Services.AddSingleton<IEssayScanService, EssayScanService>();
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);

View File

@@ -1,19 +1,16 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using YaeBlog.Core.Abstractions;
using YaeBlog.Core.Processors;
using YaeBlog.Core.Services;
using YaeBlog.Abstraction;
using YaeBlog.Processors;
using YaeBlog.Services;
namespace YaeBlog.Core.Extensions;
namespace YaeBlog.Extensions;
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,60 +2,41 @@
@attribute [StreamRendering]
<main class="container">
<div class="row d-none d-xl-flex" style="height: 80px">
<div class="px-2 col-9">
<a href="/blog/" class="p-2">
<h4>Ricardo's Blog</h4>
<main class="container mx-auto flex flex-col min-h-screen">
<div class="grid grid-cols-3 mx-3">
<div class="md:col-span-2 col-span-3 h-20 flex items-center">
<a href="/blog/">
<span class="text-blue-600 text-2xl">Ricardo's Blog</span>
</a>
</div>
<div class="col-3 d-flex justify-content-around align-items-center">
<a href="/blog/" class="p-2">
<h5>首页</h5>
</a>
<a href="/blog/archives/" class="p-2">
<h5>归档</h5>
</a>
<a href="/blog/tags/" class="p-2">
<h5>标签</h5>
</a>
<a href="/blog/about/" class="p-2">
<h5>关于</h5>
</a>
<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>
<a href="/blog/archives/">
<span class="text-xl text-blue-600">归档</span>
</a>
</div>
<div>
<a href="/blog/tags/">
<span class="text-xl text-blue-600">标签</span>
</a>
</div>
<div>
<a href="/about/" target="_blank">
<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="row d-xl-none">
<div class="px-2 col-12">
<a href="/blog/" class="p-2">
<h4>Ricardo's Blog</h4>
</a>
</div>
<div class="px-2 col-12 justify-content-end d-flex">
<a href="/blog/" class="p-2">
<h5>首页</h5>
</a>
<a href="/blog/archives/" class="p-2">
<h5>归档</h5>
</a>
<a href="/blog/tags/" class="p-2">
<h5>标签</h5>
</a>
<a href="/blog/about/" class="p-2">
<h5>关于</h5>
</a>
</div>
</div>
<div class="row px-4 py-2">
<div class="px-4 py-2 flex-grow">
@Body
</div>

View File

@@ -1,21 +1,34 @@
@inherits LayoutComponentBase
<main class="container">
<div class="row" style="height: 80px">
<div class="px-2 col-8">
<a href="/" class="p-2">
<h4>Ricardo's Index</h4>
<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">
<h4 class="text-blue-600">Ricardo's Index</h4>
</a>
</div>
<div class="col-4 d-flex justify-content-around align-items-center">
<a href="mailto://shicangjuner@outlook.com" class="p-2" target="_blank">
<h5>E-mail</h5>
</a>
<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>
</div>
<div class="row px-4 center">
<div class="px-4 mx-auto flex-grow">
<div class="py-2">
@Body
</div>

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
using System.Collections.Concurrent;
namespace YaeBlog.Core.Models;
namespace YaeBlog.Models;
public sealed class BlogContents(ConcurrentBag<BlogContent> drafts, ConcurrentBag<BlogContent> posts)
{

View File

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

View File

@@ -1,4 +1,4 @@
namespace YaeBlog.Core.Models;
namespace YaeBlog.Models;
public class BlogHeadline(string title, string selectorId)
{

View File

@@ -1,4 +1,4 @@
namespace YaeBlog.Core.Models;
namespace YaeBlog.Models;
public class BlogOptions
{

View File

@@ -1,6 +1,6 @@
using System.Text.Encodings.Web;
namespace YaeBlog.Core.Models;
namespace YaeBlog.Models;
public class EssayTag(string tagName) : IEquatable<EssayTag>
{

View File

@@ -1,4 +1,4 @@
namespace YaeBlog.Core.Models;
namespace YaeBlog.Models;
/// <summary>
/// 友链模型类

View File

@@ -1,3 +1,3 @@
namespace YaeBlog.Core.Models;
namespace YaeBlog.Models;
public record struct ImageScanResult(List<FileInfo> UnusedImages, List<FileInfo> NotFoundImages);

View File

@@ -1,4 +1,4 @@
namespace YaeBlog.Core.Models;
namespace YaeBlog.Models;
public class MarkdownMetadata
{

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
@page "/blog/archives"
@using YaeBlog.Core.Abstractions
@using YaeBlog.Core.Models
@using YaeBlog.Abstraction
@using YaeBlog.Models
@inject IEssayContentService Contents
@@ -8,68 +8,56 @@
归档
</PageTitle>
<div class="container">
<div class="row">
<div class="col">
<div class="container">
<div class="row">
<div class="col">
<h1>归档</h1>
</div>
</div>
<div class="row">
<div class="col fst-italic py-4">
时光图书馆,黑历史集散地。(๑◔‿◔๑)
</div>
</div>
</div>
</div>
<div class="flex flex-col">
<div>
<h1 class="text-4xl">归档</h1>
</div>
@foreach (IGrouping<DateTime, KeyValuePair<string, BlogEssay>> group in _essays)
{
<div class="row">
<div class="col">
<div class="container">
<div class="row">
<div class="col">
<h3>@(group.Key.Year)</h3>
</div>
</div>
<div class="py-4">
<span class="italic">
时光图书馆,黑历史集散地。(๑◔‿◔๑)
</span>
</div>
<div class="container px-3 py-2">
@foreach (KeyValuePair<string, BlogEssay> essay in group)
{
<div class="row py-1">
<div class="col-auto">
@(essay.Value.PublishTime.ToString("MM-dd"))
@foreach (IGrouping<DateTime, BlogEssay> group in _essays)
{
<div class="p-2">
<div class="flex flex-col">
<div>
<h3 class="text-xl">@(group.Key.Year)</h3>
</div>
<div class="px-4 py-4 flex flex-col">
@foreach (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>
<div class="col-auto">
<a href="/blog/essays/@(essay.Key)">
@(essay.Value.Title)
</a>
<div>
<span class="text-blue-600">
@(essay.Title)
</span>
</div>
</div>
}
</div>
</a>
}
</div>
</div>
</div>
}
</div>
@code {
private readonly List<IGrouping<DateTime, KeyValuePair<string, BlogEssay>>> _essays = [];
private readonly List<IGrouping<DateTime, BlogEssay>> _essays = [];
protected override void OnInitialized()
{
base.OnInitialized();
_essays.AddRange(from essay in Contents.Essays
orderby essay.Value.PublishTime descending
group essay by new DateTime(essay.Value.PublishTime.Year, 1, 1));
group essay by new DateTime(essay.PublishTime.Year, 1, 1));
}
}

View File

@@ -1,6 +1,6 @@
@page "/blog"
@using YaeBlog.Core.Abstractions
@using YaeBlog.Core.Models
@using YaeBlog.Abstraction
@using YaeBlog.Models
@inject IEssayContentService Contents
@inject NavigationManager NavigationInstance
@@ -9,79 +9,18 @@
Ricardo's Blog
</PageTitle>
<div class="container">
<div class="row">
<div class="col-sm-12 col-md-9">
@foreach (KeyValuePair<string, BlogEssay> pair in _essays)
<div>
<div class="grid grid-cols-4">
<div class="col-span-4 md:col-span-3">
@foreach (BlogEssay essay in _essays)
{
<EssayCard Essay="@(pair.Value)"/>
<EssayCard Essay="@(essay)"/>
}
<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>
<Pagination BaseUrl="/blog/" Page="_page" PageCount="_pageCount"/>
</div>
<div class="col-sm-12 col-md-3">
<div class="col-span-4 md:col-span-1">
<BlogInformationCard/>
</div>
</div>
@@ -91,7 +30,7 @@
[SupplyParameterFromQuery] private int? Page { get; set; }
private readonly List<KeyValuePair<string, BlogEssay>> _essays = [];
private readonly List<BlogEssay> _essays = [];
private const int EssaysPerPage = 8;
private int _pageCount = 1;
private int _page = 1;
@@ -99,16 +38,15 @@
protected override void OnInitialized()
{
_page = Page ?? 1;
_pageCount = Contents.Essays.Count / EssaysPerPage + 1;
_pageCount = Contents.Count / EssaysPerPage + 1;
if (EssaysPerPage * _page > Contents.Essays.Count + EssaysPerPage)
if (EssaysPerPage * _page > Contents.Count + EssaysPerPage)
{
NavigationInstance.NavigateTo("/NotFount");
return;
}
_essays.AddRange(Contents.Essays
.OrderByDescending(p => p.Value.PublishTime)
.Skip((_page - 1) * EssaysPerPage)
.Take(EssaysPerPage));
}

View File

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

View File

@@ -1,7 +1,7 @@
@page "/blog/essays/{BlogKey}"
@using System.Text.Encodings.Web
@using YaeBlog.Core.Abstractions
@using YaeBlog.Core.Models
@using YaeBlog.Abstraction
@using YaeBlog.Models
@inject IEssayContentService Contents
@inject NavigationManager NavigationInstance
@@ -10,96 +10,87 @@
@(_essay!.Title)
</PageTitle>
<div class="container py-4">
<div class="row">
<div class="flex flex-col py-8">
<div>
<h1 id="title" class="text-4xl">@(_essay!.Title)</h1>
<div class="col-auto">
<h1 id="title">@(_essay!.Title)</h1>
</div>
</div>
<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="col-auto">
<a href="/blog/tags/?tagName=@(UrlEncoder.Default.Encode(tag))">
# @(tag)
</a>
<div class="px-6 pt-4 pb-2">
<div class="flex flex-row gap-4">
<div class="font-light">
@(_essay!.PublishTime.ToString("yyyy-MM-dd"))
</div>
}
@foreach (string tag in _essay!.Tags)
{
<div class="text-sky-500">
<a href="/blog/tags/?tagName=@(UrlEncoder.Default.Encode(tag))">
# @(tag)
</a>
</div>
}
</div>
</div>
<div class="row px-4 py-1">
<div class="col-auto fw-light">
<div class="px-6 pt-2 pb-4">
<div class="font-light">
总字数:@(_essay!.WordCount)字,预计阅读时间 @(_essay!.ReadTime)。
</div>
</div>
<div class="row">
<div class="col-lg-8 col-md-12">
@((MarkupString)_essay!.HtmlContent)
<div class="grid grid-cols-3">
<div class="col-span-3 md:col-span-2 flex flex-col gap-3">
<div>
@((MarkupString)_essay!.HtmlContent)
</div>
<LicenseDisclaimer EssayAddress="@BlogKey"/>
<div>
<LicenseDisclaimer EssayFilename="@BlogKey"/>
</div>
</div>
<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 class="row" style="padding-left: 10px">
<div class="col-auto">
@foreach (BlogHeadline level2 in _headline!.Children)
{
<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="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="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)
<div>
@foreach (BlogHeadline level2 in _headline!.Children)
{
<div class="row">
<div class="col fst-italic">
坏了(* Ŏ∀Ŏ),没有在文章中识别到目录
</div>
<div class="py-2 pl-3">
<Anchor Address="@(GenerateSelectorUrl(level2.SelectorId))"
Text="@(level2.Title)"/>
</div>
@foreach (BlogHeadline level3 in level2.Children)
{
<div class="py-2 pl-6">
<Anchor Address="@(GenerateSelectorUrl(level3.SelectorId))"
Text="@(level3.Title)"/>
</div>
@foreach (BlogHeadline level4 in level3.Children)
{
<div class="py-2 pl-9">
<Anchor Address="@(GenerateSelectorUrl(level4.SelectorId))"
Text="@(level4.Title)"/>
</div>
}
}
}
</div>
@if (_headline!.Children.Count == 0)
{
<div class="row">
<div class="col fst-italic">
坏了(* Ŏ∀Ŏ),没有在文章中识别到目录
</div>
</div>
}
</div>
</div>
</div>
@@ -123,7 +114,7 @@
return;
}
if (!Contents.Essays.TryGetValue(BlogKey, out _essay))
if (!Contents.TryGetEssay(BlogKey, out _essay))
{
NavigationInstance.NavigateTo("/NotFound");
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,102 @@
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

@@ -1,13 +1,12 @@
using AngleSharp;
using AngleSharp.Dom;
using Microsoft.Extensions.Logging;
using YaeBlog.Core.Abstractions;
using YaeBlog.Core.Models;
using YaeBlog.Abstraction;
using YaeBlog.Models;
namespace YaeBlog.Core.Processors;
namespace YaeBlog.Processors;
public class HeadlinePostRenderProcessor(
IConfiguration angleConfiguration,
AngleSharp.IConfiguration angleConfiguration,
IEssayContentService essayContentService,
ILogger<HeadlinePostRenderProcessor> logger) : IPostRenderProcessor
{

View File

@@ -1,24 +1,21 @@
using AngleSharp;
using AngleSharp.Dom;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using YaeBlog.Core.Abstractions;
using YaeBlog.Abstraction;
using YaeBlog.Core.Exceptions;
using YaeBlog.Core.Models;
using YaeBlog.Models;
namespace YaeBlog.Core.Processors;
namespace YaeBlog.Processors;
public class ImagePostRenderProcessor(ILogger<ImagePostRenderProcessor> logger,
IOptions<BlogOptions> options)
: IPostRenderProcessor
{
private static readonly IConfiguration s_configuration = Configuration.Default;
private readonly BlogOptions _options = options.Value;
public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
{
BrowsingContext context = new(s_configuration);
BrowsingContext context = new(Configuration.Default);
IDocument html = await context.OpenAsync(
req => req.Content(essay.HtmlContent));
@@ -34,7 +31,6 @@ 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

@@ -1,13 +1,4 @@
using System.CommandLine;
using YaeBlog.Commands;
RootCommand rootCommand = new("YaeBlog CLI");
rootCommand.AddServeCommand();
rootCommand.AddNewCommand();
rootCommand.AddListCommand();
rootCommand.AddWatchCommand();
rootCommand.AddScanCommand();
rootCommand.AddPublishCommand();
await rootCommand.InvokeAsync(args);
YaeBlogCommand command = new();
await command.RunAsync(args);

View File

@@ -1,8 +1,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using YaeBlog.Core.Models;
using Microsoft.Extensions.Options;
using YaeBlog.Models;
namespace YaeBlog.Core.Services;
namespace YaeBlog.Services;
public sealed class BlogChangeWatcher : IDisposable
{

View File

@@ -1,7 +1,4 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace YaeBlog.Core.Services;
namespace YaeBlog.Services;
public class BlogHostedService(
ILogger<BlogHostedService> logger,
@@ -9,14 +6,12 @@ public class BlogHostedService(
{
public async Task StartAsync(CancellationToken cancellationToken)
{
logger.LogInformation("Welcome to YaeBlog!");
logger.LogInformation("Failed to load cache, render essays.");
await rendererService.RenderAsync();
}
public Task StopAsync(CancellationToken cancellationToken)
{
logger.LogInformation("YaeBlog stopped!\nHave a nice day!");
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,41 @@
using YaeBlog.Abstraction;
namespace YaeBlog.Services;
public sealed class BlogHotReloadService(
RendererService rendererService,
IEssayContentService essayContentService,
BlogChangeWatcher watcher,
ILogger<BlogHotReloadService> logger) : BackgroundService
{
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.");
await rendererService.RenderAsync(true);
Task[] reloadTasks = [FileWatchTask(stoppingToken)];
await Task.WhenAll(reloadTasks);
}
private async Task FileWatchTask(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
logger.LogInformation("Watching file changes...");
string? changeFile = await watcher.WaitForChange(token);
if (changeFile is null)
{
logger.LogInformation("File watcher is stopping.");
break;
}
logger.LogInformation("{} changed, re-rendering.", changeFile);
essayContentService.Clear();
await rendererService.RenderAsync(true);
}
}
}

View File

@@ -1,23 +1,36 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using YaeBlog.Core.Abstractions;
using YaeBlog.Core.Models;
using YaeBlog.Abstraction;
using YaeBlog.Models;
namespace YaeBlog.Core.Services;
namespace YaeBlog.Services;
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) => _essays.TryAdd(essay.FileName, essay);
public bool TryAdd(BlogEssay essay)
{
_sortedEssays.Add(essay);
return _essays.TryAdd(essay.FileName, essay);
}
public bool TryAddHeadline(string filename, BlogHeadline headline) => _headlines.TryAdd(filename, headline);
public IReadOnlyDictionary<string, BlogEssay> Essays => _essays;
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<EssayTag, List<BlogEssay>> Tags => _tags;

View File

@@ -1,14 +1,13 @@
using System.Collections.Concurrent;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using YaeBlog.Core.Abstractions;
using YaeBlog.Abstraction;
using YaeBlog.Core.Exceptions;
using YaeBlog.Core.Models;
using YaeBlog.Models;
using YamlDotNet.Core;
using YamlDotNet.Serialization;
namespace YaeBlog.Core.Services;
namespace YaeBlog.Services;
public partial class EssayScanService(
ISerializer yamlSerializer,
@@ -23,8 +22,8 @@ public partial class EssayScanService(
ValidateDirectory(_blogOptions.Root, out DirectoryInfo drafts, out DirectoryInfo posts);
return new BlogContents(
await ScanContentsInternal(drafts),
await ScanContentsInternal(posts));
await ScanContentsInternal(drafts, true),
await ScanContentsInternal(posts, false));
}
public async Task SaveBlogContent(BlogContent content, bool isDraft = true)
@@ -61,7 +60,7 @@ public partial class EssayScanService(
}
}
private async Task<ConcurrentBag<BlogContent>> ScanContentsInternal(DirectoryInfo directory)
private async Task<ConcurrentBag<BlogContent>> ScanContentsInternal(DirectoryInfo directory, bool isDraft)
{
// 扫描以md结果的但是不是隐藏文件的文件
IEnumerable<FileInfo> markdownFiles = from file in directory.EnumerateFiles()
@@ -98,7 +97,8 @@ public partial class EssayScanService(
contents.Add(new BlogContent
{
FileName = filename[..^3], Metadata = metadata, FileContent = content[(endPos + 3)..]
FileName = filename[..^3], Metadata = metadata, FileContent = content[(endPos + 3)..],
IsDraft = isDraft
});
}
catch (YamlException e)
@@ -133,13 +133,12 @@ public partial class EssayScanService(
private static Task<ImageScanResult> ScanUnusedImagesInternal(IEnumerable<BlogContent> contents,
DirectoryInfo root)
{
Regex imageRegex = ImageRegex();
ConcurrentBag<FileInfo> unusedImage = [];
ConcurrentBag<FileInfo> notFoundImage = [];
Parallel.ForEach(contents, content =>
{
MatchCollection result = imageRegex.Matches(content.FileContent);
MatchCollection result = ImagePattern.Matches(content.FileContent);
DirectoryInfo imageDirectory = new(Path.Combine(root.FullName, content.FileName));
Dictionary<string, bool> usedDictionary;
@@ -182,7 +181,7 @@ public partial class EssayScanService(
}
[GeneratedRegex(@"\!\[.*?\]\((.*?)\)")]
private static partial Regex ImageRegex();
private static partial Regex ImagePattern { get; }
private void ValidateDirectory(string root, out DirectoryInfo drafts, out DirectoryInfo posts)
{

View File

@@ -3,12 +3,11 @@ using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
using Markdig;
using Microsoft.Extensions.Logging;
using YaeBlog.Core.Abstractions;
using YaeBlog.Abstraction;
using YaeBlog.Core.Exceptions;
using YaeBlog.Core.Models;
using YaeBlog.Models;
namespace YaeBlog.Core.Services;
namespace YaeBlog.Services;
public partial class RendererService(
ILogger<RendererService> logger,
@@ -22,40 +21,43 @@ public partial class RendererService(
private readonly List<IPostRenderProcessor> _postRenderProcessors = [];
public async Task RenderAsync()
public async Task RenderAsync(bool includeDrafts = false)
{
_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)
{
foreach (BlogContent content in preProcessedContents)
uint wordCount = GetWordCount(content);
BlogEssay essay = new()
{
uint wordCount = GetWordCount(content);
BlogEssay essay = new()
{
Title = content.Metadata.Title ?? content.FileName,
FileName = content.FileName,
Description = GetDescription(content),
WordCount = wordCount,
ReadTime = CalculateReadTime(wordCount),
PublishTime = content.Metadata.Date ?? DateTime.Now,
HtmlContent = content.FileContent
};
Title = content.Metadata.Title ?? content.FileName,
FileName = content.FileName,
IsDraft = content.IsDraft,
Description = GetDescription(content),
WordCount = wordCount,
ReadTime = CalculateReadTime(wordCount),
PublishTime = content.Metadata.Date ?? DateTime.Now,
HtmlContent = content.FileContent
};
if (content.Metadata.Tags is not null)
{
essay.Tags.AddRange(content.Metadata.Tags);
}
essays.Add(essay);
if (content.Metadata.Tags is not null)
{
essay.Tags.AddRange(content.Metadata.Tags);
}
});
essays.Add(essay);
}
ConcurrentBag<BlogEssay> postProcessEssays = [];
Parallel.ForEach(essays, essay =>
@@ -67,7 +69,16 @@ public partial class RendererService(
logger.LogDebug("Render markdown file {}.", newEssay);
});
await PostProcess(postProcessEssays);
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}'.");
}
}
essayContentService.RefreshTags();
_stopwatch.Stop();
@@ -118,8 +129,10 @@ public partial class RendererService(
return processedContents;
}
private async Task PostProcess(IEnumerable<BlogEssay> essays)
private async Task<IEnumerable<BlogEssay>> PostProcess(IEnumerable<BlogEssay> essays)
{
ConcurrentBag<BlogEssay> processedContents = [];
await Parallel.ForEachAsync(essays, async (essay, _) =>
{
foreach (IPostRenderProcessor processor in _postRenderProcessors)
@@ -127,16 +140,18 @@ public partial class RendererService(
essay = await processor.ProcessAsync(essay);
}
if (!essayContentService.TryAdd(essay))
{
throw new BlogFileException(
$"There are two essays with the same name: '{essay.FileName}'.");
}
processedContents.Add(essay);
});
List<BlogEssay> result = processedContents.ToList();
result.Sort();
return result;
}
[GeneratedRegex(@"(?<!\\)[^\#\*_\-\+\`{}\[\]!~]+")]
private static partial Regex DescriptionPattern();
// private static partial Regex DescriptionPattern();
private static partial Regex DescriptionPattern { get; }
private string GetDescription(BlogContent content)
{
@@ -152,7 +167,7 @@ public partial class RendererService(
}
string rawContent = content.FileContent[..pos];
MatchCollection matches = DescriptionPattern().Matches(rawContent);
MatchCollection matches = DescriptionPattern.Matches(rawContent);
StringBuilder builder = new();
foreach (Match match in matches)

View File

@@ -1,18 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<ProjectReference Include="..\YaeBlog.Core\YaeBlog.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1"/>
<PackageReference Include="AngleSharp" Version="1.1.0"/>
<PackageReference Include="Markdig" Version="0.38.0"/>
<PackageReference Include="YamlDotNet" Version="16.2.1"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Blazor.Bootstrap" Version="3.0.0-preview.2" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net8.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..."/>
<Exec Command="pnpm install"/>
</Target>
<Target Name="TailwindGenerate" AfterTargets="EnsurePnpmInstalled">
<Message Importance="normal" Text="Generate css files using tailwind..."/>
<Exec Command="pnpm tailwind -i wwwroot/input.css -o wwwroot/output.css"/>
</Target>
</Project>

View File

@@ -6,6 +6,5 @@
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using BlazorBootstrap
@using YaeBlog
@using YaeBlog.Components

View File

@@ -6,6 +6,10 @@
}
},
"AllowedHosts": "*",
"Tailwind": {
"InputFile": "wwwroot/input.css",
"OutputFile": "wwwroot/output.css"
},
"Blog": {
"Root": "source",
"Announcement": "博客锐意装修中,敬请期待!测试阶段如有问题还请海涵。",

12
YaeBlog/package.json Normal file
View File

@@ -0,0 +1,12 @@
{
"name": "YaeBlog",
"version": "1.0.0",
"description": "",
"scripts": {},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"tailwindcss": "^3.4.16"
}
}

836
YaeBlog/pnpm-lock.yaml generated Normal file
View File

@@ -0,0 +1,836 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
tailwindcss:
specifier: ^3.4.16
version: 3.4.16
packages:
'@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
'@jridgewell/gen-mapping@0.3.8':
resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
engines: {node: '>=6.0.0'}
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
'@jridgewell/set-array@1.2.1':
resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
engines: {node: '>=6.0.0'}
'@jridgewell/sourcemap-codec@1.5.0':
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
'@nodelib/fs.stat@2.0.5':
resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
engines: {node: '>= 8'}
'@nodelib/fs.walk@1.2.8':
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
ansi-regex@6.1.0:
resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==}
engines: {node: '>=12'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
ansi-styles@6.2.1:
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
engines: {node: '>=12'}
any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
arg@5.0.2:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
brace-expansion@2.0.1:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
camelcase-css@2.0.1:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
engines: {node: '>= 6'}
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
commander@4.1.1:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
hasBin: true
didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
dlv@1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
fast-glob@3.3.2:
resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
engines: {node: '>=8.6.0'}
fastq@1.17.1:
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
foreground-child@3.3.0:
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
engines: {node: '>=14'}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
glob-parent@6.0.2:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
glob@10.4.5:
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
hasBin: true
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
is-core-module@2.16.0:
resolution: {integrity: sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==}
engines: {node: '>= 0.4'}
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
jiti@1.21.6:
resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==}
hasBin: true
lilconfig@3.1.3:
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
engines: {node: '>=14'}
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
micromatch@4.0.8:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
minimatch@9.0.5:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'}
minipass@7.1.2:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
nanoid@3.3.8:
resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
object-hash@3.0.0:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
engines: {node: '>= 6'}
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
path-scurry@1.11.1:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
pify@2.3.0:
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
engines: {node: '>=0.10.0'}
pirates@4.0.6:
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
engines: {node: '>= 6'}
postcss-import@15.1.0:
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
engines: {node: '>=14.0.0'}
peerDependencies:
postcss: ^8.0.0
postcss-js@4.0.1:
resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==}
engines: {node: ^12 || ^14 || >= 16}
peerDependencies:
postcss: ^8.4.21
postcss-load-config@4.0.2:
resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==}
engines: {node: '>= 14'}
peerDependencies:
postcss: '>=8.0.9'
ts-node: '>=9.0.0'
peerDependenciesMeta:
postcss:
optional: true
ts-node:
optional: true
postcss-nested@6.2.0:
resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==}
engines: {node: '>=12.0'}
peerDependencies:
postcss: ^8.2.14
postcss-selector-parser@6.1.2:
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
engines: {node: '>=4'}
postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
postcss@8.4.49:
resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==}
engines: {node: ^10 || ^12 || >=14}
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
read-cache@1.0.0:
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
resolve@1.22.9:
resolution: {integrity: sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==}
hasBin: true
reusify@1.0.4:
resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
shebang-regex@3.0.0:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
string-width@5.1.2:
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
engines: {node: '>=12'}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
strip-ansi@7.1.0:
resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
engines: {node: '>=12'}
sucrase@3.35.0:
resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
engines: {node: '>=16 || 14 >=14.17'}
hasBin: true
supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
tailwindcss@3.4.16:
resolution: {integrity: sha512-TI4Cyx7gDiZ6r44ewaJmt0o6BrMCT5aK5e0rmJ/G9Xq3w7CX/5VXl/zIPEJZFUK5VEqwByyhqNPycPlvcK4ZNw==}
engines: {node: '>=14.0.0'}
hasBin: true
thenify-all@1.6.0:
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
engines: {node: '>=0.8'}
thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
hasBin: true
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
wrap-ansi@8.1.0:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
engines: {node: '>=12'}
yaml@2.6.1:
resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==}
engines: {node: '>= 14'}
hasBin: true
snapshots:
'@alloc/quick-lru@5.2.0': {}
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
string-width-cjs: string-width@4.2.3
strip-ansi: 7.1.0
strip-ansi-cjs: strip-ansi@6.0.1
wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0
'@jridgewell/gen-mapping@0.3.8':
dependencies:
'@jridgewell/set-array': 1.2.1
'@jridgewell/sourcemap-codec': 1.5.0
'@jridgewell/trace-mapping': 0.3.25
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/set-array@1.2.1': {}
'@jridgewell/sourcemap-codec@1.5.0': {}
'@jridgewell/trace-mapping@0.3.25':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
run-parallel: 1.2.0
'@nodelib/fs.stat@2.0.5': {}
'@nodelib/fs.walk@1.2.8':
dependencies:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.17.1
'@pkgjs/parseargs@0.11.0':
optional: true
ansi-regex@5.0.1: {}
ansi-regex@6.1.0: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
ansi-styles@6.2.1: {}
any-promise@1.3.0: {}
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
picomatch: 2.3.1
arg@5.0.2: {}
balanced-match@1.0.2: {}
binary-extensions@2.3.0: {}
brace-expansion@2.0.1:
dependencies:
balanced-match: 1.0.2
braces@3.0.3:
dependencies:
fill-range: 7.1.1
camelcase-css@2.0.1: {}
chokidar@3.6.0:
dependencies:
anymatch: 3.1.3
braces: 3.0.3
glob-parent: 5.1.2
is-binary-path: 2.1.0
is-glob: 4.0.3
normalize-path: 3.0.0
readdirp: 3.6.0
optionalDependencies:
fsevents: 2.3.3
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
color-name@1.1.4: {}
commander@4.1.1: {}
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
cssesc@3.0.0: {}
didyoumean@1.2.2: {}
dlv@1.1.3: {}
eastasianwidth@0.2.0: {}
emoji-regex@8.0.0: {}
emoji-regex@9.2.2: {}
fast-glob@3.3.2:
dependencies:
'@nodelib/fs.stat': 2.0.5
'@nodelib/fs.walk': 1.2.8
glob-parent: 5.1.2
merge2: 1.4.1
micromatch: 4.0.8
fastq@1.17.1:
dependencies:
reusify: 1.0.4
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
foreground-child@3.3.0:
dependencies:
cross-spawn: 7.0.6
signal-exit: 4.1.0
fsevents@2.3.3:
optional: true
function-bind@1.1.2: {}
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
glob-parent@6.0.2:
dependencies:
is-glob: 4.0.3
glob@10.4.5:
dependencies:
foreground-child: 3.3.0
jackspeak: 3.4.3
minimatch: 9.0.5
minipass: 7.1.2
package-json-from-dist: 1.0.1
path-scurry: 1.11.1
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
is-binary-path@2.1.0:
dependencies:
binary-extensions: 2.3.0
is-core-module@2.16.0:
dependencies:
hasown: 2.0.2
is-extglob@2.1.1: {}
is-fullwidth-code-point@3.0.0: {}
is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
is-number@7.0.0: {}
isexe@2.0.0: {}
jackspeak@3.4.3:
dependencies:
'@isaacs/cliui': 8.0.2
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
jiti@1.21.6: {}
lilconfig@3.1.3: {}
lines-and-columns@1.2.4: {}
lru-cache@10.4.3: {}
merge2@1.4.1: {}
micromatch@4.0.8:
dependencies:
braces: 3.0.3
picomatch: 2.3.1
minimatch@9.0.5:
dependencies:
brace-expansion: 2.0.1
minipass@7.1.2: {}
mz@2.7.0:
dependencies:
any-promise: 1.3.0
object-assign: 4.1.1
thenify-all: 1.6.0
nanoid@3.3.8: {}
normalize-path@3.0.0: {}
object-assign@4.1.1: {}
object-hash@3.0.0: {}
package-json-from-dist@1.0.1: {}
path-key@3.1.1: {}
path-parse@1.0.7: {}
path-scurry@1.11.1:
dependencies:
lru-cache: 10.4.3
minipass: 7.1.2
picocolors@1.1.1: {}
picomatch@2.3.1: {}
pify@2.3.0: {}
pirates@4.0.6: {}
postcss-import@15.1.0(postcss@8.4.49):
dependencies:
postcss: 8.4.49
postcss-value-parser: 4.2.0
read-cache: 1.0.0
resolve: 1.22.9
postcss-js@4.0.1(postcss@8.4.49):
dependencies:
camelcase-css: 2.0.1
postcss: 8.4.49
postcss-load-config@4.0.2(postcss@8.4.49):
dependencies:
lilconfig: 3.1.3
yaml: 2.6.1
optionalDependencies:
postcss: 8.4.49
postcss-nested@6.2.0(postcss@8.4.49):
dependencies:
postcss: 8.4.49
postcss-selector-parser: 6.1.2
postcss-selector-parser@6.1.2:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
postcss-value-parser@4.2.0: {}
postcss@8.4.49:
dependencies:
nanoid: 3.3.8
picocolors: 1.1.1
source-map-js: 1.2.1
queue-microtask@1.2.3: {}
read-cache@1.0.0:
dependencies:
pify: 2.3.0
readdirp@3.6.0:
dependencies:
picomatch: 2.3.1
resolve@1.22.9:
dependencies:
is-core-module: 2.16.0
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
reusify@1.0.4: {}
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
shebang-regex@3.0.0: {}
signal-exit@4.1.0: {}
source-map-js@1.2.1: {}
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
string-width@5.1.2:
dependencies:
eastasianwidth: 0.2.0
emoji-regex: 9.2.2
strip-ansi: 7.1.0
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
strip-ansi@7.1.0:
dependencies:
ansi-regex: 6.1.0
sucrase@3.35.0:
dependencies:
'@jridgewell/gen-mapping': 0.3.8
commander: 4.1.1
glob: 10.4.5
lines-and-columns: 1.2.4
mz: 2.7.0
pirates: 4.0.6
ts-interface-checker: 0.1.13
supports-preserve-symlinks-flag@1.0.0: {}
tailwindcss@3.4.16:
dependencies:
'@alloc/quick-lru': 5.2.0
arg: 5.0.2
chokidar: 3.6.0
didyoumean: 1.2.2
dlv: 1.1.3
fast-glob: 3.3.2
glob-parent: 6.0.2
is-glob: 4.0.3
jiti: 1.21.6
lilconfig: 3.1.3
micromatch: 4.0.8
normalize-path: 3.0.0
object-hash: 3.0.0
picocolors: 1.1.1
postcss: 8.4.49
postcss-import: 15.1.0(postcss@8.4.49)
postcss-js: 4.0.1(postcss@8.4.49)
postcss-load-config: 4.0.2(postcss@8.4.49)
postcss-nested: 6.2.0(postcss@8.4.49)
postcss-selector-parser: 6.1.2
resolve: 1.22.9
sucrase: 3.35.0
transitivePeerDependencies:
- ts-node
thenify-all@1.6.0:
dependencies:
thenify: 3.3.1
thenify@3.3.1:
dependencies:
any-promise: 1.3.0
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
ts-interface-checker@0.1.13: {}
util-deprecate@1.0.2: {}
which@2.0.2:
dependencies:
isexe: 2.0.0
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi@8.1.0:
dependencies:
ansi-styles: 6.2.1
string-width: 5.1.2
strip-ansi: 7.1.0
yaml@2.6.1: {}

View 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} ");
}
```

View File

@@ -0,0 +1,89 @@
---
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本其中不乏超过一千页的大部头说能够一年看完显然是痴人说梦。这里先列两本同我的工作关系密切的书籍
- 陆风,《光变:一个企业及其工业史》
- 吴军,《浪潮之巅》
其次是补番计划,这一年刷到了不少押井守导演的《机动警察》系列,虽然我之前对于人形机器人并不热心,但剧中精细的作画和宏大的背景设定确实非常吸引人,遂决定今年找来看看。

BIN
YaeBlog/source/posts/2024-final/image-20250115171809775.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,875 @@
---
title: 异构编程模型的昨天、今天与明天
date: 2024-11-04T22:20:41.2571467+08:00
tags:
- 编译原理
- 组会汇报
---
随着摩尔定律的逐渐失效将CPU和其他架构的计算设备集成在片上或者通过高速总线互联构建的异构系统成为了高性能计算的主流。但是在这种系统中上层应用的设计与实现面临着异构系统中各个设备之间体系结构差异过大、缺乏良好的异构抽象以及统一的编程接口和应用程序的优化难度大等困难。
异构并行编程模型便是解决这些编程和执行效率问题的解决方案。
<!--more-->
## 异构并行编程模型概述
异构并行编程模型是沟通上层应用和下层异构系统之间的桥梁,其的设计需要处理好下面五个问题:任务划分、任务映射、数据分布、同步和通信。
### 异构并行编程模型面临的技术挑战
异构并行编程模型面临的技术挑战主要是由两方面带来的:首先异构架构本身为编程模型带来的挑战,其次是上层应用带来的挑战。
异构并行编程模型需要解决的一个重要问题就是为上层应用的程序员提供一个合理的硬件平台抽象,使得其在编程是可以充分释放异构资源带来的计算能力,同时不需要考虑复杂的硬件细节。但是异构系统中各个计算设备在内部体系结构、设备间互联架构上的复杂性和多样性使得异构并行编程模型在提供建立统一的平台抽象上遇到了巨大的困难。具体来说,主要体现下述三点。
首先是异构系统中各个设备之间的并行计算能力不同。在同构的并行计算系统中比如多核CPU中虽然同一CPU的不同核之间、同一核的不同SIMD部件之间可以承担不同粒度的并行计算任务但是其并行计算的能力是完全相同的。但是在一个典型的异构计算系统例如CPU、GPU和FPGA组成的异构系统不同设备的微架构具有本质差异其并行计算的模式和能力都完全不同设备之间的特长也完全不同。这种设备之间并行计算能力的差异使得系统中的任务划分和任务映射不再是均一的而是具有显著的特异性。这种特点虽然也有利于表达实际应用的特点但是却给异构并行计算模型的设计带来了巨大的困难。
![9eb06d8be92ddef3db33e040163c67a7.png](./heterogeneous-programming-model/9eb06d8be92ddef3db33e040163c67a7.png)
其次是异构系统中加速设备数据分布可配置、设备间数据通信渠道多样性给数据分布和通信带来的困难。在同构并行系统中CPU片内的存储是对于软件透明的缓存架构在片外则是一个共享内存模型因此在这类系统中数据仅可能分布在片外的共享存储中具有存储位置单一的特点也不需要进行显式的通信操作。但是在异构系统中不仅在单个加速设备内部可能有软件可分配的快速局部存储设备之间的连接方式差异也很大。目前大多个加速设备都是通过PCIe总线的方式同CPU进行连接这使得加速设备无法通过和CPU相同的方式完成地址映射存在某一设备无法访问另一设备片外存储的问题。这使得异构系统中数据可以分布在CPU、加速设备的片外存储和加速设备的片内多层次局部存储等多个位置不仅使得编程模型的数据分布问题变得十分复杂设备间的通信文件也可能需要显式进行。
![eab553f9e30d8d866a1ddd201b5e4c85.png](./heterogeneous-programming-model/eab553f9e30d8d866a1ddd201b5e4c85.png)
最后是异构系统中多层次数据共享和多范围同步操作带来的同步困难问题。这也可以认为是上个数据同步问题带来的后继问题在异构系统中数据可能分布在不同位置的条件下同步操作需要在众多的位置上保证共享数据的一致性这使得同步操作的范围变得十分复杂。同时在一些特定的加速设备中例如GPU可能还会有局部的硬件同步机制这更加提高了在异构系统的同步操作的设计和实现难度。
上层应用带来的挑战主要集中在缺少良好的异构抽象和统一的编程接口上。例如在CPU上进行编程时通常使用Java、Python等高级语言而在进行GPU编程时则使用各种C语言的变体其中的核心计算函数Kernel Function则通常只支持一个C语言的子集而FPGA这些硬件设备又需要使用硬件描述语言进行编程。
### 异构并行编程接口和编译/运行时支持机制
异构并行编程接口是编程模型暴露给程序员使用的界面,它既需要为程序员提供合理的异构架构抽象,使程序员可以对异构计算资源加以合理利用,又需要保证接口的易用性,避免程序员陷入复杂的硬件细节中。编译/运行时系统是异构并行编程模型的软件工具层,它将程序员编写的加速器代码编译为可执行文件,并通过运行时系统完成任务的加速执行。
在任务划分、任务映射、数据分布、通信和同步这五个关键任务中,程序员往往只需要关注所编写应用程序的特点,因此显示的任务划分机制对应程序员来说可能是必不可少的,而其他的数据分布、通信和同步等任务只会加剧程序员开发应用程序的负担,但是这些任务通过接口暴露出来也为后续进行深度优化提供了空间。异构编译/运行时支持机制的主要任务就是保障任务映射,即明确任务将具体在哪个设备或者计算单元上执行,以何种顺序执行,同时在当程序员没有显式处理数据分布、通信和同步问题时进行自动处理并进行全系统级别的优化。
## 异构并行编程接口的研究
异构并行编程接口一般可以划分成两类新设计的异构编程语言和现有语言的异构并行扩展。对于现有语言进行的异构并行扩展一般通过库Library或者是制导Directive的方法进行。
从异构并行编程接口的功能角度上来说也可以分成两类:有些接口屏蔽了较多的异构并行编程细节,通常仅给程序员提供显式异构任务划分的机制,而数据分布和通信、同步等的工作由运行时系统负责完成,也有些接口将多数异构系统的硬件细节通过上述机制暴露给程序员使用,这在给编程带来更大自由度的同时带来了使用上的困难。
![83ee1d254d638536d0fb4197ff63e758.png](./heterogeneous-programming-model/83ee1d254d638536d0fb4197ff63e758.png)
### 异构任务划分机制研究
在同构的并行编程语言中,并行编程接口需要提供一种面向单一设备的并行任务划分机制,这种并行任务划分机制有**任务并行**和**数据并行**等。数据并行指的是对源集合或者数组的元素同时执行相同操作的场景,一个数据并行的典型例子如下面计算两个矩阵的乘积:
```csharp
static void MultiplyMatricesParallel(double[,] matA, double[,] matB, double[,] result)
{
int matACols = matA.GetLength(1);
int matBCols = matB.GetLength(1);
int matARows = matA.GetLength(0);
// A basic matrix multiplication.
// Parallelize the outer loop to partition the source array by rows.
Parallel.For(0, matARows, i =>
{
for (int j = 0; j < matBCols; j++)
{
double temp = 0;
for (int k = 0; k < matACols; k++)
{
temp += matA[i, k] * matB[k, j];
}
result[i, j] = temp;
}
}); // Parallel.For
}
```
任务并行的概念一般是指一个或者多个独立的任务同时运行,是一种比数据并行更高的抽象层级。
```csharp
public class Result
{
public static void Main()
{
Task<Double>[] taskArray = { Task<Double>.Factory.StartNew(() => DoComputation(1.0)),
Task<Double>.Factory.StartNew(() => DoComputation(100.0)),
Task<Double>.Factory.StartNew(() => DoComputation(1000.0)) };
var results = new Double[taskArray.Length];
Double sum = 0;
for (int i = 0; i < taskArray.Length; i++) {
results[i] = taskArray[i].Result;
Console.Write("{0:N1} {1}", results[i],
i == taskArray.Length - 1 ? "= " : "+ ");
sum += results[i];
}
Console.WriteLine("{0:N1}", sum);
}
private static Double DoComputation(Double start)
{
Double sum = 0;
for (var value = start; value <= start + 10; value += .1)
sum += value;
return sum;
}
}
```
不论是高级或者是低级的异构并行编程接口都需要提供一种异构并行任务的划分机制。同传统的同构并行编程接口只需要提供面向单一设备的并行任务划分机制不同,异构并行编程接口还需要提供描述任务在不同设备间分配的机制。因此,异构并行编程接口的任务划分机制需要包括两个维度:异构特征描述和并行性表达两个维度。
一种典型异构任务划分机制是由`BrookGPU`编程语言提出的。该编程语言采用特殊函数`kernel`标记需要在GPU上执行的代码段`kernel`函数必须作用在流上。这个流Stream在并行性表达方面表达了细粒度的数据并行。后面的OpenCL和CUDA在C语言的基础上提供了异构扩展这种扩展的任务划分机制和`BrookGPU`的十分类似。但是OpenCL和CUDA在并行行表达的层面上支持了SPMD计算模型这个`BrookGPU`编程语言采用的流式编程模型不同。OpenCL在数据并行之外还提供了任务并行的机制。
`Lime`则是一门完全新的异构并行编程语言,通过语言结构为程序提供了丰富的操作符用于任务的划分。同时在异构特征描述方面,`Lime`也没有任何显式的接口,同`BrookGPU`等一系列需要手动指定设备代码段的编程模型完全不同,这也是因为`Lime`采用了基于任务的并行划分方式。同时在任务并行之外,`Lime`也通过`MapReduce`操作符提供了中粒度的数据并行机制。
`Merge`还是一门新的异构并行编程语言基于Intel提出的异构多核多线程系统编程环境`EXOCHI`。在并行性表达上,`Merge`使用`MapReduce`思想。而在异构特征描述方面,`Merge`则提供了一种成为平台变体Target Variant的机制程序员需要为异构系统中的不同设备提供不同版本的代码实现。
### 异构数据分布和通信机制
异构数据分布和通信机制主要分成显式和隐式两种,其中`OpenCL/CUDA`等使用了显式的数据分布的通信机制,为程序员提供了丰富的异构数据分布与通信接口。而`Lime``Merge`等语言则使用了隐式机制,运行时系统代为完成这部分的工作。
采用显示异步数据分布和通信机制的主要问题是普通程序员一般无法充分利用这些接口获得性能上的提升。这通常使用因为加速设备通常采用了大量的硬件加速机制例如GPU的全局内存访存合并机制这使得程序员如果没有为数据分配合理的存储位置或者设定足够多的线程会使得加速的效果大打折扣。因此出现了针对这类显式控制语言的优化方法例如`CUDA-lite`这个运行时允许程序元在CUDA程序中加入简单的制导语句数据分布的相关工作使用`CUDA-lite`的运行时系统完成降低了CUDA程序的编写难度。
![628804b3fe95d39013ff55ae84516d14.png](./heterogeneous-programming-model/Screenshot_20241016_214139.png)
总结一下,为了解决异构系统带来的问题,异构并行编程接口具有如下三个特点:
- 异构任务划分机制在传统并行编程模型的基础上增加了"异构特征描述"的维度,用于描述任务在不同设备上的分配情况;
- 异构数据分布和通知机制在传统并行编程模型的基础上增加了"设备内数据多层分布"和"设备间显式通信"接口;
- 异构同步机制在传统并行编程模型的基础上增加了"设备间同步"的机制。
## 异步编译/运行时的研究
### 异构任务映射机制
异构编程/运行时系统的任务映射机制主要有两种:一类是直接映射,即独立完成并行任务向异构平台映射的工作,另一种是间接映射,即需要借助其他异构编译和运行时系统协助来完成部分任务映射工作。直接映射系统一般在运行时系统中实现,而间接映射通过源到源变换和是运行时分析相结合的方式实现。
![](./heterogeneous-programming-model/Screenshot_20241016_214939.png)
### 异构编译/运行时优化
与同构平台类似,异构编译/运行时优化有两条路径:
- 平台相关的优化,其核心是挖掘系统的硬件优势;
- 应用导向的优化,其核心是实施特定领域的优化并解决应用的输入敏感问题。
在平台优化上,异构系统通常具有复杂且多变的硬件结构, 因此程序员仅负责编写正确实现程序功能的代码、由编译/运行时系统完成面向加速设备结构特点的优化是比较合理的方式, 这样也有利于程序在不同异构系统中获得良好的性能。
## 异构并行编程模型的研究方向
- 面向普通用户的异构并行编程接口
- 面向多种加速设备的异构编译/运行时优化
- 面向异构集群的异构并行编程模型
## 异构并行编程模型调研
为了调研各个异构并行编程模型的不同,使用不同的编程模型实现一个通用矩阵乘法算法,并通过计算`2048*2048`大小的矩阵乘法时间来比较各个模型的加速效果。
辅助计算的`Calculator`类如下所示:
```cpp
#define MATRIX_SIZE 2048
#include <chrono>
#include <functional>
#include <iostream>
#include <random>
class Calculator
{
public:
static void validate_matrix(const std::vector<std::vector<int>>& a, const std::vector<std::vector<int>>& b)
{
for (int i = 0; i < MATRIX_SIZE; i++)
{
for (int j = 0; j < MATRIX_SIZE; j++)
{
if (a[i][j] != b[i][j])
{
std::cout << "Two matrix must be the same." << std::endl;
}
}
}
}
std::vector<std::vector<int>> calculate(const std::string& method,
const std::function<std::vector<std::vector<int>>(
const std::vector<std::vector<int>>&,
const std::vector<std::vector<int>>&)>& calculator) const
{
std::cout << "Calculator '" << method << "' start." << std::endl;
const auto start_time = std::chrono::high_resolution_clock::now();
const auto result = calculator(a, b);
const auto end_time = std::chrono::high_resolution_clock::now();
const auto span = end_time - start_time;
std::cout << "Calculator '" << method << "' end, time is " << std::chrono::duration_cast<
std::chrono::milliseconds>(span).count() << " ms." << std::endl;
return result;
}
private:
std::vector<std::vector<int>> a = initialize_matrix();
std::vector<std::vector<int>> b = initialize_matrix();
static std::vector<std::vector<int>> initialize_matrix()
{
std::vector<std::vector<int>> matrix;
std::random_device seed;
std::ranlux48 engine(seed());
std::uniform_int_distribution distribute(0, 102400);
for (int i = 0; i < MATRIX_SIZE; i++)
{
std::vector row(MATRIX_SIZE, 0);
for (int j = 0; j < MATRIX_SIZE; j++)
{
row[j] = distribute(engine);
}
matrix.emplace_back(row);
}
return matrix;
}
}
```
作为对比一个使用CPU单线程计算的例子如下
```cpp
inline std::vector<int> cpuMatrixMultiply(
const std::vector<int>& a,
const std::vector<int>& b)
{
std::vector result(MATRIX_SIZE * MATRIX_SIZE, 0);
for (int i = 0; i < MATRIX_SIZE; i++)
{
for (int j = 0; j < MATRIX_SIZE; j++)
{
int temp = 0;
for (int k = 0; k < MATRIX_SIZE; k++)
{
// a[i][j] = a[i][k] * b[k][j] where k in (0..MATRIX_SIZE)
temp += a[i * MATRIX_SIZE + k] * b[k * MATRIX_SIZE + j];
}
result[i * MATRIX_SIZE + j] = temp;
}
}
return result;
}
```
### OpenMP
OpenMP是`Open MultiProcessing`的缩写是一个使用编译器制导Directives来进行共享内存平行计算的框架在C、C++和Fortran语言的并行编程中得到的了广泛的应用。OpenMP提供了一个简单而灵活的接口让程序员能够充分释放多核和多处理器系统性能。
OpenMP从上面的介绍来看似乎并不是一个严格的异步并行编程模型但是第一OpenMP作为一个经典的并行编程框架研究价值还是非常高的其次在一些较新的OpenMP版本中其宣称也能利用NVIDIA GPU进行加速似乎也能算是一个异构并行编程模型。
使用OpenMP进行并行加速的代码如下
```C++
std::vector<std::vector<int>> omp_matrix_multiply(
const std::vector<std::vector<int>>& a,
const std::vector<std::vector<int>>& b)
{
std::vector result(MATRIX_SIZE, std::vector(MATRIX_SIZE, 0));
#pragma omp parallel for shared(a, b, result) default(none)
for (int i = 0; i < MATRIX_SIZE; i++)
{
for (int j = 0; j < MATRIX_SIZE; j++)
{
int temp = 0;
for (int k = 0; k < MATRIX_SIZE; k++)
{
temp += a[i][k] * b[k][j];
}
result[i][j] = temp;
}
}
return result;
}
```
加速的结果如下:
| 运行方法 | 运行时间 | 比率 |
| ------------ | -------- | ---- |
| SingleThread | 21685 ms | 1.00 |
| OpenMP | 2268 ms | 0.10 |
### CUDA
CUDA是NVIDIA公司设计的一套GPU加速应用程序的编程框架是将NVIDIA GPU作为GPGPU使用的官方解决方案。
CUDA的异构编程接口是经典的Device-Host两元结构程序员需要编写两部分代码Device代码是实际运行在GPU上的逻辑部分而Host代码则负责将数据从内存中复制到GPU上的显存和复制回来等准备工作并负责以特定的参数调用GPU上的Device代码。
一个使用GPU的矩阵乘法程序如下所示
```c++
template <typename T>
void check(T result, char const* const func, const char* const file, int const line)
{
if (result)
{
std::cerr << "CUDA error at " << file << ":" << line << "code = " << result << "(" << cudaGetErrorString(result)
<< ") '" << func << "'" << std::endl;
exit(EXIT_FAILURE);
}
}
#define checkCudaErrors(val) check((val), #val, __FILE__, __LINE__)
__global__ void cudaMatrixMultiply(const int* a, const int* b, int* c)
{
const int totalSize = MATRIX_SIZE * MATRIX_SIZE;
int threadId = threadIdx.x + blockIdx.x * blockDim.x;
while (threadId < totalSize)
{
const int x = threadId / MATRIX_SIZE;
const int y = threadId % MATRIX_SIZE;
int result = 0;
for (int i = 0; i < MATRIX_SIZE; i++)
{
result += a[x * MATRIX_SIZE + i] * b[i * MATRIX_SIZE + y];
}
c[MATRIX_SIZE * x + y] = result;
threadId += gridDim.x * blockDim.x;
}
__syncthreads();
}
std::vector<std::vector<int>> cudaCalculateMatrix(const std::vector<std::vector<int>>& a,
const std::vector<std::vector<int>>& b)
{
constexpr unsigned int matrixSize = sizeof(int) * MATRIX_SIZE * MATRIX_SIZE;
// 在host上为a, b, c分配空间
int *hostA, *hostB, *hostC;
checkCudaErrors(cudaMallocHost(&hostA, matrixSize));
checkCudaErrors(cudaMallocHost(&hostB, matrixSize));
checkCudaErrors(cudaMallocHost(&hostC, matrixSize));
// 将数据复制到host上
for (int i = 0; i < MATRIX_SIZE; i++)
{
for (int j = 0; j < MATRIX_SIZE; j++)
{
hostA[i * MATRIX_SIZE + j] = a[i][j];
hostB[i * MATRIX_SIZE + j] = b[i][j];
}
}
// 在device上分配空间
int *deviceA, *deviceB, *deviceC;
checkCudaErrors(cudaMalloc(reinterpret_cast<void**>(&deviceA), matrixSize));
checkCudaErrors(cudaMalloc(reinterpret_cast<void**>(&deviceB), matrixSize));
checkCudaErrors(cudaMalloc(reinterpret_cast<void**>(&deviceC), matrixSize));
cudaStream_t stream;
checkCudaErrors(cudaStreamCreateWithFlags(&stream, cudaStreamNonBlocking));
// 将数据从host复制到device
checkCudaErrors(cudaMemcpyAsync(deviceA, hostA, matrixSize, cudaMemcpyHostToDevice, stream));
checkCudaErrors(cudaMemcpyAsync(deviceB, hostB, matrixSize, cudaMemcpyHostToDevice, stream));
constexpr int threadSize = 32 * 32;
constexpr int grid = MATRIX_SIZE * MATRIX_SIZE / threadSize;
cudaEvent_t start, stop;
cudaEventCreate(&start);
cudaEventCreate(&stop);
cudaStreamSynchronize(stream);
cudaEventRecord(start, stream);
cudaMatrixMultiply<<<grid, threadSize, 0, stream>>>(deviceA, deviceB, deviceC);
cudaEventRecord(stop, stream);
cudaEventSynchronize(stop);
float cudaRunTime = 0;
cudaEventElapsedTime(&cudaRunTime, start, stop);
std::cout << "CUDA actual run time is " << cudaRunTime << " ms" << std::endl;
// 将数据从device复制到host
cudaMemcpyAsync(hostC, deviceC, matrixSize, cudaMemcpyDeviceToHost, stream);
cudaStreamSynchronize(stream);
std::vector<std::vector<int>> result;
for (int i = 0; i < MATRIX_SIZE; i++)
{
std::vector<int> row;
for (int j = 0; j < MATRIX_SIZE; j++)
{
row.emplace_back(hostC[i * MATRIX_SIZE + j]);
}
result.emplace_back(row);
}
// 释放内存
cudaFreeHost(hostA);
cudaFreeHost(hostB);
cudaFreeHost(hostC);
cudaFree(deviceA);
cudaFree(deviceB);
cudaFree(deviceC);
cudaEventDestroy(start);
cudaEventDestroy(stop);
cudaStreamDestroy(stream);
return result;
}
```
加速的结果如下所示:
| 类型 | 运行时间 | 比率 |
| ---- | -------- | ----- |
| CPU | 22059ms | 1.000 |
| GPU | 32ms | 0.001 |
需要注意的是上面编写的CUDA代码还没有完全利用GPU的并行计算能力。
> 这里我遇到的一个非常奇怪的问题是相同的CPU计算代码在运行完OpenMP测试之后再运行就会比在CUDA运行之后再运行慢上一倍而且可复现性极高。这里我给出一个典型的运行时间比较CUDA计算的时间是323毫秒CUDA之后的CPU计算时间是38602毫秒OpenMP的计算时间是8721毫秒OpenMP之后的计算时间是76598毫秒。
>
> 针对这个比较奇怪的情况我觉得可以做出三个猜想:
>
> - 考虑到我使用的CPU是Intel的i7-13600K这是一个有性能核和效率核组成的大小核异构系统可能在两次计算的过程中调度到了不同的核上
> - 在进行CUDA计算的过程中提高了缓存的亲和性
> - 在测试中没有设计热身Warm up的过程而在CUDA计算的过程中部分起到了这个作用。
>
> 针对上面三个猜测做个两个实验:
>
> - 首先是换了一台没有大小核异构设计的计算机进行实验发现这下两次使用CPU计算的时间差异不大
> - 加上了热身的阶段之后,计算时间没有发生明显的变化。
>
> 综上所述可以认为此现象和异构CPU之间存在着明显的关联但是缺乏直接证据。
>
> 在我们调整了矩阵的数据布局之后这里提到的实验结果又发生了变化。上面的实验结果是使用二维数据存储矩阵得到的而在修改为使用一维数组也就是现在提供的代码之后相同的CPU计算代码的计算时间又没有产生明显的变化了。看来这个问题可能和数据布局、CPU缓存等问题相关。
### OpenCL
OpenCL是目前最为典型、发展最好的异构并行编程模型毕竟其在官网的第一句话就是“为异构系统中并行编程的开放标准“。
![image-20241020142938110](./heterogeneous-programming-model/image-20241020142938110.png)
从上图的OpenCL工作原理中可以看出OpenCL和CUDA类似也采用了Device-Host类型的编程接口。主机代码通常通过普通的C/C++代码进行编写编译之后在CPU上执行而设备代码使用一个特定的C语言方言OpenCL C进行编写这个方言针对并行编程进行了扩展并提供了一系列封装好的数学计算函数。
设备代码上的编译方法有两种在线编译和离线编译。其中在线编译就是指在程序运行时由对应设备厂商开发的OpenCL驱动将设备代码编译为在对应设备上运行的可执行代码离线编译则有两种表现形式第一种是在线编译的扩展版由驱动编译得到的可执行程序可以通过API获取并保存下来当下一需要在同一设备上调用时可以直接使用而不是再次编译第二种则是完全独立的编译过程在OpenCL程序运行之前使用单独的编译工具编译得到可执行文件。
![image-20241020155656219](./heterogeneous-programming-model/image-20241020155656219.png)
在提出离线编译之后为了让驱动编译好的二进制文件可以在不同的设备之间复用同时也是支持更为丰富的编译器生态系统OpenCL的提出者Khronos设计了一种跨设备的、可迁移的中间表示形式[SPIRV](https://www.khronos.org/spir/)。这种中间形式的提出使得编程语言的提出者、编译器的开发人员可以直接将语言编译为`SPIRV`内核,这样就可以在任何支持`SPIRV`的OpenCL驱动上运行。下面将会介绍的`SYCL`和`Julia`语言都是基于`SPIRV`的中间语言进行构建的。`SPIRV`中间语言的提出也扩展了可以支持`OpenCL`的设备范围,现在已经有开发者和公司在探索将`SPIRV`编译到`Vulkan`、`DirectX`和`Metal`等传统意义上的图形API。
下面是一个使用OpenCL进行矩阵计算的例子。
```cpp
struct ComputationContext
{
cl_platform_id platform;
cl_device_id device;
};
static std::unique_ptr<ComputationContext> selectDevice()
{
cl_uint platformCount;
checkOpenCLError(clGetPlatformIDs(0, nullptr, &platformCount));
std::cout << "Platform count: " << platformCount << std::endl;
std::vector<cl_platform_id> platforms(platformCount);
checkOpenCLError(clGetPlatformIDs(platformCount, platforms.data(), nullptr));
std::unique_ptr<ComputationContext> selectedDevice = nullptr;
for (const auto& platform : platforms)
{
cl_uint deviceCount = 0;
checkOpenCLError(clGetDeviceIDs(platform, CL_DEVICE_TYPE_ALL, 0, nullptr, &deviceCount));
std::vector<cl_device_id> devices(deviceCount);
checkOpenCLError(clGetDeviceIDs(platform, CL_DEVICE_TYPE_ALL, deviceCount, devices.data(), nullptr));
for (const auto& device : devices)
{
size_t deviceNameLength;
checkOpenCLError(clGetDeviceInfo(device, CL_DEVICE_NAME, 0, nullptr, &deviceNameLength));
std::vector<char> deviceNameArray(deviceNameLength);
checkOpenCLError(
clGetDeviceInfo(device, CL_DEVICE_NAME, deviceNameLength, deviceNameArray.data(), nullptr));
std::string deviceName(deviceNameArray.data(), deviceNameArray.size() - 1);
std::cout << "Found device: " << deviceName << std::endl;
if (deviceName.find("4060") != std::string::npos)
{
std::cout << "Select device '" << deviceName << "' as runner." << std::endl;
selectedDevice = std::make_unique<ComputationContext>();
selectedDevice->platform = platform;
selectedDevice->device = device;
}
else
{
clReleaseDevice(device);
}
}
}
if (selectedDevice == nullptr)
{
std::cout << "Failed to find the target device." << std::endl;
std::exit(EXIT_FAILURE);
}
return selectedDevice;
}
std::vector<int> clCalculateMatrix(const std::vector<int>& a,
const std::vector<int>& b)
{
cl_int error;
const std::unique_ptr<ComputationContext> computationContext = selectDevice();
// A key-value list ends with 0
// See also https://www.khronos.org/registry/OpenCL/specs/3.0-unified/html/OpenCL_API.html#context-properties-table
std::array<cl_context_properties, 3> properties = {
CL_CONTEXT_PLATFORM,
reinterpret_cast<cl_context_properties>(computationContext->platform),
0
};
cl_context context = clCreateContext(properties.data(), 1, &computationContext->device, nullptr, nullptr,
&error);
checkOpenCLError(error);
cl_command_queue queue = clCreateCommandQueueWithProperties(context, computationContext->device, nullptr,
&error);
checkOpenCLError(error);
std::vector result(MATRIX_SIZE * MATRIX_SIZE, 0);
constexpr size_t matrixSize = MATRIX_SIZE * MATRIX_SIZE * sizeof(int);
cl_mem deviceA = clCreateBuffer(context, CL_MEM_READ_ONLY, matrixSize, nullptr, &error);
checkOpenCLError(error);
cl_mem deviceB = clCreateBuffer(context, CL_MEM_READ_ONLY, matrixSize, nullptr, &error);
checkOpenCLError(error);
cl_mem deviceC = clCreateBuffer(context, CL_MEM_READ_WRITE, matrixSize, nullptr, &error);
checkOpenCLError(error);
checkOpenCLError(
clEnqueueWriteBuffer(queue, deviceA, CL_TRUE, 0, matrixSize, a.data(), 0, nullptr,
nullptr));
checkOpenCLError(
clEnqueueWriteBuffer(queue, deviceB, CL_TRUE, 0, matrixSize, b.data(), 0, nullptr,
nullptr));
// Copy result to erase the previous result
checkOpenCLError(
clEnqueueWriteBuffer(queue, deviceC, CL_TRUE, 0, matrixSize, result.data(), 0,
nullptr, nullptr
));
auto source = R"(
#define MATRIX_SIZE 2048
__kernel void calculate(const __global int* a, const __global int* b, __global int* c)
{
const int x = get_global_id(0);
const int y = get_global_id(1);
int result = 0;
for (int i = 0; i < MATRIX_SIZE; i++)
{
result += a[x * MATRIX_SIZE + i] * b[i * MATRIX_SIZE + y];
}
c[x * MATRIX_SIZE + y] = result;
})";
cl_program program = clCreateProgramWithSource(context, 1, &source, nullptr, &error);
checkOpenCLError(error);
checkOpenCLError(clBuildProgram(program, 0, nullptr, "", nullptr, nullptr));
size_t messageSize;
checkOpenCLError(
clGetProgramBuildInfo(program, computationContext->device, CL_PROGRAM_BUILD_LOG, 0, nullptr, &messageSize));
std::vector<char> messageArray(messageSize);
checkOpenCLError(
clGetProgramBuildInfo(program, computationContext->device, CL_PROGRAM_BUILD_LOG, messageSize, messageArray.data(
), nullptr));
std::string message(messageArray.data(), messageSize - 1);
std::cout << "Build log: " << message << std::endl;
cl_kernel kernel = clCreateKernel(program, "calculate", &error);
checkOpenCLError(error);
checkOpenCLError(clSetKernelArg(kernel, 0, sizeof(cl_mem), &deviceA));
checkOpenCLError(clSetKernelArg(kernel, 1, sizeof(cl_mem), &deviceB));
checkOpenCLError(clSetKernelArg(kernel, 2, sizeof(cl_mem), &deviceC));
cl_event event;
constexpr std::size_t globalSize[2] = {MATRIX_SIZE, MATRIX_SIZE};
checkOpenCLError(clEnqueueNDRangeKernel(queue, kernel, 2, nullptr,
globalSize, nullptr, 0, nullptr, &event));
checkOpenCLError(clWaitForEvents(1, &event));
checkOpenCLError(
clEnqueueReadBuffer(queue, deviceC, CL_TRUE, 0, matrixSize, result.data(), 0,
nullptr, nullptr));
clReleaseMemObject(deviceA);
clReleaseMemObject(deviceB);
clReleaseMemObject(deviceC);
clReleaseKernel(kernel);
clReleaseProgram(program);
clReleaseCommandQueue(queue);
clReleaseContext(context);
clReleaseDevice(computationContext->device);
return result;
}
```
从上面的代码中可以看出两点:
- OpenCL的编程比CUDA的更为繁琐因为OpenCL支持的设备种类更多在主机代码上还需要多出一块选择运行设备的代码
- OpenCL在主机代码和核函数的解耦更为彻底核函数直接以字符串的形式存在于主机代码中而各个厂商提供的驱动才是真正的编译器。
测试的运行结果如下:
| 类型 | 运行时间 | 比率 |
| ----------------------------- | -------- | ---- |
| NVIDIA 4060 Ti OpenCL | 173ms | 0.01 |
| Intel UHD Graphics 770 OpenCL | 1020ms | 0.04 |
| CPU | 21255ms | 1.00 |
### SYCL
SYCL是一个使用标准C++编写在各种异构计算设备上运行核函数的抽象层并提供了一套新的API来查找各种设备并管理这些设备上的内存资源和代码执行。这个标准是开发、无版税、跨平台的抽象标准。同时也是因为这是一个**标准**因此需要寻找支持这个标准的编译器才能使用这个标准。按照官网上的说明我们选择了两个看上去还在活跃开发的项目Intel的[oneAPI](https://www.intel.com/content/www/us/en/developer/tools/oneapi/overview.html)和开源的[AdaptiveCpp](https://github.com/AdaptiveCpp/AdaptiveCpp)进行调研考虑到在后文中还将继续介绍oneAPI相关的工作因此这里将重点放在AdaptiveCpp上。
AdaptiveCpp由四个部分组成分别在不同的C++命名空间中提供。
- SYCL Interface实现了SYCL标准中规定的各种类和函数是实际上同用户交互的接口。这些接口实际上可以仍然可以分成主机API和核函数库两个部分。主机API是普通的C++代码负责任务调度、任务管理和平台射别管理等。核函数库包括了这种在编写核函数时可以使用的类和函数这些接口暴露一些后端特定的功能其中的一些甚至需要使用后端特定的方言来编写例如CUDA。
- AdaptiveCpp Runtime运行时实际上实现了设备调度、任务图管理和执行、数据管理、后端管理、任务调度和同步等等功能运行时负责同各种支持后端的运行时交互来实现上述的功能。
![image-20241029123308139](./heterogeneous-programming-model/image-20241029123308139.png)
- Compiler考虑到在用户编写的代码中可能使用一些特定后端的方言因此普通的C++编译器无法正常编译所有的用户代码。因此用户代码的编译是通过一个名为`acpp`的Python脚本驱动的这个脚本将各个后端的不同编译器暴露为一个统一的编程接口。
- Glue将上述的各个部分连接在一起的胶水代码。一种典型的胶水代码是内核函数的启动代码`kernel launcher`由于启动器中往往涉及到一些后端特定的方言例如CUDA中的`<<<>>>`或者OpenMP中的各种`pragma`因此这些代码通常需要使用特定的编译器进行编译所以这些胶水代码直接以头文件的方式提供以方便在编译时被特定的编译器处理。这些胶水代码将会把核函数包裹为一个合法的C++函数对象,这样运行时就可以获得这个函数对象并控制代码在设备上的运行。
AdaptiveCpp同时支持多种不同的编译流程。
1. 一种通用的一遍编译流程,将核函数编译到一种统一的中间表示形式,这种中间表示形式将在运行时被编译到特定的后端架构上。这种编译流程提供了高度的可移植性和较快的编译速度。这种编译设施支持的后端有:通过`PTX`在NVIDIA的GPU上运行通过`amdgcn`在AMD的GPU上运行通过`SPIR-V`在Intel的GPU上运行通过`SPIR-V`在任何支持OpenCL驱动的设备上运行也可以通过LLVM直接在CPU上运行。
2. 一种为互操作性优化的多遍编译流程在这个流程中AdaptiveCpp将聚合现有的各种LLVM/Clang的编译工具链使得用户可以在单个代码文件中混合编写SYCL和各种特定的编程模型例如CUDA和HIP。使用这个编译流程的好处有亮点1在这种编译流程中可以直接在SYCL代码使用各个特定编译模型中提供最新设备内部优化Intrinsics不用等待SYCL标准的支持2在这种编译流程中可以使用各个厂商提供的优化模板库例如`rocPRIM`和`CUB`。这种编译流程是提供聚合`CUDA`的clang前端和`ROCm`的clang前端来实现的。
3. 一种只将AdaptiveCpp作为函数使用的编程流程。在这种情况AdaptiveCpp作为一个三方库被引入其他的编译器编译流程中。
第一种通用的编译流程显然是泛用性最广的一种编译流程同时也是AdaptiveCpp推荐的编译流程。
![image-20241029163654675](./heterogeneous-programming-model/image-20241029163654675.png)
下面是一段使用SYCL进行矩阵乘法加速的代码
```cpp
struct CustomDeviceSelector
{
explicit CustomDeviceSelector(std::string vendorName) : _vendorName(std::move(vendorName))
{
}
int operator()(const sycl::device& d) const
{
int deviceRating = 0;
if (d.is_gpu() && d.get_info<sycl::info::device::name>().find(_vendorName) != std::string::npos)
{
deviceRating = 3;
}
else if (d.is_cpu())
{
deviceRating = 1;
}
return deviceRating;
}
private:
std::string _vendorName;
};
static std::vector<int> syclCalculateMatrix(const std::vector<int>& a, const std::vector<int>& b,
const std::string& hint)
{
const CustomDeviceSelector selector(hint);
sycl::queue queue(selector);
const std::string deviceName = queue.get_device().get_info<sycl::info::device::name>();
std::cout << "Select device: " << deviceName << std::endl;
std::vector result(MATRIX_SIZE * MATRIX_SIZE, 0);
sycl::buffer aBuffer(a);
sycl::buffer bBuffer(b);
sycl::buffer resultBuffer(result);
queue.submit([&](sycl::handler& h)
{
const sycl::accessor aBufferAccessor(aBuffer, h, sycl::read_only);
const sycl::accessor bBufferAccessor(bBuffer, h, sycl::read_only);
const sycl::accessor resultBufferAccessor(resultBuffer, h, sycl::write_only);
h.parallel_for(sycl::nd_range<2>({MATRIX_SIZE, MATRIX_SIZE}, {16, 16}), [=](const sycl::nd_item<2>& item)
{
const size_t x = item.get_global_id(0);
const size_t y = item.get_global_id(1);
int temp = 0;
for (size_t k = 0; k < MATRIX_SIZE; ++k)
{
temp += aBufferAccessor[x * MATRIX_SIZE + k] * bBufferAccessor[k * MATRIX_SIZE + y];
}
resultBufferAccessor[x * MATRIX_SIZE + y] = temp;
});
});
sycl::host_accessor resultHostAccessor(resultBuffer, sycl::read_only);
for (size_t i = 0; i < MATRIX_SIZE; ++i)
{
for (size_t j = 0; j < MATRIX_SIZE; ++j)
{
result[i * MATRIX_SIZE + j] = resultHostAccessor[i * MATRIX_SIZE + j];
}
}
return result;
}
```
测试之后的运行结果如下所示:
| 类型 | 运行时间 | 比率 |
| --------------------------- | -------- | ----- |
| Intel UHD Graphics 770 SYCL | 488ms | 0.023 |
| NVIDIA 4060 Ti SYCL | 180ms | 0.008 |
| OpenMP SYCL | 1591ms | 0.076 |
| CPU | 20930ms | 1.000 |
### OpenACC
OpenACC是一个通过编译器制导来在代码中表达并行性并利用并行编译器为多个并行加速器生成代码的编程模型。为了保证OpenACC可以适配于各种计算架构的加速设备OpenACC设计了一个各种并行层次和有着不同速度和寻址方式内存的编程模型。同时OpenACC主要的功能即是支持同时将计算和数据卸载到一个加速设备上考虑到加速设备可能有着同宿主设备完全不同的内存架构OpenACC编译器和运行时将会自动分析代码并负责加速器上内存的管理和加速器和主机之间的数据传输。
作为一个高等级、平台独立的加速器编程框架使用OpenACC进行开发能够使开发人员将一个源代码编译到一系列设备上运行并实现一个相对较好的性能但是这个简易性和移植性也在一定程度上造成使用OpenACC编程无法完全利用加速设备上的算力。
OpenACC是作为一个标准的形式提供的实现了该标准的编译器有
| 编译器名称 | 情况 |
| ------------------------------------------------------------ | ------------------------------------------------------------ |
| NVIDIA HPC SDK | 支持在NVIDIA GPU和多核CPU上的OpenACC并行编程 |
| Sourcery CodeBench Lite | OpenACC官网上说支持针对AMD GPU的编译但是官网页面似乎改版了没有找到相关的内容 |
| GCC 12 | 支持到OpenACC 2.6 |
| [Omni Compiler Project](https://github.com/omni-compiler/omni-compiler) | 源到源编译器,将带有制导的源代码翻译到带有运行时调用的平台代码,近两年没有活跃开发 |
| [OpenUH](https://github.com/uhhpctools/openuh) | 项目开发者在7年前的最后一次提交了中删除了README中有关OpenACC的内容 |
| [OpenArc](https://csmd.ornl.gov/project/openarc-open-accelerator-research-compiler) | 是学术界出品的还在活跃开发的编译器看上去还做了不少工作的样子就是OpenACC官网上的链接已经失效了找起来比较麻烦而且宣称是一个开源编译器但是获取源代码和二进制文件需要联系他们美国橡树岭国家实验室创建账户这看去对于我们这些Foreign Adversary有些抽象了。 |
在试验OpenACC时遇到了巨大的困难不论是使用gcc还是NVIDIA HPC SDK都没有办法实现明显的并行编程加速多次实验之后都没有找到的问题的所在。这里还是贴一下实验的代码和实验的数据。
实验中编写的OpenACC加速代码如下
```cpp
static std::vector<int> OpenACCCpuCalculateMatrix(const std::vector<int>& a, const std::vector<int>& b)
{
constexpr int length = MATRIX_SIZE * MATRIX_SIZE;
const auto aBuffer = new int[length];
const auto bBuffer = new int[length];
const auto cBuffer = new int[length];
for (int i = 0; i < length; i++)
{
aBuffer[i] = a[i];
bBuffer[i] = b[i];
cBuffer[i] = 0;
}
#pragma acc enter data copyin(aBuffer[0:length], bBuffer[0:length])
#pragma acc enter data create(bBuffer[0:length])
#pragma acc data present(aBuffer[0:length], bBuffer[0:length], cBuffer[0:length])
{
#pragma acc kernels loop independent
for (int i = 0; i < MATRIX_SIZE; i++)
{
#pragma acc loop independent
for (int j = 0; j < MATRIX_SIZE; j++)
{
int temp = 0;
#pragma acc loop independent reduction(+:temp)
for (int k = 0; k < MATRIX_SIZE; k++)
{
temp += aBuffer[i * MATRIX_SIZE + k] * bBuffer[k * MATRIX_SIZE + j];
}
cBuffer[i * MATRIX_SIZE + j] = temp;
}
}
}
#pragma acc exit data copyout(cBuffer[0:length])
#pragma acc exit data delete(aBuffer[0:length], bBuffer[0:length])
std::vector result(MATRIX_SIZE * MATRIX_SIZE, 0);
for (int i = 0; i < length; ++i)
{
result[i] = cBuffer[i];
}
delete[] aBuffer;
delete[] bBuffer;
delete[] cBuffer;
return result;
}
```
实验中使用分别使用`NVIDIA HPC SDK`和`GCC`编译运行的结果如下:
| 编译器 | 类型 | 运行时间 |
| -------------- | ------- | -------- |
| NVIDIA HPC SDK | OpenACC | 19315ms |
| NVIDIA HPC SDK | CPU | 22942ms |
| GCC | OpenACC | 19999ms |
| GCC | CPU | 22623ms |
### oneAPI
oneAPI是Intel公司提出的一套异构并行编程框架该框架致力于达成如下几个目标1定义一个跨架构、跨制造商的统一开放软件平台2允许同一套代码可以在不同硬件制造商和加速技术的硬件上运行3提供一套覆盖多个编程领域的库API。为了实现这些目标oneAPI同上文中已经提到过的开放编程标准SYCL紧密合作oneAPI也提供了一个SYCL的编译器和运行时同时oneAPI也提供了一系列API库包括`oneDPL`、`oneDNN`、`oneTBB`和`oneMKL`等。
![image-20241103162259981](./heterogeneous-programming-model/image-20241103162259981.png)
我对于oneAPI的理解就是Intel用来对标NVIDIA的CUDA的一套高性能编程工具箱。首先为了和NVIDIA完全闭源的CUDA形成鲜明的对比Intel选择了OpenCL合作同时开发SYCL当时也有可能是Intel知道自己的显卡技不如人如果不兼容市面上其他的部件是没有出路的同时为了和CUDA丰富的生态竞争Intel再开发并开源了一系列的`oneXXX`。
这里我就把上面SYCL写的例子用Intel提供的`DPC++`编译运行一下,看看在效率上会不会有所变化。
| 类型 | 运行时间 | 比率 |
| ----------------------------- | -------- | ----- |
| Intel UHD Graphics 770 oneAPI | 429ms | 0.023 |
| NVIDIA 4060 Ti oneAPI | 191ms | 0.010 |
| Intel i5-13600K oneAPI | 198ms | 0.011 |
| CPU | 18643ms | 1.000 |
在显卡上的计算时间没有明显的变化但是我们Intel的编译器却在选择到使用Intel CPU进行计算时展现了不俗的实力。
## 参考文献
1. 刘颖,吕方,王蕾,陈莉,崔慧敏,冯晓兵.异构并行编程模型研究与进展.软件学报,2014,25(7):1459-1475. [http://www.jos.org.cn/1000-9825/4608.htm](http://www.jos.org.cn/1000-9825/4608.htm)
2. AdaptiveCpp官方文档. [https://adaptivecpp.github.io/AdaptiveCpp/](https://adaptivecpp.github.io/AdaptiveCpp/)
3. Exploring the performance of SGEMM in OpenCL on NVIDIA GPUs. [https://github.com/CNugteren/myGEMM](https://github.com/CNugteren/myGEMM)
4. OpenACC Programming and Best Practices Guide. [https://openacc-best-practices-guide.readthedocs.io/en/latest/01-Introduction.html](https://openacc-best-practices-guide.readthedocs.io/en/latest/01-Introduction.html)
5. oneAPI What is it?. [https://www.intel.com/content/www/us/en/developer/articles/technical/oneapi-what-is-it.html](https://www.intel.com/content/www/us/en/developer/articles/technical/oneapi-what-is-it.html)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,65 @@
---
title: 内存栈被Rust自动生成的Drop函数塞满了
date: 2024-11-05T20:36:07.3930374+08:00
tags:
- Rust
- 技术笔记
---
这辈子就是被Rust编译器害了.jpg
<!--more-->
最近在用Rust写一个[Sysy](https://gitlab.eduxiji.net/csc1/nscscc/compiler2022/-/blob/master/SysY2022%E8%AF%AD%E8%A8%80%E5%AE%9A%E4%B9%89-V1.pdf)语言的编译器,但是在实现完语法分析之后针对官方提供的测试用例进行测试时遇到的一个抽象的栈溢出报错。
事情是这样的,当我实现完`Sysy`语言的语法分析器并编写了一些白盒测试用例之后我便打算将官方提供的100个测试用例作为输入运行看看能不能**正常**的解析成抽象语法树(显然不可能手动检查生成的抽象语法树是否正确)。我首先在`main.rs`里面实现了读取所有的`.sy`文件进行词法分析和语法分析的逻辑程序在这里这正常的识别了大多数的输入文件在一些浮点数的输入上还存在一些问题。于是我便打算将这些逻辑重构到一个Rust的集成测试中方便在CI中使用`cargo test`进行运行测试。但是在重构完成之后使用`cargo test`进行运行时我去遇到了如下的运行时错误。
![image-20241105181144993](./rust-drop-stack-overflow/image-20241105181144993.png)
看到这个报错的第一瞬间,我怀疑是因为`cargo test``cargo run`的运行环境不同,导致测试程序读取到了其他其实不是`sysy`程序但是以`.sy`结尾的文件,而恰好这个文件又能被解析,使得解析器组合子工作的过程中调用链太长而导致栈溢出,于是我在`RustRover`中打断点调试运行,却发现程序正确的读取到输入文件。这就奇怪了,我于是让程序继续运行到报错,看看报错时候程序的调用栈是被什么东西填满了,然后发现程序的调用栈长这样:
![image-20241105181612954](./rust-drop-stack-overflow/image-20241105181612954.png)
并不是我程序中代码的调用太深导致的而是Rust编译器自动生成的`drop`函数导致的。于是尝试看看调用栈的底部,看看是在读取什么输入数据,`drop`什么神仙数据结构的时候发生的。调试器很快告诉我们,`drop`的数据结构是抽象语法树中的二元表达式而此时的输入代码则如下图所示而且图中的代码重复了400行。
![image-20241105182036975](./rust-drop-stack-overflow/image-20241105182036975.png)
我已经能想象到那棵高耸如云的抽象语法树了。
虽然找到了问题的根源,但是还有一个问题没有解决:为什么在`main.rs`上运行的时候程序并不会出现问题,但是在`cargo test`上运行时却会遇到栈溢出的问题?
这个问题其实在[Rust语言圣经](https://course.rs/compiler/pitfalls/stack-overflow.html)中就有记载不过问题的背景略有不同。Rust语言圣经中导致栈溢出的问题是尝试在栈上分配一个4MB的超大数组但是出现问题的原因是一致的。在`main.rs`中运行程序时,如果不使用多线程,那么程序的所有逻辑将运行在`main`线程上这个线程在Linux下的栈大小是8MB而当使用Rust提供的集成测试时Rust为了实现测试的并行运行会把所有的测试都运行在新线程上这就导致在使用`cargo test`时程序会出现问题。
解决这个问题的方案可以是设置环境变量设置创建新线程的栈大小:`RUST_MIN_STACK=8388608 cargo test`,但是这种方法总是不太优雅。合理的解决方案是重写造成问题数据结构的`drop`方法,避免使用编译器自动生成的`drop`方法。这里我提供的抽象语法树`drop`方法如下所示。通过广度优先搜索的方式遍历语法树,手动释放一些可能子节点可能较多的语法树节点(其中释放内存的方式来自于[reddit](https://www.reddit.com/r/rust/comments/x97a4a/stack_overflow_during_drop_of_huge_abstract/))。
```rust
fn collect_node_rubbishes(
rubbish: &mut Vec<Rc<RefCell<SyntaxNode>>>,
node_type: &mut SyntaxNodeType,
) {
match node_type {
SyntaxNodeType::BinaryExpression(node) => {
rubbish.push(std::mem::replace(&mut node.left, SyntaxNode::unit()));
rubbish.push(std::mem::replace(&mut node.right, SyntaxNode::unit()));
}
SyntaxNodeType::Block(nodes) => {
while let Some(child) = nodes.pop() {
rubbish.push(child);
}
}
_ => {}
}
}
impl Drop for SyntaxNode {
fn drop(&mut self) {
let mut rubbish = Vec::new();
collect_node_rubbishes(&mut rubbish, &mut self.node_type);
while let Some(node) = rubbish.pop() {
collect_node_rubbishes(&mut rubbish, &mut node.borrow_mut().node_type);
}
}
}
```

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,73 @@
---
title: Rust中将子特征的特征对象转换为父特征的特征对象
date: 2024-12-15T15:49:33.5102602+08:00
tags:
- Rust
- 技术笔记
---
这辈子就是被Rust编译器害了.jpg
<!--more-->
## 背景
还是在开发同[上一篇](https://rrricardo.top/blog/essays/rust-drop-stack-overflow)相同的项目——一个编译器。在编写语法分析构建抽象语法树的过程中设计了这样一种抽象语法树的数据结构:每一个抽象语法树节点都实现了一个基础的语法树节点特征`SyntaxNode`,同时每个可以参加运算、有返回类型的语法树节点都需要实现`ExpressionSyntaxNode`特征,该特征是`SyntaxNode`特征的子特征。因此,从特征对象`Rc<dyn ExpressionSyntaxNode>`到特征对象`Rc<dyn SyntaxNode>`的转换就成为在语法树构建过程中必然会遇到的问题。
这种数据结构的设计就是一个非常具有面向对象特色的设计思路但是我们伟大的Rust目前却不支持这种特征对象的转换。这种转换在Rust语言内部称作`trait-upcasting`,已经在[RFC3324](https://github.com/rust-lang/rfcs/blob/master/text/3324-dyn-upcasting.md)中完成了定义但其的实现从2021年开始一直到现在都处于`unstable`的状态,需要在`nightly`版本的编译器中开启`#[feature(trait_upcasting)]`。具体来说,这个特点允许当特征`Bar` 是另一个特征`Foo`的子特征`Bar : Foo`时是一个特征对象`dyn Bar`被转换为特征对象`dyn Foo`
## 当前条件下的实现方法
虽然我们可以在使用`nightly`编译器的条件下使用`feature`开关抢先实用这个功能,但是应该没有人会在生产环境下使用`nightly`编译器罢。
所以我们需要一个在当前环境下可以使用的解决方法。解决的思路是设计一个类型转换的辅助特征`CastHelper`,这个特征就提供了需要的转换方法:
```rust
trait CastHelper {
fn cast(&self) -> &dyn Foo;
}
```
然后在定义`Bar`特征时,让`CastHelper`也成为`Bar`特征的超特征。
```rust
trait Bar : Foo + CastHelper {
// Other method.
}
```
接下来使用泛型的方式为所有实现了`Bar` 的结构体实现`CastHelper`
```rust
impl<T> CastHelper for T
where
T : Bar + 'static
{
fn cast(&self) -> &dyn Foo {
self as _
}
}
```
`CastHelper`中也可以定义到`Rc<dyn Foo>``Box<dyn Foo>`等特征对象的转换。
所有的实现代码如下:
```rust
trait Foo {}
trait Bar: Foo + CastHelper {}
trait CastHelper {
fn cast(&self) -> &dyn Foo;
}
impl<T> CastHelper for T
where
T : Bar + 'static
{
fn cast(&self) -> &dyn Foo {
self as _
}
}
```

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -13,78 +13,3 @@
font-display: block;
src: url(fonts/fa-solid-900.woff2) format("woff2"), url(fonts/fa-solid-900.ttf) format("truetype")
}
.essay-image {
width: 90%;
display: block;
margin: 1.5rem auto;
box-shadow: 0 5px 11px 0 rgba(0, 0, 0, 0.18),
0 4px 15px 0 rgba(0, 0, 0, 0.15);
border-radius: 4px;
background-color: transparent;
}
.table-wrapper {
overflow-x: auto;
}
body a {
text-decoration: none;
}
body main {
flex: 1;
min-height: 100vh;
overflow-x: visible;
}
body p {
margin: 0;
margin-block: 1em;
}
h2, h3 {
margin-block-start: 1.5em;
margin-block-end: 1em;
}
h4, h5 {
margin-block-start: 1em;
margin-block-end: 1em;
}
table {
margin: 0 auto;
border-collapse: collapse;
}
table td {
padding: 3px 20px;
border: 1px var(--bs-border-color) solid;
}
table thead {
background: var(--bs-secondary-bg);
}
table thead td {
font-weight: 700;
border: none;
}
table thead th {
padding: 3px 20px;
}
table thead tr {
border: 1px var(--bs-border-color) solid;
}
blockquote {
margin: 20px 0;
padding: 0 20px;
color: var(--bs-body-color);
background-color: var(--bs-primary-bg-subtle);
border-block-start: .1em solid var(--bs-primary-border-subtle);
border-block-end: .1em solid var(--bs-primary-border-subtle);
}

Some files were not shown because too many files have changed in this diff Show More