8 Commits

Author SHA1 Message Date
8c2cce59e4 Merge branch 'master' into write-async-await 2025-01-24 16:58:18 +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
438 changed files with 2506 additions and 5422 deletions

View File

@@ -15,9 +15,6 @@ trim_trailing_whitespace = true
[project.json]
indent_size = 2
[*.{yaml,yml}]
indent_size = 2
# C# and Visual Basic files
[*.{cs,vb}]
charset = utf-8-bom

3
.gitattributes vendored
View File

@@ -1,5 +1,2 @@
*.png filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.jpeg filter=lfs diff=lfs merge=lfs -text
*.avif filter=lfs diff=lfs merge=lfs -text
*.webp filter=lfs diff=lfs merge=lfs -text

View File

@@ -1,30 +1,29 @@
name: Build blog docker image
on:
push:
branches:
- master
push:
branches:
- master
jobs:
Build-Blog-Image:
runs-on: archlinux
steps:
- name: Check out code.
uses: https://mirrors.rrricardo.top/actions/checkout.git@v4
with:
lfs: true
- name: Build project.
run: |
podman pull mcr.azure.cn/dotnet/aspnet:10.0
cd YaeBlog
pwsh build.ps1 build
- name: Workaround to make sure podman-login working.
run: |
mkdir /root/.docker
- name: Login tencent cloud docker registry.
uses: https://mirrors.rrricardo.top/actions/podman-login.git@v1
with:
registry: ccr.ccs.tencentyun.com
username: 100044380877
password: ${{ secrets.TENCENT_REGISTRY_PASSWORD }}
auth_file_path: /etc/containers/auth.json
- name: Push docker image.
run: podman push ccr.ccs.tencentyun.com/jackfiled/blog:latest
Build-Blog-Image:
runs-on: archlinux
steps:
- uses: https://git.rrricardo.top/actions/checkout@v4
name: Check out code
with:
lfs: true
- name: Build project
run: |
cd YaeBlog
dotnet publish
- name: Build docker image
run: |
cd YaeBlog
docker build . -t registry.cn-beijing.aliyuncs.com/jackfiled/blog:latest
- name: Login aliyun docker registry
uses: https://git.rrricardo.top/actions/login-action@v3
with:
registry: registry.cn-beijing.aliyuncs.com
username: 初冬的朝阳
password: ${{ secrets.ALIYUN_PASSWORD }}
- name: Push docker image
run: docker push registry.cn-beijing.aliyuncs.com/jackfiled/blog:latest

2
.gitignore vendored
View File

@@ -484,4 +484,4 @@ $RECYCLE.BIN/
*.swp
# Tailwind auto-generated stylesheet
*.g.css
output.css

View File

@@ -1,13 +0,0 @@
namespace YaeBlog.Tests;
public class DateTimeOffsetTests
{
[Fact]
public void DateTimeOffsetParseTest()
{
const string input = "2026-01-04T16:36:36.5629759+08:00";
DateTimeOffset time = DateTimeOffset.Parse(input);
Assert.Equal("2026年01月04日 16:36:36", time.ToString("yyyy年MM月dd日 HH:mm:ss"));
}
}

View File

@@ -1,25 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\YaeBlog\YaeBlog.csproj" />
</ItemGroup>
</Project>

41
YaeBlog.sln Normal file
View File

@@ -0,0 +1,41 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
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}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ADBC3DA8-F65C-4B5D-A97A-DC351F8E6592}"
ProjectSection(SolutionItems) = preProject
.gitea\workflows\build.yaml = .gitea\workflows\build.yaml
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{753B998C-1B9E-498F-B949-845CE86C4075}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.gitattributes = .gitattributes
.gitignore = .gitignore
README.md = README.md
LICENSE = LICENSE
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{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
{20438EFD-8DDE-43AF-92E2-76495C29233C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{ADBC3DA8-F65C-4B5D-A97A-DC351F8E6592} = {9B5AAA29-37D8-454A-8D8F-3E6B6BCF38E6}
EndGlobalSection
EndGlobal

View File

@@ -1,15 +0,0 @@
<Solution>
<Folder Name="/.gitea/" />
<Folder Name="/.gitea/workflows/">
<File Path=".gitea/workflows/build.yaml" />
</Folder>
<Folder Name="/Solution Items/">
<File Path=".editorconfig" />
<File Path=".gitattributes" />
<File Path=".gitignore" />
<File Path="LICENSE" />
<File Path="README.md" />
</Folder>
<Project Path="YaeBlog.Tests/YaeBlog.Tests.csproj" />
<Project Path="YaeBlog/YaeBlog.csproj" />
</Solution>

View File

@@ -6,11 +6,7 @@ public interface IEssayScanService
{
public Task<BlogContents> ScanContents();
/// <summary>
/// 将对应的博客文章保存在磁盘上
/// </summary>
/// <param name="content"></param>
/// <param name="isDraft">指定对应博客文章是否为草稿。因为BlogContent是不可变对象因此提供该参数以方便publish的实现。</param>
/// <returns></returns>
public Task SaveBlogContent(BlogContent content, bool isDraft = true);
public Task<ImageScanResult> ScanImages();
}

View File

@@ -1,21 +0,0 @@
using System.CommandLine.Binding;
using YaeBlog.Abstraction;
using YaeBlog.Services;
namespace YaeBlog.Commands.Binders;
public sealed class ImageCompressServiceBinder : BinderBase<ImageCompressService>
{
protected override ImageCompressService GetBoundValue(BindingContext bindingContext)
{
bindingContext.AddService(provider =>
{
IEssayScanService essayScanService = provider.GetRequiredService<IEssayScanService>();
ILogger<ImageCompressService> logger = provider.GetRequiredService<ILogger<ImageCompressService>>();
return new ImageCompressService(essayScanService, logger);
});
return bindingContext.GetRequiredService<ImageCompressService>();
}
}

View File

@@ -1,6 +1,4 @@
using System.CommandLine;
using Microsoft.Extensions.Options;
using YaeBlog.Abstraction;
using YaeBlog.Commands.Binders;
using YaeBlog.Components;
using YaeBlog.Extensions;
@@ -19,10 +17,8 @@ public sealed class YaeBlogCommand
AddWatchCommand(_rootCommand);
AddListCommand(_rootCommand);
AddNewCommand(_rootCommand);
AddUpdateCommand(_rootCommand);
AddPublishCommand(_rootCommand);
AddScanCommand(_rootCommand);
AddCompressCommand(_rootCommand);
}
public Task<int> RunAsync(string[] args)
@@ -47,7 +43,7 @@ public sealed class YaeBlogCommand
WebApplication application = builder.Build();
application.MapStaticAssets();
application.UseStaticFiles();
application.UseAntiforgery();
application.UseYaeBlog();
@@ -77,7 +73,7 @@ public sealed class YaeBlogCommand
WebApplication application = builder.Build();
application.MapStaticAssets();
application.UseStaticFiles();
application.UseAntiforgery();
application.UseYaeBlog();
@@ -98,57 +94,28 @@ public sealed class YaeBlogCommand
Argument<string> filenameArgument = new(name: "blog name", description: "The created blog filename.");
newCommand.AddArgument(filenameArgument);
newCommand.SetHandler(async (file, blogOption, _, essayScanService) =>
newCommand.SetHandler(async (file, _, _, essayScanService) =>
{
BlogContents contents = await essayScanService.ScanContents();
if (contents.Posts.Any(content => content.BlogName == file))
if (contents.Posts.Any(content => content.FileName == file))
{
Console.WriteLine("There exists the same title blog in posts.");
return;
}
await essayScanService.SaveBlogContent(new BlogContent(
new FileInfo(Path.Combine(blogOption.Value.Root, "drafts", file + ".md")),
new MarkdownMetadata
{
Title = file,
Date = DateTimeOffset.Now.ToString("o"),
UpdateTime = DateTimeOffset.Now.ToString("o")
},
string.Empty, true, [], []));
await essayScanService.SaveBlogContent(new BlogContent
{
FileName = file,
FileContent = string.Empty,
Metadata = new MarkdownMetadata { Title = file, Date = DateTime.Now }
});
Console.WriteLine($"Created new blog '{file}.");
}, filenameArgument, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(),
new EssayScanServiceBinder());
}
private static void AddUpdateCommand(RootCommand rootCommand)
{
Command newCommand = new("update", "Update the blog essay.");
rootCommand.AddCommand(newCommand);
Argument<string> filenameArgument = new(name: "blog name", description: "The blog filename to update.");
newCommand.AddArgument(filenameArgument);
newCommand.SetHandler(async (file, _, _, essayScanService) =>
{
Console.WriteLine("HINT: The update command only consider published blogs.");
BlogContents contents = await essayScanService.ScanContents();
BlogContent? content = contents.Posts.FirstOrDefault(c => c.BlogName == file);
if (content is null)
{
Console.WriteLine($"Target essay {file} is not exist.");
return;
}
content.Metadata.UpdateTime = DateTimeOffset.Now.ToString("o");
await essayScanService.SaveBlogContent(content, content.IsDraft);
}, filenameArgument,
new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder());
}
private static void AddListCommand(RootCommand rootCommand)
{
Command command = new("list", "List all blogs");
@@ -159,15 +126,15 @@ public sealed class YaeBlogCommand
BlogContents contents = await essyScanService.ScanContents();
Console.WriteLine($"All {contents.Posts.Count} Posts:");
foreach (BlogContent content in contents.Posts.OrderBy(x => x.BlogName))
foreach (BlogContent content in contents.Posts.OrderBy(x => x.FileName))
{
Console.WriteLine($" - {content.BlogName}");
Console.WriteLine($" - {content.FileName}");
}
Console.WriteLine($"All {contents.Drafts.Count} Drafts:");
foreach (BlogContent content in contents.Drafts.OrderBy(x => x.BlogName))
foreach (BlogContent content in contents.Drafts.OrderBy(x => x.FileName))
{
Console.WriteLine($" - {content.BlogName}");
Console.WriteLine($" - {content.FileName}");
}
}, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder());
}
@@ -183,39 +150,32 @@ public sealed class YaeBlogCommand
command.SetHandler(async (_, _, essayScanService, removeOptionValue) =>
{
BlogContents contents = await essayScanService.ScanContents();
List<BlogImageInfo> unusedImages = (from content in contents
from image in content.Images
where image is { IsUsed: false }
select image).ToList();
ImageScanResult result = await essayScanService.ScanImages();
if (unusedImages.Count != 0)
if (result.UnusedImages.Count != 0)
{
Console.WriteLine("Found unused images:");
Console.WriteLine("HINT: use '--rm' to remove unused images.");
}
foreach (BlogImageInfo image in unusedImages)
foreach (FileInfo image in result.UnusedImages)
{
Console.WriteLine($" - {image.File.FullName}");
Console.WriteLine($" - {image.FullName}");
}
if (removeOptionValue)
{
foreach (BlogImageInfo image in unusedImages)
foreach (FileInfo image in result.UnusedImages)
{
image.File.Delete();
image.Delete();
}
}
Console.WriteLine("Used not existed images:");
foreach (BlogContent content in contents)
foreach (FileInfo image in result.NotFoundImages)
{
foreach (FileInfo file in content.NotfoundImages)
{
Console.WriteLine($"- {file.Name} in {content.BlogName}");
}
Console.WriteLine($" - {image.FullName}");
}
}, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder(), removeOption);
}
@@ -233,7 +193,7 @@ public sealed class YaeBlogCommand
BlogContents contents = await essayScanService.ScanContents();
BlogContent? content = (from blog in contents.Drafts
where blog.BlogName == filename
where blog.FileName == filename
select blog).FirstOrDefault();
if (content is null)
@@ -242,18 +202,14 @@ public sealed class YaeBlogCommand
return;
}
// 设置发布的时间
content.Metadata.Date = DateTimeOffset.Now.ToString("o");
content.Metadata.UpdateTime = DateTimeOffset.Now.ToString("o");
// 将选中的博客文件复制到posts
await essayScanService.SaveBlogContent(content, isDraft: false);
// 复制图片文件夹
DirectoryInfo sourceImageDirectory =
new(Path.Combine(blogOptions.Value.Root, "drafts", content.BlogName));
new(Path.Combine(blogOptions.Value.Root, "drafts", content.FileName));
DirectoryInfo targetImageDirectory =
new(Path.Combine(blogOptions.Value.Root, "posts", content.BlogName));
new(Path.Combine(blogOptions.Value.Root, "posts", content.FileName));
if (sourceImageDirectory.Exists)
{
@@ -267,30 +223,9 @@ public sealed class YaeBlogCommand
}
// 删除原始的文件
FileInfo sourceBlogFile = new(Path.Combine(blogOptions.Value.Root, "drafts", content.BlogName + ".md"));
FileInfo sourceBlogFile = new(Path.Combine(blogOptions.Value.Root, "drafts", content.FileName + ".md"));
sourceBlogFile.Delete();
}, new BlogOptionsBinder(),
new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder(), filenameArgument);
}
private static void AddCompressCommand(RootCommand rootCommand)
{
Command command = new("compress", "Compress png/jpeg image to webp image to reduce size.");
rootCommand.Add(command);
Option<bool> dryRunOption = new("--dry-run", description: "Dry run the compression task but not write.",
getDefaultValue: () => false);
command.AddOption(dryRunOption);
command.SetHandler(ImageCommandHandler,
new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new LoggerBinder<ImageCompressService>(),
new EssayScanServiceBinder(), new ImageCompressServiceBinder(), dryRunOption);
}
private static async Task ImageCommandHandler(IOptions<BlogOptions> _, ILogger<EssayScanService> _1,
ILogger<ImageCompressService> _2,
IEssayScanService _3, ImageCompressService imageCompressService, bool dryRun)
{
await imageCompressService.Compress(dryRun);
}
}

View File

@@ -5,33 +5,16 @@
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<base href="/"/>
<link rel="stylesheet" href="@Assets["YaeBlog.styles.css"]"/>
<link rel="icon" href="@Assets["images/favicon.ico"]"/>
<link rel="stylesheet" href="@Assets["tailwind.g.css"]"/>
<style>
@@font-face {
font-family: "Font Awesome 6 Free";
font-style: normal;
font-weight: 400;
font-display: block;
src: url(@Assets["fonts/fa-regular-400.woff2"]) format("woff2") url(@Assets["fonts/fa-regular-400.ttf"]) format("truetype");
}
@@font-face {
font-family: "Font Awesome 6 Free";
font-style: normal;
font-weight: 900;
font-display: block;
src: url(@Assets["fonts/fa-solid-900.woff2"]) format("woff2"), url(@Assets["fonts/fa-solid-900.ttf"]) format("truetype")
}
</style>
<link rel="stylesheet" href="YaeBlog.styles.css"/>
<link rel="icon" href="images/favicon.ico"/>
<link rel="stylesheet" href="globals.css"/>
<link rel="stylesheet" href="output.css"/>
<HeadOutlet/>
<ImportMap/>
</head>
<body>
<Routes/>
<script src="@Assets["_framework/blazor.web.js"]"></script>
<Routes/>
<script src="_framework/blazor.web.js"></script>
</body>
</html>

View File

@@ -1,29 +0,0 @@
<div class="flex flex-wrap justify-center gap-12 max-w-md md:max-w-lg">
<div class="relative w-40 h-48 md:w-48 md:w-48 overflow-hidden
transition-all duration-300 ease-out hover:scale-125 group">
<img
src="./images/wechat-code.jpeg"
alt="微信赞赏码"
class="w-full h-full object-cover"
/>
<div class="absolute -bottom-8 left-0 right-0 text-center
text-white bg-black opacity-60 text-sm font-medium
backdrop-blur-sm group-hover:bottom-2 transition-all duration-300">
请我喝奶茶<br/>
</div>
</div>
<div class="relative w-40 h-48 md:w-48 md:h-48 overflow-hidden
transition-all duration-300 ease-out hover:scale-125 group">
<img
src="./images/alipay-code.jpeg"
alt="支付宝赞赏码"
class="w-full h-full object-cover"/>
<div class="absolute -bottom-8 left-0 right-0 text-center
text-white bg-black opacity-60 text-sm font-medium
backdrop-blur-sm group-hover:bottom-2 transition-all duration-300">
请我吃晚饭<br/>
</div>
</div>
</div>

View File

@@ -3,7 +3,7 @@
<div class="flex flex-col p-3">
<div class="text-3xl font-bold py-2">
<a href="/blog/essays/@(Essay.FileName)">@(Essay.Title)</a>
<a href="/blog/essays/@(Essay.FileName)" target="_blank">@(Essay.Title)</a>
</div>
<div class="p-2 flex flex-row justify-content-start gap-2">
@@ -14,7 +14,9 @@
@foreach (string key in Essay.Tags)
{
<div class="text-sky-600">
<Anchor Address="@($"/blog/tags/?tagName={UrlEncoder.Default.Encode(key)}")" Text="@($"# {key}")"/>
<a href="/blog/tags/?tagName=@(UrlEncoder.Default.Encode(key))">
# @key
</a>
</div>
}
</div>

View File

@@ -7,15 +7,11 @@
<Anchor Address="https://dotnet.microsoft.com" Text="@DotnetVersion"/>
驱动。
</p>
<p class="text-md">
Build Commit #
<Anchor Address="@BuildCommitUrl" Text="@BuildCommitId"/>
</p>
</div>
<div>
<p class="text-md">
<Anchor Address="https://beian.miit.gov.cn" Text="蜀ICP备2022004429号-1" NewPage="true"/>
<a href="https://beian.miit.gov.cn" target="_blank" class="text-black">蜀ICP备2022004429号-1</a>
</p>
</div>
</div>
@@ -23,8 +19,4 @@
@code
{
private string DotnetVersion => $".NET {Environment.Version}";
private string BuildCommitId => Environment.GetEnvironmentVariable("COMMIT_ID") ?? "local_build";
private string BuildCommitUrl => $"https://git.rrricardo.top/jackfiled/YaeBlog/commit/{BuildCommitId}";
}

View File

@@ -1,28 +0,0 @@
@inherits LayoutComponentBase
@attribute [StreamRendering]
<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="md:col-span-1 col-span-3 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/archives" Text="归档"/>
<Anchor Address="/blog/tags/" Text="标签"/>
<Anchor Address="/about/" Text="关于" NewPage="@(true)"/>
<Anchor Address="/friends" Text="友链" NewPage="@(true)"/>
</div>
</div>
</div>
<div class="px-4 py-2 flex-grow">
@Body
</div>
<Foonter/>
</main>

View File

@@ -1,3 +1,6 @@
@using YaeBlog.Models
@inject BlogOptions Options
<div class="px-4 py-8 border border-sky-700 rounded-md bg-sky-200">
<div class="flex flex-col gap-3 text-md">
<div>
@@ -21,17 +24,6 @@
Ricardo's Blog
</a>”。
</div>
<div class="flex flex-col">
<div class="flex justify-center">
<p>如果觉得不错的话,可以支持一下作者哦~</p>
</div>
<div class="flex justify-center">
<AppreciationCode/>
</div>
</div>
</div>
</div>

View File

@@ -1,53 +0,0 @@
<Project>
<PropertyGroup>
<ClientAssetsRestoreCommand Condition="'$(ClientAssesRestoreCommand)' == ''">pnpm install</ClientAssetsRestoreCommand>
<ClientAssetsBuildCommand Condition="'$(ClientAssetsBuildCommand)' == ''">pnpm run build</ClientAssetsBuildCommand>
<ClientAssetsBuildOutputParameter Condition="'$(ClientAssetsBuildOutputParameter)' == ''">--output</ClientAssetsBuildOutputParameter>
</PropertyGroup>
<PropertyGroup>
<_RestoreClientAssetsBeforeTargets Condition="'$(TargetFramework)' == ''">DispatchToInnerBuilds</_RestoreClientAssetsBeforeTargets>
</PropertyGroup>
<Target Name="RestoreClientAssets" BeforeTargets="$(_RestoreClientAssetsBeforeTargets)">
<Message Importance="high" Text="Running $(ClientAssetsRestoreCommand)"/>
<Exec Command="$(ClientAssetsRestoreCommand)"/>
</Target>
<Target Name="BuildClientAssets" DependsOnTargets="RestoreClientAssets" BeforeTargets="AssignTargetPaths">
<PropertyGroup>
<_ClientAssetsOutputFullPath>$([System.IO.Path]::GetFullPath('$(IntermediateOutputPath)ClientAssets'))</_ClientAssetsOutputFullPath>
</PropertyGroup>
<MakeDir Directories="$(_ClientAssetsOutputFullPath)"/>
<Exec Command="$(ClientAssetsBuildCommand) $(ClientAssetsBuildOutputParameter) $(_ClientAssetsOutputFullPath)"/>
<ItemGroup>
<_ClientAssetsBuildOutput Include="$(IntermediateOutputPath)ClientAssets\**"/>
</ItemGroup>
</Target>
<Target Name="DefineClientAssets" AfterTargets="BuildClientAssets" DependsOnTargets="ResolveStaticWebAssetsConfiguration">
<ItemGroup>
<FileWrites Include="@(_ClientAssetsBuildOutput)"/>
</ItemGroup>
<DefineStaticWebAssets
CandidateAssets="@(_ClientAssetsBuildOutput)"
SourceId="$(PackageId)"
SourceType="Computed"
ContentRoot="$(_ClientAssetsOutputFullPath)"
BasePath="$(StaticWebAssetBasePath)"
>
<Output TaskParameter="Assets" ItemName="StaticWebAsset"/>
<Output TaskParameter="Assets" ItemName="_ClientAssetsStaticWebAsset"/>
</DefineStaticWebAssets>
<DefineStaticWebAssetEndpoints
CandidateAssets="@(_ClientAssetsStaticWebAsset)"
ContentTypeMappings="@(StaticWebAssetContentTypeMapping)"
>
<Output TaskParameter="Endpoints" ItemName="StaticWebAssetEndpoint" />
</DefineStaticWebAssetEndpoints>
</Target>
</Project>

View File

@@ -1,10 +1,7 @@
FROM mcr.azure.cn/dotnet/aspnet:10.0
ARG COMMIT_ID
ENV COMMIT_ID=${COMMIT_ID}
FROM mcr.microsoft.com/dotnet/aspnet:9.0
WORKDIR /app
COPY bin/Release/net10.0/publish/ ./
COPY bin/Release/net9.0/publish/ ./
COPY source/ ./source/
COPY appsettings.json .

View File

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

View File

@@ -1,18 +0,0 @@
using AngleSharp.Dom;
namespace YaeBlog.Extensions;
public static class AngleSharpExtensions
{
public static IEnumerable<IElement> EnumerateParentElements(this IElement element)
{
IElement? e = element.ParentElement;
while (e is not null)
{
IElement c = e;
e = e.ParentElement;
yield return c;
}
}
}

View File

@@ -0,0 +1,44 @@
@inherits LayoutComponentBase
@attribute [StreamRendering]
<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="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="px-4 py-2 flex-grow">
@Body
</div>
<Foonter/>
</main>

View File

@@ -1,20 +1,12 @@
namespace YaeBlog.Models;
/// <summary>
/// 单个博客文件的所有数据和元数据
/// </summary>
/// <param name="BlogFile">博客文件</param>
/// <param name="Metadata">文件中的MD元数据</param>
/// <param name="Content">文件内容</param>
/// <param name="IsDraft">是否为草稿</param>
/// <param name="Images">博客中使用的文件</param>
public record BlogContent(
FileInfo BlogFile,
MarkdownMetadata Metadata,
string Content,
bool IsDraft,
List<BlogImageInfo> Images,
List<FileInfo> NotfoundImages)
public class BlogContent
{
public string BlogName => BlogFile.Name.Split('.')[0];
public required string FileName { get; init; }
public required MarkdownMetadata Metadata { get; init; }
public required string FileContent { get; set; }
public bool IsDraft { get; set; } = false;
}

View File

@@ -1,12 +1,10 @@
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Concurrent;
namespace YaeBlog.Models;
public record BlogContents(ConcurrentBag<BlogContent> Drafts, ConcurrentBag<BlogContent> Posts)
: IEnumerable<BlogContent>
public sealed class BlogContents(ConcurrentBag<BlogContent> drafts, ConcurrentBag<BlogContent> posts)
{
public IEnumerator<BlogContent> GetEnumerator() => Posts.Concat(Drafts).GetEnumerator();
public ConcurrentBag<BlogContent> Drafts { get; } = drafts;
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public ConcurrentBag<BlogContent> Posts { get; } = posts;
}

View File

@@ -8,9 +8,7 @@ public class BlogEssay : IComparable<BlogEssay>
public required bool IsDraft { get; init; }
public required DateTimeOffset PublishTime { get; init; }
public required DateTimeOffset UpdateTime { get; init; }
public required DateTime PublishTime { get; init; }
public required string Description { get; init; }
@@ -30,7 +28,6 @@ public class BlogEssay : IComparable<BlogEssay>
FileName = FileName,
IsDraft = IsDraft,
PublishTime = PublishTime,
UpdateTime = UpdateTime,
Description = Description,
WordCount = WordCount,
ReadTime = ReadTime,

View File

@@ -1,44 +0,0 @@
using System.Text;
namespace YaeBlog.Models;
public record BlogImageInfo(FileInfo File, long Width, long Height, string MineType, byte[] Content, bool IsUsed)
: IComparable<BlogImageInfo>
{
public int Size => Content.Length;
public override string ToString()
{
StringBuilder builder = new();
builder.AppendLine($"Blog image {File.Name}:");
builder.AppendLine($"\tWidth: {Width}; Height: {Height}");
builder.AppendLine($"\tSize: {FormatSize()}");
builder.AppendLine($"\tImage Format: {MineType}");
return builder.ToString();
}
public int CompareTo(BlogImageInfo? other)
{
if (other is null)
{
return -1;
}
return other.Size.CompareTo(Size);
}
private string FormatSize()
{
double size = Size;
if (size / 1024 > 3)
{
size /= 1024;
return size / 1024 > 3 ? $"{size / 1024}MB" : $"{size}KB";
}
return $"{size}B";
}
}

View File

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

View File

@@ -4,9 +4,7 @@ public class MarkdownMetadata
{
public string? Title { get; set; }
public string? Date { get; set; }
public string? UpdateTime { get; set; }
public DateTime? Date { get; set; }
public List<string>? Tags { get; set; }
}

View File

@@ -19,7 +19,7 @@
</span>
</div>
@foreach (IGrouping<DateTimeOffset, BlogEssay> group in _essays)
@foreach (IGrouping<DateTime, BlogEssay> group in _essays)
{
<div class="p-2">
<div class="flex flex-col">
@@ -30,7 +30,7 @@
<div class="px-4 py-4 flex flex-col">
@foreach (BlogEssay essay in group)
{
<a href="@($"/blog/essays/{essay.FileName}")">
<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日"))
@@ -51,13 +51,13 @@
</div>
@code {
private readonly List<IGrouping<DateTimeOffset, BlogEssay>> _essays = [];
private readonly List<IGrouping<DateTime, BlogEssay>> _essays = [];
protected override void OnInitialized()
{
base.OnInitialized();
_essays.AddRange(from essay in Contents.Essays
group essay by new DateTimeOffset(essay.PublishTime.Year, 1, 1,0, 0, 0, TimeSpan.Zero));
group essay by new DateTime(essay.PublishTime.Year, 1, 1));
}
}

View File

@@ -39,11 +39,6 @@
{
_page = Page ?? 1;
_pageCount = Contents.Count / EssaysPerPage + 1;
(_pageCount, int reminder) = int.DivRem(Contents.Count, EssaysPerPage);
if (reminder > 0)
{
_pageCount += 1;
}
if (EssaysPerPage * _page > Contents.Count + EssaysPerPage)
{

View File

@@ -12,42 +12,47 @@
<div class="flex flex-col py-8">
<div>
<div class="flex flex-col items-center">
<div>
<h1 id="title" class="text-4xl">@(_essay!.Title)</h1>
<h1 id="title" class="text-4xl">@(_essay!.Title)</h1>
<div class="col-auto">
</div>
</div>
<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>
<div class="flex flex-row gap-4 py-2">
@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 class="font-light pb-1">
发布于: @(_essay.PublishTime.ToString("yyyy年MM月dd日 HH:mm:ss"))
</div>
@if (_essay.UpdateTime != _essay.PublishTime)
@foreach (string tag in _essay!.Tags)
{
<div class="font-light pb-1">
更新于: @(_essay.UpdateTime.ToString("yyyy年MM月dd日 HH:mm:ss"))
<div class="text-sky-500">
<a href="/blog/tags/?tagName=@(UrlEncoder.Default.Encode(tag))">
# @(tag)
</a>
</div>
}
</div>
</div>
<div class="font-light pb-1">
总字数:@(_essay!.WordCount)字,预计阅读时间 @(_essay!.ReadTime)
</div>
<div class="px-6 pt-2 pb-4">
<div class="font-light">
总字数:@(_essay!.WordCount)字,预计阅读时间 @(_essay!.ReadTime)。
</div>
</div>
<div class="grid grid-cols-3">
<div class="col-span-3 md:col-span-2 flex flex-col gap-3">
<div>
@((MarkupString)_essay!.HtmlContent)
</div>
<div>
<LicenseDisclaimer EssayFilename="@BlogKey"/>
</div>
</div>
<div class="col-span-3 md:col-span-1">
<div class="flex flex-col sticky top-20 px-8 pt-20">
<div class="flex flex-col sticky top-0 px-8">
<div>
<h3 class="text-2xl">文章目录</h3>
</div>
@@ -88,17 +93,8 @@
}
</div>
</div>
<div class="col-span-3 md:col-span-2 flex flex-col gap-3">
<div>
@((MarkupString)_essay!.HtmlContent)
</div>
<div>
<LicenseDisclaimer EssayFilename="@BlogKey"/>
</div>
</div>
</div>
</div>
@code {

View File

@@ -7,7 +7,7 @@
<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="@Assets["images/avatar.png"]" alt="Ricardo's Avatar" class="h-auto max-w-full">
<img src="images/avatar.png" alt="Ricardo's Avatar" class="h-auto max-w-full">
</div>
<div class="col-span-3 md:col-span-2">
@@ -26,7 +26,7 @@
<div class="">
<p class="text-lg">
学过一些基础的计算机知识,略懂一些代码
平平无奇的计算机科学与技术学徒,连微小的贡献都没做
</p>
</div>
</div>

View File

@@ -1,7 +1,6 @@
using AngleSharp;
using AngleSharp.Dom;
using YaeBlog.Abstraction;
using YaeBlog.Extensions;
using YaeBlog.Models;
namespace YaeBlog.Processors;
@@ -16,26 +15,25 @@ public sealed class EssayStylesPostRenderProcessor : IPostRenderProcessor
public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
{
BrowsingContext context = new(Configuration.Default);
IDocument document = await context.OpenAsync(req => req.Content(essay.HtmlContent));
IDocument document = await context.OpenAsync(
req => req.Content(essay.HtmlContent));
ApplyGlobalCssStyles(document);
BeatifyTable(document);
BeatifyList(document);
BeatifyInlineCode(document);
return essay.WithNewHtmlContent(document.DocumentElement.OuterHtml);
}
private readonly Dictionary<string, string> _globalCssStyles = new()
{
{ "pre", "p-4 bg-gray-100 rounded-sm overflow-x-auto" },
{ "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" },
{ "a", "text-blue-600" }
{ "ul", "list-disc pl-2" }
};
private void ApplyGlobalCssStyles(IDocument document)
@@ -101,61 +99,4 @@ public sealed class EssayStylesPostRenderProcessor : IPostRenderProcessor
}
}
}
/// <summary>
/// 美化各种列表元素
/// </summary>
/// <param name="document"></param>
private static void BeatifyList(IDocument document)
{
foreach (IElement listElement in from e in document.All
where e.LocalName is "ol" or "ul"
select e)
{
// 给有序或者无序列表添加不同的样式
listElement.ClassList.Add("ml-10");
switch (listElement.LocalName)
{
case "ul":
{
listElement.ClassList.Add("list-disc");
break;
}
case "ol":
{
listElement.ClassList.Add("list-decimal");
break;
}
}
foreach (IElement liElement in from e in listElement.Children
where e.LocalName == "li"
select e)
{
// 修改<li>元素中的<p>元素样式
// 默认的p-2间距有点太宽了
foreach (IElement pElement in from e in liElement.Children
where e.LocalName == "p"
select e)
{
pElement.ClassList.Remove("p-2");
pElement.ClassList.Add("p-1");
}
}
}
}
private static void BeatifyInlineCode(IDocument document)
{
// 选择不在<pre>元素内的<code>元素
// 即行内代码
IEnumerable<IElement> inlineCodes = from e in document.All
where e.LocalName == "code" && e.EnumerateParentElements().All(p => p.LocalName != "pre")
select e;
foreach (IElement e in inlineCodes)
{
e.ClassList.Add("bg-gray-100 inline p-1 rounded-xs");
}
}
}

View File

@@ -7,8 +7,7 @@ using YaeBlog.Models;
namespace YaeBlog.Processors;
public class ImagePostRenderProcessor(
ILogger<ImagePostRenderProcessor> logger,
public class ImagePostRenderProcessor(ILogger<ImagePostRenderProcessor> logger,
IOptions<BlogOptions> options)
: IPostRenderProcessor
{
@@ -30,27 +29,22 @@ public class ImagePostRenderProcessor(
if (attr is not null)
{
logger.LogDebug("Found image link: '{}'", attr.Value);
attr.Value = GenerateImageLink(attr.Value, essay.FileName, essay.IsDraft);
attr.Value = GenerateImageLink(attr.Value, essay.FileName);
}
}
return essay.WithNewHtmlContent(html.DocumentElement.OuterHtml);
}
public string Name => nameof(ImagePostRenderProcessor);
private string GenerateImageLink(string filename, string essayFilename, bool isDraft)
private string GenerateImageLink(string filename, string essayFilename)
{
// 如果图片路径中没有包含文件名
// 则添加文件名
if (!filename.Contains(essayFilename))
{
filename = Path.Combine(essayFilename, filename);
}
filename = isDraft
? Path.Combine(_options.Root, "drafts", filename)
: Path.Combine(_options.Root, "posts", filename);
filename = Path.Combine(_options.Root, "posts", filename);
if (!Path.Exists(filename))
{

View File

@@ -16,11 +16,11 @@ public sealed class BlogHotReloadService(
await rendererService.RenderAsync(true);
Task[] reloadTasks = [WatchFileAsync(stoppingToken)];
Task[] reloadTasks = [FileWatchTask(stoppingToken)];
await Task.WhenAll(reloadTasks);
}
private async Task WatchFileAsync(CancellationToken token)
private async Task FileWatchTask(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
@@ -33,15 +33,6 @@ public sealed class BlogHotReloadService(
break;
}
FileInfo changeFileInfo = new(changeFile);
if (changeFileInfo.Name.StartsWith('.'))
{
// Ignore dot-started file and directory.
logger.LogDebug("Ignore hidden file: {}.", changeFile);
continue;
}
logger.LogInformation("{} changed, re-rendering.", changeFile);
essayContentService.Clear();
await rendererService.RenderAsync(true);

View File

@@ -1,7 +1,5 @@
using System.Collections.Concurrent;
using System.Text.RegularExpressions;
using Imageflow.Bindings;
using Imageflow.Fluent;
using Microsoft.Extensions.Options;
using YaeBlog.Abstraction;
using YaeBlog.Core.Exceptions;
@@ -11,30 +9,17 @@ using YamlDotNet.Serialization;
namespace YaeBlog.Services;
public partial class EssayScanService : IEssayScanService
public partial class EssayScanService(
ISerializer yamlSerializer,
IDeserializer yamlDeserializer,
IOptions<BlogOptions> blogOptions,
ILogger<EssayScanService> logger) : IEssayScanService
{
private readonly BlogOptions _blogOptions;
private readonly ISerializer _yamlSerializer;
private readonly IDeserializer _yamlDeserializer;
private readonly ILogger<EssayScanService> _logger;
public EssayScanService(ISerializer yamlSerializer,
IDeserializer yamlDeserializer,
IOptions<BlogOptions> blogOptions,
ILogger<EssayScanService> logger)
{
_yamlSerializer = yamlSerializer;
_yamlDeserializer = yamlDeserializer;
_logger = logger;
_blogOptions = blogOptions.Value;
RootDirectory = ValidateRootDirectory();
}
private DirectoryInfo RootDirectory { get; }
private readonly BlogOptions _blogOptions = blogOptions.Value;
public async Task<BlogContents> ScanContents()
{
ValidateDirectory(out DirectoryInfo drafts, out DirectoryInfo posts);
ValidateDirectory(_blogOptions.Root, out DirectoryInfo drafts, out DirectoryInfo posts);
return new BlogContents(
await ScanContentsInternal(drafts, true),
@@ -43,98 +28,82 @@ public partial class EssayScanService : IEssayScanService
public async Task SaveBlogContent(BlogContent content, bool isDraft = true)
{
ValidateDirectory(out DirectoryInfo drafts, out DirectoryInfo posts);
ValidateDirectory(_blogOptions.Root, out DirectoryInfo drafts, out DirectoryInfo posts);
FileInfo targetFile = isDraft
? new FileInfo(Path.Combine(drafts.FullName, content.BlogName + ".md"))
: new FileInfo(Path.Combine(posts.FullName, content.BlogName + ".md"));
? new FileInfo(Path.Combine(drafts.FullName, content.FileName + ".md"))
: new FileInfo(Path.Combine(posts.FullName, content.FileName + ".md"));
if (!isDraft)
{
content.Metadata.Date = DateTime.Now;
}
if (targetFile.Exists)
{
_logger.LogWarning("Blog {} exists, overriding.", targetFile.Name);
logger.LogWarning("Blog {} exists, overriding.", targetFile.Name);
}
await using StreamWriter writer = targetFile.CreateText();
await writer.WriteAsync("---\n");
await writer.WriteAsync(_yamlSerializer.Serialize(content.Metadata));
await writer.WriteAsync(yamlSerializer.Serialize(content.Metadata));
await writer.WriteAsync("---\n");
if (string.IsNullOrEmpty(content.Content) && isDraft)
if (isDraft)
{
// 如果博客为操作且内容为空
// 创建简介隔断符号
await writer.WriteLineAsync("<!--more-->");
}
else
{
await writer.WriteAsync(content.Content);
await writer.WriteAsync(content.FileContent);
}
// 保存图片文件
await Task.WhenAll(from image in content.Images
select File.WriteAllBytesAsync(image.File.FullName, image.Content));
}
private record struct BlogResult(
FileInfo BlogFile,
string BlogContent,
List<BlogImageInfo> Images,
List<FileInfo> NotFoundImages);
private async Task<ConcurrentBag<BlogContent>> ScanContentsInternal(DirectoryInfo directory, bool isDraft)
{
// 扫描以md结尾且不是隐藏文件的文件
// 扫描以md结果的但是不是隐藏文件的文件
IEnumerable<FileInfo> markdownFiles = from file in directory.EnumerateFiles()
where file.Extension == ".md" && !file.Name.StartsWith('.')
select file;
ConcurrentBag<BlogResult> fileContents = [];
ConcurrentBag<(string, string)> fileContents = [];
await Parallel.ForEachAsync(markdownFiles, async (file, token) =>
{
using StreamReader reader = file.OpenText();
string blogName = file.Name.Split('.')[0];
string blogContent = await reader.ReadToEndAsync(token);
ImageResult imageResult =
await ScanImagePreBlog(directory, blogName,
blogContent);
fileContents.Add(new BlogResult(file, blogContent, imageResult.Images, imageResult.NotfoundImages));
fileContents.Add((file.Name, await reader.ReadToEndAsync(token)));
});
ConcurrentBag<BlogContent> contents = [];
await Task.Run(() =>
{
foreach (BlogResult blog in fileContents)
foreach ((string filename, string content) in fileContents)
{
if (blog.BlogContent.Length < 4)
int endPos = content.IndexOf("---", 4, StringComparison.Ordinal);
if (!content.StartsWith("---") || endPos is -1 or 0)
{
// Even not contains a legal header.
continue;
}
int endPos = blog.BlogContent.IndexOf("---", 4, StringComparison.Ordinal);
if (!blog.BlogContent.StartsWith("---") || endPos is -1 or 0)
{
_logger.LogWarning("Failed to parse metadata from {}, skipped.", blog.BlogFile.Name);
logger.LogWarning("Failed to parse metadata from {}, skipped.", filename);
return;
}
string metadataString = blog.BlogContent[4..endPos];
string metadataString = content[4..endPos];
try
{
MarkdownMetadata metadata = _yamlDeserializer.Deserialize<MarkdownMetadata>(metadataString);
_logger.LogDebug("Scan metadata title: '{title}' for {name}.", metadata.Title, blog.BlogFile.Name);
MarkdownMetadata metadata = yamlDeserializer.Deserialize<MarkdownMetadata>(metadataString);
logger.LogDebug("Scan metadata title: '{}' for {}.", metadata.Title, filename);
contents.Add(new BlogContent(blog.BlogFile, metadata, blog.BlogContent[(endPos + 3)..], isDraft,
blog.Images, blog.NotFoundImages));
contents.Add(new BlogContent
{
FileName = filename[..^3], Metadata = metadata, FileContent = content[(endPos + 3)..],
IsDraft = isDraft
});
}
catch (YamlException e)
{
_logger.LogWarning("Failed to parser metadata from {name} due to {exception}, skipping", blog.BlogFile.Name, e);
logger.LogWarning("Failed to parser metadata from {} due to {}, skipping", filename, e);
}
}
});
@@ -142,96 +111,99 @@ public partial class EssayScanService : IEssayScanService
return contents;
}
private record struct ImageResult(List<BlogImageInfo> Images, List<FileInfo> NotfoundImages);
private async Task<ImageResult> ScanImagePreBlog(DirectoryInfo directory, string blogName, string content)
public async Task<ImageScanResult> ScanImages()
{
MatchCollection matchResult = ImagePattern.Matches(content);
DirectoryInfo imageDirectory = new(Path.Combine(directory.FullName, blogName));
BlogContents contents = await ScanContents();
ValidateDirectory(_blogOptions.Root, out DirectoryInfo drafts, out DirectoryInfo posts);
Dictionary<string, bool> usedImages = imageDirectory.Exists
? imageDirectory.EnumerateFiles().ToDictionary(file => file.FullName, _ => false)
: [];
List<FileInfo> notFoundImages = [];
List<FileInfo> unusedFiles = [];
List<FileInfo> notFoundFiles = [];
foreach (Match match in matchResult)
ImageScanResult draftResult = await ScanUnusedImagesInternal(contents.Drafts, drafts);
ImageScanResult postResult = await ScanUnusedImagesInternal(contents.Posts, posts);
unusedFiles.AddRange(draftResult.UnusedImages);
notFoundFiles.AddRange(draftResult.NotFoundImages);
unusedFiles.AddRange(postResult.UnusedImages);
notFoundFiles.AddRange(postResult.NotFoundImages);
return new ImageScanResult(unusedFiles, notFoundFiles);
}
private static Task<ImageScanResult> ScanUnusedImagesInternal(IEnumerable<BlogContent> contents,
DirectoryInfo root)
{
ConcurrentBag<FileInfo> unusedImage = [];
ConcurrentBag<FileInfo> notFoundImage = [];
Parallel.ForEach(contents, content =>
{
string imageName = match.Groups[1].Value;
MatchCollection result = ImagePattern.Matches(content.FileContent);
DirectoryInfo imageDirectory = new(Path.Combine(root.FullName, content.FileName));
// 判断md文件中的图片名称中是否包含文件夹名称
// 例如 blog-1/image.png 或者 image.png
// 如果不带文件夹名称
// 默认添加同博客名文件夹
FileInfo usedFile = imageName.Contains(blogName)
? new FileInfo(Path.Combine(directory.FullName, imageName))
: new FileInfo(Path.Combine(directory.FullName, blogName, imageName));
Dictionary<string, bool> usedDictionary;
if (usedImages.TryGetValue(usedFile.FullName, out _))
if (imageDirectory.Exists)
{
usedImages[usedFile.FullName] = true;
usedDictionary = (from file in imageDirectory.EnumerateFiles()
select new KeyValuePair<string, bool>(file.FullName, false)).ToDictionary();
}
else
{
notFoundImages.Add(usedFile);
usedDictionary = [];
}
}
List<BlogImageInfo> images = (await Task.WhenAll((from pair in usedImages
select GetImageInfo(new FileInfo(pair.Key), pair.Value)).ToArray())).ToList();
foreach (Match match in result)
{
string imageName = match.Groups[1].Value;
return new ImageResult(images, notFoundImages);
}
FileInfo usedFile = imageName.Contains(content.FileName)
? new FileInfo(Path.Combine(root.FullName, imageName))
: new FileInfo(Path.Combine(root.FullName, content.FileName, imageName));
private static async Task<BlogImageInfo> GetImageInfo(FileInfo file, bool isUsed)
{
byte[] image = await File.ReadAllBytesAsync(file.FullName);
if (usedDictionary.TryGetValue(usedFile.FullName, out _))
{
usedDictionary[usedFile.FullName] = true;
}
else
{
notFoundImage.Add(usedFile);
}
}
if (file.Extension is ".jpg" or ".jpeg" or ".png")
{
ImageInfo imageInfo =
await ImageJob.GetImageInfoAsync(MemorySource.Borrow(image), SourceLifetime.NowOwnedAndDisposedByTask);
foreach (KeyValuePair<string, bool> pair in usedDictionary.Where(p => !p.Value))
{
unusedImage.Add(new FileInfo(pair.Key));
}
});
return new BlogImageInfo(file, imageInfo.ImageWidth, imageInfo.ImageWidth, imageInfo.PreferredMimeType,
image, isUsed);
}
return new BlogImageInfo(file, 0, 0, file.Extension switch
{
"svg" => "image/svg",
"avif" => "image/avif",
_ => string.Empty
}, image, isUsed);
return Task.FromResult(new ImageScanResult(unusedImage.ToList(), notFoundImage.ToList()));
}
[GeneratedRegex(@"\!\[.*?\]\((.*?)\)")]
private static partial Regex ImagePattern { get; }
private DirectoryInfo ValidateRootDirectory()
private void ValidateDirectory(string root, out DirectoryInfo drafts, out DirectoryInfo posts)
{
DirectoryInfo rootDirectory = new(Path.Combine(Environment.CurrentDirectory, _blogOptions.Root));
root = Path.Combine(Environment.CurrentDirectory, root);
DirectoryInfo rootDirectory = new(root);
if (!rootDirectory.Exists)
{
throw new BlogFileException($"'{_blogOptions.Root}' is not a directory.");
throw new BlogFileException($"'{root}' is not a directory.");
}
return rootDirectory;
}
private void ValidateDirectory(out DirectoryInfo drafts, out DirectoryInfo posts)
{
if (RootDirectory.EnumerateDirectories().All(dir => dir.Name != "drafts"))
if (rootDirectory.EnumerateDirectories().All(dir => dir.Name != "drafts"))
{
throw new BlogFileException($"'{_blogOptions.Root}/drafts' not exists.");
throw new BlogFileException($"'{root}/drafts' not exists.");
}
if (RootDirectory.EnumerateDirectories().All(dir => dir.Name != "posts"))
if (rootDirectory.EnumerateDirectories().All(dir => dir.Name != "posts"))
{
throw new BlogFileException($"'{_blogOptions.Root}/posts' not exists.");
throw new BlogFileException($"'{root}/posts' not exists.");
}
drafts = new DirectoryInfo(Path.Combine(_blogOptions.Root, "drafts"));
posts = new DirectoryInfo(Path.Combine(_blogOptions.Root, "posts"));
drafts = new DirectoryInfo(Path.Combine(root, "drafts"));
posts = new DirectoryInfo(Path.Combine(root, "posts"));
}
}

View File

@@ -1,119 +0,0 @@
using Imageflow.Fluent;
using YaeBlog.Abstraction;
using YaeBlog.Core.Exceptions;
using YaeBlog.Models;
namespace YaeBlog.Services;
public sealed class ImageCompressService(IEssayScanService essayScanService, ILogger<ImageCompressService> logger)
{
private record struct CompressResult(BlogImageInfo ImageInfo, byte[] CompressContent);
public async Task<List<BlogImageInfo>> ScanUsedImages()
{
BlogContents contents = await essayScanService.ScanContents();
List<BlogImageInfo> originalImages = (from content in contents.Posts.Concat(contents.Drafts)
from image in content.Images
where image.IsUsed
select image).ToList();
originalImages.Sort();
return originalImages;
}
public async Task Compress(bool dryRun)
{
BlogContents contents = await essayScanService.ScanContents();
// 筛选需要压缩的图片
// 即图片被博客使用且是jpeg/png格式
List<BlogContent> needCompressContents = (from content in contents
where content.Images.Any(i => i is { IsUsed: true } and { File.Extension: ".jpg" or ".jpeg" or ".png" })
select content).ToList();
if (needCompressContents.Count == 0)
{
return;
}
int uncompressedSize = 0;
int compressedSize = 0;
List<BlogContent> compressedContent = new(needCompressContents.Count);
foreach (BlogContent content in needCompressContents)
{
List<BlogImageInfo> uncompressedImages = (from image in content.Images
where image is { IsUsed: true } and { File.Extension: ".jpg" or ".jpeg" or ".png" }
select image).ToList();
uncompressedSize += uncompressedImages.Select(i => i.Size).Sum();
foreach (BlogImageInfo image in uncompressedImages)
{
logger.LogInformation("Uncompressed image: {} belonging to blog {}.", image.File.Name,
content.BlogName);
}
CompressResult[] compressedImages = (await Task.WhenAll(from image in uncompressedImages
select Task.Run(async () => new CompressResult(image, await ConvertToWebp(image))))).ToArray();
compressedSize += compressedImages.Select(i => i.CompressContent.Length).Sum();
// 直接在原有的图片列表上添加图片
List<BlogImageInfo> images = content.Images.Concat(from r in compressedImages
select r.ImageInfo with
{
File = new FileInfo(r.ImageInfo.File.FullName.Split('.')[0] + ".webp"),
Content = r.CompressContent,
MineType = "image/webp"
}).ToList();
// 修改文本
string blogContent = compressedImages.Aggregate(content.Content, (c, r) =>
{
string originalName = r.ImageInfo.File.Name;
string outputName = originalName.Split('.')[0] + ".webp";
return c.Replace(originalName, outputName);
});
compressedContent.Add(content with { Images = images, Content = blogContent });
}
logger.LogInformation("Compression ratio: {}%.", (double)compressedSize / uncompressedSize * 100.0);
if (dryRun is false)
{
await Task.WhenAll(from content in compressedContent
select essayScanService.SaveBlogContent(content, content.IsDraft));
}
}
private static async Task<byte[]> ConvertToWebp(BlogImageInfo image)
{
using ImageJob job = new();
BuildJobResult result = await job.Decode(MemorySource.Borrow(image.Content))
.Branch(f => f.EncodeToBytes(new WebPLosslessEncoder()))
.EncodeToBytes(new WebPLossyEncoder(75))
.Finish()
.InProcessAsync();
// 超过128KB的图片使用有损压缩
// 反之使用无损压缩
ArraySegment<byte>? losslessImage = result.TryGet(1)?.TryGetBytes();
ArraySegment<byte>? lossyImage = result.TryGet(2)?.TryGetBytes();
if (image.Size <= 128 * 1024 && losslessImage.HasValue)
{
return losslessImage.Value.ToArray();
}
if (lossyImage.HasValue)
{
return lossyImage.Value.ToArray();
}
throw new BlogCommandException($"Failed to convert {image.File.Name} to webp format: return value is null.");
}
}

View File

@@ -1,62 +0,0 @@
using YaeBlog.Extensions;
using YaeBlog.Models;
namespace YaeBlog.Services
{
public class MarkdownWordCounter
{
private bool _inCodeBlock;
private int _index;
private readonly string _content;
private uint WordCount { get; set; }
private MarkdownWordCounter(BlogContent content)
{
_content = content.Content;
}
private void CountWordInner()
{
while (_index < _content.Length)
{
if (IsCodeBlockTag())
{
_inCodeBlock = !_inCodeBlock;
}
if (!_inCodeBlock && char.IsLetterOrDigit(_content, _index))
{
WordCount += 1;
}
_index++;
}
}
private bool IsCodeBlockTag()
{
// 首先考虑识别代码块
bool outerCodeBlock =
Enumerable.Range(0, 3)
.Select(i => _index + i < _content.Length && _content.AsSpan().Slice(_index + i, 1) is "`")
.All(i => i);
if (outerCodeBlock)
{
return true;
}
// 然后识别行内代码
return _index < _content.Length && _content.AsSpan().Slice(_index, 1) is "`";
}
public static uint CountWord(BlogContent content)
{
MarkdownWordCounter counter = new(content);
counter.CountWordInner();
return counter.WordCount;
}
}
}

View File

@@ -9,7 +9,7 @@ using YaeBlog.Models;
namespace YaeBlog.Services;
public sealed partial class RendererService(
public partial class RendererService(
ILogger<RendererService> logger,
IEssayScanService essayScanService,
MarkdownPipeline markdownPipeline,
@@ -38,26 +38,17 @@ public sealed partial class RendererService(
List<BlogEssay> essays = [];
foreach (BlogContent content in preProcessedContents)
{
(uint wordCount, string readTime) = GetWordCount(content);
DateTimeOffset publishDate = content.Metadata.Date is null
? DateTimeOffset.Now
: DateTimeOffset.Parse(content.Metadata.Date);
// 如果不存在最后的更新时间,就把更新时间设置为发布时间
DateTimeOffset updateTime = content.Metadata.UpdateTime is null
? publishDate
: DateTimeOffset.Parse(content.Metadata.UpdateTime);
uint wordCount = GetWordCount(content);
BlogEssay essay = new()
{
Title = content.Metadata.Title ?? content.BlogName,
FileName = content.BlogName,
Title = content.Metadata.Title ?? content.FileName,
FileName = content.FileName,
IsDraft = content.IsDraft,
Description = GetDescription(content),
WordCount = wordCount,
ReadTime = readTime,
PublishTime = publishDate,
UpdateTime = updateTime,
HtmlContent = content.Content
ReadTime = CalculateReadTime(wordCount),
PublishTime = content.Metadata.Date ?? DateTime.Now,
HtmlContent = content.FileContent
};
if (content.Metadata.Tags is not null)
@@ -165,17 +156,17 @@ public sealed partial class RendererService(
private string GetDescription(BlogContent content)
{
const string delimiter = "<!--more-->";
int pos = content.Content.IndexOf(delimiter, StringComparison.Ordinal);
int pos = content.FileContent.IndexOf(delimiter, StringComparison.Ordinal);
bool breakSentence = false;
if (pos == -1)
{
// 自动截取前50个字符
pos = content.Content.Length < 50 ? content.Content.Length : 50;
pos = content.FileContent.Length < 50 ? content.FileContent.Length : 50;
breakSentence = true;
}
string rawContent = content.Content[..pos];
string rawContent = content.FileContent[..pos];
MatchCollection matches = DescriptionPattern.Matches(rawContent);
StringBuilder builder = new();
@@ -191,21 +182,28 @@ public sealed partial class RendererService(
string description = builder.ToString();
logger.LogDebug("Description of {name} is {desc}.", content.BlogName,
logger.LogDebug("Description of {} is {}.", content.FileName,
description);
return description;
}
private (uint, string) GetWordCount(BlogContent content)
private uint GetWordCount(BlogContent content)
{
uint count = MarkdownWordCounter.CountWord(content);
int count = (from c in content.FileContent
where char.IsLetterOrDigit(c)
select c).Count();
logger.LogDebug("Word count of {blog} is {count}", content.BlogName,
logger.LogDebug("Word count of {} is {}", content.FileName,
count);
// 据说语文教学大纲规定中国高中生阅读现代文的速度是600字每分钟
uint second = count / 10;
TimeSpan span = new(0, 0, (int)second);
return (uint)count;
}
return (count, span.ToString("mm'分'ss'秒'"));
private static string CalculateReadTime(uint wordCount)
{
// 据说语文教学大纲规定中国高中生阅读现代文的速度是600字每分钟
int second = (int)wordCount / 10;
TimeSpan span = new(0, 0, second);
return span.ToString("mm'分 'ss'秒'");
}
}

View File

@@ -1,9 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<PackageReference Include="ImageFlow.NativeRuntime.ubuntu-x86_64" Version="2.1.0-rc11" Condition="$([MSBuild]::IsOsPlatform('Linux'))"/>
<PackageReference Include="ImageFlow.NativeRuntime.osx-arm64" Version="2.1.0-rc11" Condition="$([MSBuild]::IsOsPlatform('OSX'))"/>
<PackageReference Include="ImageFlow.Net" Version="0.13.2"/>
<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"/>
@@ -11,13 +8,26 @@
</ItemGroup>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<PropertyGroup>
<ClientAssetsRestoreCommand>pnpm install</ClientAssetsRestoreCommand>
<ClientAssetsBuildCommand>pwsh build.ps1 tailwind</ClientAssetsBuildCommand>
</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,4 +6,5 @@
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using YaeBlog
@using YaeBlog.Components

View File

@@ -37,7 +37,7 @@
"Name": "万木长风",
"Description": "世界渲染中...",
"Link": "https://ryohai.fun",
"AvatarImage": "https://ryohai.fun/static/favicons/favicon-32x32.png"
"AvatarImage": "https://ryohai.fun/icon.jpg"
}
]
}

View File

@@ -1,124 +0,0 @@
#!pwsh
[cmdletbinding()]
param(
[Parameter(Mandatory = $true, Position = 0, HelpMessage = "Specify the build target")]
[ValidateSet("tailwind", "publish", "compress", "build", "dev", "new")]
[string]$Target,
[string]$Output = "wwwroot",
[string]$Essay,
[switch]$Compress
)
begin {
Write-Host "Building $Target..."
if ($Target -eq "publish")
{
if ($Essay -eq "")
{
Write-Error "No publish target, please add with --essay argument."
exit 1
}
}
if ($Target -eq "new")
{
if ($Essay -eq "")
{
Write-Error "No new name, please add with --essay argument."
exit 1
}
}
}
process {
function Compress-Image
{
Write-Host "Compress image assets..."
dotnet run -- compress --dry-run
$confirm = Read-Host "Really compress images? (y/n)"
if ($confirm -notmatch "^[yY]$")
{
Write-Host "Not compress images."
return
}
Write-Host "Do compress image..."
dotnet run -- compress
dotnet run -- scan
$confirm = Read-Host "Really delete unused images? (y/n)"
if ($confirm -notmatch "^[yY]$")
{
Write-Host "Not delete images."
return
}
Write-Host "Do delete unused images.."
dotnet run -- scan --rm
}
function Build-Image
{
$commitId = git rev-parse --short=10 HEAD
dotnet publish
podman build . -t ccr.ccs.tencentyun.com/jackfiled/blog --build-arg COMMIT_ID=$commitId
}
function Start-Develop {
Write-Host "Start tailwindcss and dotnet watch servers..."
$pnpmProcess = Start-Process pnpm "tailwindcss -i wwwroot/tailwind.css -o obj/Debug/net10.0/ClientAssets/tailwind.g.css -w" `
-PassThru
try
{
Write-Host "Started pnpm process exit? " $pnpmProcess.HasExited
Start-Process dotnet "watch -- serve" -PassThru | Wait-Process
}
finally
{
if ($pnpmProcess.HasExited)
{
Write-Error "pnpm process has exited!"
exit 1
}
Write-Host "Kill tailwindcss and dotnet watch servers..."
$pnpmProcess | Stop-Process
}
}
switch ($Target)
{
"tailwind" {
Write-Host "Build tailwind css into $Output."
pnpm tailwindcss -i wwwroot/tailwind.css -o $Output/tailwind.g.css
break
}
"publish" {
Write-Host "Publish essay $Essay..."
dotnet run -- publish $Essay
if ($Compress)
{
Compress-Image
}
break
}
"compress" {
Compress-Image
break
}
"build" {
Build-Image
break
}
"dev" {
Start-Develop
break
}
"new" {
dotnet run -- new $Essay
}
}
}

View File

@@ -1,13 +1,13 @@
version: '3.8'
services:
blog:
image: registry.cn-beijing.aliyuncs.com/jackfiled/blog:latest
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.blog.rule=Host(`rrricardo.top`) || Host(`www.rrricardo.top`)"
- "traefik.http.services.blog.loadbalancer.server.port=8080"
- "traefik.http.routers.blog.tls=true"
- "traefik.http.routers.blog.tls.certresolver=myresolver"
- "com.centurylinklabs.watchtower.enable=true"
blog:
image: registry.cn-beijing.aliyuncs.com/jackfiled/blog:latest
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.blog.rule=Host(`rrricardo.top`) || Host(`www.rrricardo.top`)"
- "traefik.http.services.blog.loadbalancer.server.port=8080"
- "traefik.http.routers.blog.tls=true"
- "traefik.http.routers.blog.tls.certresolver=myresolver"
- "com.centurylinklabs.watchtower.enable=true"

View File

@@ -1,15 +1,12 @@
{
"name": "yae-blog",
"version": "1.0.0",
"description": "",
"scripts": {
"dev": "tailwindcss -i wwwroot/tailwind.css -o wwwroot/tailwind.g.css -w"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"tailwindcss": "^4.0.0",
"@tailwindcss/cli": "^4.0.0"
}
"name": "YaeBlog",
"version": "1.0.0",
"description": "",
"scripts": {},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"tailwindcss": "^3.4.16"
}
}

1089
YaeBlog/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

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

@@ -1,12 +1,10 @@
---
title: 2021年终总结
date: 2022-01-12T16:27:19.0000000
date: 2022-01-12 16:27:19
tags:
- 杂谈
- 年终总结
- 随笔
---
2021年已经过去2022年已经来临。每每一年开始的时候我都会展开一张纸或者新建一个文档思量着又是一年时光也该同诸大杂志一般写几句意味深长的话语怀念过去的时光也祝福未来的自己。可往往脑海中已是三万字的长篇落在笔头却又是一个字都没有了。
如今跨年的时候已经过去朋友圈中已经不见文案的踪影我也该重新提笔细说自己2021年中做过的种种。
@@ -24,7 +22,7 @@ tags:
在前12年的学生生涯中我们都在期待着这一次的暑假以为在这个没有作业的假期里我们就可以充分的享受人间的美好。可是当时我们不知道这人间的烦恼可不止作业这一种无论是突如其来的疫情导致开学延期还是等待录取时的不安。
虽说在暑假时,拥有了自己的笔记本电脑,可是在高中三年屯下的游戏还是没有玩几个,看来我也是“喜加一”的受害者。虽然在高考后入坑了原神,但是假期间我并没有太过投入的玩。
暑假下定决心要好好的学一学可是看着我gitee上暑假期间那稀疏的提交我就知道我又摸了一个暑假的鱼。
![gitee贡献](./2021-final/1.webp)
![gitee贡献](./2021-final/1.png)
即使我想写的很多项目都没有被扎实的推进下来但是学习的一些的C语言还是让我受益匪浅。
现在看来,这个假期真是,**学也没有学好,耍也没有耍好**的典型。

BIN
YaeBlog/source/posts/2021-final/1.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
YaeBlog/source/posts/2021-final/1.webp (Stored with Git LFS)

Binary file not shown.

View File

@@ -1,13 +1,11 @@
---
title: 2022年终总结
date: 2022-12-30T14:58:12.0000000
tags:
- 杂谈
- 年终总结
- 随笔
date: 2022-12-30 14:58:12
---
2022是困难的一年。我们需要为2023年做好准备。
<!--more-->
@@ -58,11 +56,11 @@ tags:
小小的总结一下2022年可以算得上是一事无成的一年还搞砸了不少的事情。在写代码上进展有限成绩上大幅倒退说好的六级英语和大学物理竞赛都没有参加在年末应对疫情进展的时候更是把“不知所措”这个成语诠释的淋漓尽致。
![](./2022-final/2022-12-30-14-26-19-QQ_Image_1672381538441.webp)
![](./2022-final/2022-12-30-14-26-19-QQ_Image_1672381538441.jpg)
关于今年的人际交往和社会关系我愿意用QQ2022年年终总结中的一张截屏来总结这张图片透漏出一种无可救药的悲伤。
![](./2022-final/2022-12-30-14-28-12-QQ_Image_1672381543836.webp)
![](./2022-final/2022-12-30-14-28-12-QQ_Image_1672381543836.jpg)
## 展望

Binary file not shown.

Binary file not shown.

View File

@@ -1,11 +1,11 @@
---
title: 2022年暑假碎碎念
date: 2022-08-22T15:39:13.0000000
tags:
- 杂谈
- 随笔
typora-root-url: 2022-summer-vacation
date: 2022-08-22 15:39:13
---
在8个月的漫长寒假的最后两个月~~也就是俗称的暑假中~~,我都干了些什么?
<!--more-->
@@ -32,7 +32,7 @@ tags:
- 下定决定要参加下一学期的物理竞赛,但是在听了讲座之后直接决定开学再开始学习,~~我知道我在家没法学习,俗称开摆~~
- 又捡起了`Blender`,并在[Github](https://github.com/tanjian1998/bupt_minecraft)上找到了伟大的前辈们在`Minecraft`里复刻的老校区,希望能用`Blender`渲染几张图当作桌面。
![唯一的一张成品](result1.webp)
![唯一的一张成品](result1.png)
> 在此感谢所有为此付出过汗水的前辈们,让我这个即将搬入老校区的萌新能提前一睹老校区的风采。

BIN
YaeBlog/source/posts/2022-summer-vacation/result1.png (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -1,12 +1,10 @@
---
title: 2023年年终总结
date: 2024-02-29T20:18:19.0000000
tags:
- 杂谈
- 年终总结
- 随笔
date: 2024-2-29 20:18:19
---
虽然2023年已经过去了两个月但是年终总结还是要发的。
<!--more-->
@@ -45,7 +43,7 @@ tags:
2023年最令我吃惊的事情是我刷B站的时长
![image-20240303165826486](2023-final/image-20240303165826486.webp)
![image-20240303165826486](2023-final/image-20240303165826486.png)
容易计算得出我一共看了64天的B站接近六分之一的时间都在看。虽然我确实有着在干活的时候黑听B站和把B站当作音乐播放器的习惯但是这个时间未免有点太长了。下一年一定要在这个方面做出一定的改变将更多的时间放在看书上面去~~虽然写这句话的时候我就在黑听B站~~。

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

Binary file not shown.

Binary file not shown.

View File

@@ -6,7 +6,6 @@ tags:
- 年终总结
---
欸,年终总结难道不是应该在新年当天发出吗,什么已经是新年第三天了?!
然而年末偶遇流感病毒,头疼脑热强如怪物,拼尽全力也无法战胜。
@@ -71,7 +70,7 @@ tags:
不过我的B站观看时长再度增长30%,这好吗,这不好,~~有这么多时间刷B站鬼知道你匆匆在哪了~~。
![image-20250115171809775](./2024-final/image-20250115171809775.webp)
![image-20250115171809775](./2024-final/image-20250115171809775.png)
### 未来

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

Binary file not shown.

Binary file not shown.

View File

@@ -1,224 +0,0 @@
---
title: ASP.NET Core中的静态Web资产
date: 2026-01-04T16:36:36.5629759+08:00
tags:
- 技术笔记
- dotnet
- ASP.NET Core
---
Web服务器应该如何扫描与提供静态Web文件尤其是在考虑到缓存、压缩的情况下还需要正确的处理开发环境和部署环境之间的差异让我们来看看ASP.NET Core是如何处理这个问题的。以及如何将通过其他工具例如`pnpm`生成的前端资产文件集成到ASP.NET Core中。
<!--more-->
### 引言——Blazor开发中的静态Web资源
Blazor是ASP.NET Core中~~新推出的~~Web应用程序开发框架通过一系列精巧的设计实现了使用HTML和C#编写运行在浏览器中的应用程序避免了使用丑陋的JavaScript。但是现代的前端开发生态几乎都建立在JavaScript之上尤其是考虑在JavaScript在很长的一段时间都是浏览器唯一支持的脚本语言在Blazor项目开发的过程中必然会遇到一些只能编写JavaScript才能解决的问题。同时一系列的现代前端工具例如[tailwindcss](https://tailwindcss.com/)提供了更加优秀的前端开发体验但是这些都基于NodeJS和NPM等前端工具。以上的前端生态引入了一个问题如何在MSBuild驱动的Blazor应用构建流程中自然地运行前端工具链和ASP.NET Core支持的服务器部署生成的静态资源
Blazor目前提供了一个入口简洁但是功能丰富的静态Web资源提供功能。在使用默认应用目录的情况下项目将会提供一个`wwwroot`文件夹,这个文件夹中的内容将可以从`/`直接寻址。为了提升前端静态文件的使用体验,该文件夹下的文件将会经过一个复杂的管道:
- 在构建扫描到这些资产文件之后MSBuild将会给静态文件加上内容指纹以防止重复使用旧文件。资源还会被压缩以减少资产交付的时间。
- 在运行时,所发现的资产文件将会作为终结点公开,并添加上合适的缓存头和内容类型头。在设置`ETtag``Last-Modified``Content-Type`头之后,浏览器将可以合理的缓存这些静态文件直到应用更新。
该静态文件功能还需要适应应用程序的部署状态:
- 在开发时,或者说运行`dotnet run``dotnet build`该功能需要将对应的静态文件终结点URL映射到磁盘上存储的实际静态文件上就像它们实际上就在`wwwroot`文件夹中一样。考虑到实际上开发过程中会用到Blazor内部的资产文件`blazor.web.js`,引用项目中的资产文件等等,这实际上一个相当复杂的检测-映射流程。
- 在发布时,或者说运行`dotnet publish`时,该功能需要收集所有需要的静态文件并复制到最终发布文件夹的`wwwroot`文件夹之下。
### Microsoft.AspNetCore.ClientAssets
在默认的应用模板下如果需要使用其他的现代前端工具生成静态资产文件最简单的方法就是手动或者编写MSBuild目标Target生成资产文件并放在`wwwroot`文件夹中。但是这个方法存在着如下几个问题:
- 开发者需要编写 MSBuild 目标targets来调用他们的工具。
- 开发者通常没有在构建过程的恰当时机执行其自定义目标。
- 开发者将工具生成的输出文件放入应用的 wwwroot 文件夹中,这会导致这些文件在下一次构建时被误认为是输入文件。
- 开发者没有为这些工具正确声明输入和输出依赖,导致即使输出文件已是最新,工具仍会重复运行,从而增加构建耗时。
面对这些问题M$提供了一个Alpha状态的库`Microsoft.AspNetCore.ClientAssets`来解决这个问题。不幸的是,这个库已经因为年久失修(上一次[更新](https://github.com/aspnet/AspLabs/pull/572)是在3年前引入对于.NET 7的支持在.NET 9引入新的静态资产部署管线之后使用会直接报错了。
^^ 相关的Issues链接[#38445](https://github.com/dotnet/aspnetcore/issues/38445)[#62925](https://github.com/dotnet/aspnetcore/issues/62925)
为了良好地解决如上的问题我们需要首先了解一下ASP.NET Core中静态资产文件的构建和部署过程。
### StaticWebAssetsSdk
在.NET中构建静态资产文件的相关代码在[dotnet/sdk](https://github.com/dotnet/sdk)仓库中,称作`StaticWebAssetsSdk`
静态 Web 资源会接管应用程序 wwwroot 文件夹中的内容项,并全面管理这些内容。在开发过程中,系统会生成一个 JSON 清单manifest其中包含以下信息
- 版本号version number标识清单格式的版本。
- 清单内容的哈希值hash用于判断清单内容是否发生变化。
- 库的包 IDlibrary package id用于区分当前项目与其他项目所提供的资源。
- 库的资源基础路径asset base path在将其他库的路径应用“发现模式”时用于确定要添加的基础路径。
- 清单模式manifest mode定义来自特定项目的资源在构建和发布时应如何处理。
- 相关项目清单及其哈希值的列表:用于判断自清单生成以来,项目引用是否发生变化,或是否有清单被更新。
- “发现模式”discovery patterns列表用于在清单构建完成后有选择性地在运行时提供某些资源。例如可以使用如下模式
```json
{ "Path": "<Project>/Pages", "BasePath": "_content/Something", "Pattern": "**/*.js" }
```
表示仅提供该目录下扩展名为`js`的文件。如果有人添加了图片或其他文件,它们将不会被提供。(这一点很重要,因为这些文件并不符合任何资源规则,也不会包含在发布输出目录中。)
- 构建/发布过程中生成的静态 Web 资源列表。
系统会生成两套清单:**构建清单build manifest** 和 **发布清单publish manifest**
- **构建清单**在构建过程中生成,用于开发阶段,使资源表现得如同它们属于应用程序本身。
- **发布清单**在发布过程中生成,记录了发布阶段对资源执行的所有转换操作。
资源可以在构建阶段或发布阶段定义,并可在任意阶段被标记为“仅构建”或“仅发布”。例如,你可以有两个文件:一个用于开发,一个用于发布,但它们都需要通过相同的 URL 路径提供服务。`service-worker.js` 就是一个典型例子。
**构建时清单**由项目中发现的资源以及来自被引用项目和包的资源共同组成。
**发布清单**则以构建清单为基础,过滤掉仅用于构建的文件,并包含在发布过程中对这些文件执行的所有转换(如链接、打包、压缩等)。
这种机制使得在发布阶段可以执行如链接Linking、打包Bundling、压缩Compression等优化操作。被引用的项目也会生成自己的发布清单其内容会在发布过程中与当前项目的清单合并。同时在发布过程中我们仍会保留被引用项目的原始构建清单以便应用程序可以选择忽略被引用项目的发布资源并对整个依赖传递闭包中的资源执行全局优化。例如一个类库在发布时可能生成一个压缩后的 JS 包,而主应用可以选择不使用多个独立的包,而是收集所有原始构建阶段的资源,生成一个统一的包。通常情况下,构建清单和发布清单内容相同,除非存在仅在发布阶段才应用的转换。
每份清单中会列出在构建/发布过程中生成或计算出的所有资源及其属性。这些属性包括:
- **Identity**:资源的唯一标识(文件的完整路径)。
- **SourceType**:资源类型('Discovered', 'Computed', 'Project', 'Package')。
- **ContentRoot**:开发阶段资源暴露的原始路径。
- **BasePath**:资源暴露的基础路径。
- **RelativePath**:资源的相对路径。
- **AssetKind**:资源用途('Build', 'Publish', 'All'),由 `CopyToOutputDirectory` / `CopyToPublishDirectory` 推断得出。
- **AssetMode**:资源作用范围('CurrentProject', 'Reference', 'All')。
- **AssetRole**:资源角色('Primary', 'Related', 'Alternative')。
- **AssetMergeSource**:当资源被嵌入到其他 TFM目标框架时的来源。
- **AssetMergeBehavior**:当同一 TFM 中出现资源冲突时的合并行为。
- **RelatedAsset**:当前资源所依赖的主资源的 Identity。
- **AssetTraitName**:区分相关或替代资源与主资源的特征名称(如语言、编码格式等)。
- **AssetTraitValue**:该特征的具体值。
- **CopyToBuildDirectory**:与 Content 项一致(如 PreserveNewest、Always
- **CopyToPublishDirectory**:与 Content 项一致。
- **OriginalItemSpec**:定义该资源的原始项规范。
关于资源在不同场景下的使用(作为主项目的一部分,或作为被引用项目的一部分),有三种可能的选项:
- **All**:资源在所有情况下都应被使用。
- **Root**:资源仅在当前项目作为主项目构建时使用。
- **Reference**:资源仅在当前项目被其他项目引用时使用。
例如CSS 隔离CSS isolation生成的两个包
- `<<Project>>.styles.css` 是 **Root** 资源,仅在作为主项目时使用。
- `<<Project>>.lib.bundle.css` 是 **Reference** 资源,仅在被其他项目引用时使用。
除了上述三种使用模式,项目还需定义其在构建和发布过程中如何处理清单中的文件。对此有三种模式:
- **Default**:项目在发布时将所有内容复制到发布输出目录,但当被其他项目引用时不做任何操作,而是期望引用方负责处理静态 Web 资源的发布。
→ 通常用于类库class libraries
- **Root**:项目被视为静态 Web 资源的“根”,即使被引用,其资源也应像主项目一样被处理(例如,不复制传递依赖资源,而只复制 Root 资源)。
→ 用于如 Blazor WebAssembly 托管项目(被 ASP.NET Core 主机项目引用,但资源应视为根项目)。
- **Isolated**:与 Root 类似,但引用项目完全不知道静态 Web 资源的存在;当前项目会自行在发布时设置处理程序,将资源复制到正确位置。
→ 用于如 Blazor 桌面应用,将静态 Web 资源自动纳入 `GetCopyToPublishDirectoryItems`,使引用方无需了解静态 Web 资源机制。
关于资源类型,静态 Web 资源可分为四类:
- **Discovered assets**:从项目中已有项(如 Content、None 等)中发现的资源。
- **Computed assets**:在构建过程中生成、需要在构建时复制到最终位置的资源。
- **Project**:来自被引用项目的资源。当合并被引用项目的清单时,其 Discovered 和 Computed 资源会转换为 Project 类型。
- **Package**:来自被引用 NuGet 包的资源。
关于资源角色Asset Role有三种
- **Primary主资源**:表示可通过其相对路径直接访问的资源。大多数资源属于此类。
- **Related相关资源**:表示与另一个资源相关,但两者都可通过各自的相对路径独立访问。
- **Alternative替代资源**:表示是另一个资源的替代形式,例如预压缩版本或不同格式版本。通常应通过与主资源相同的相对路径提供(具体实现由运行时决定)。静态 Web 资源层仅记录这种关系。
对于 Related 和 Alternative 资源,其 `RelatedAsset` 属性指向其所依赖的主资源。这种依赖链可多层嵌套,以表示一个资源的多种表示形式。静态 Web 资源仅记录这些信息,具体如何使用由 MSBuild 目标决定。
`AssetTraitName` 和 `AssetTraitValue` 用于区分相关/替代资源与其主资源。例如:
- 对于全球化程序集可记录程序集的文化culture
- 对于压缩资源,可记录编码方式(如 gzip、brotli
下图展示了在构建过程中被调用的MSBuild Target
![image-20251231225433184](./aspnetcore-swa/image-20251231225433184.webp)
Sdk提供了一些重要的MSBuild Task供程序员调用
- `DefineStaticWebAssets`该Task扫描提供了一系列候选的资产文件并构建一个*标准化的*静态资产对象;
- `DefineStaticWebAssetEndpoints`该Task以上一个任务输出的静态资产对象为输入输出每个静态资产文件的Web终结点
在构建中过程中`GenerateStaticWebAssetsManifest`和`GenerateStaticWebAssetsDevelopmentManifest`、`GenerateStaticWebAssetEndpointsManifest`等几个任务会产生一个重要的清单文件,这些文件通常存放在*obj*文件夹中,名称为`staticwebassets.*.json`。其中一个较为重要的清单文件是`staticwebassets.development.json`,其存储了所有的静态资产文件和对应的存储目录。这个文件在构建的过程中会被复制到输出目录`bin`中,名称为`$(PackageId).staticwebassets.runtime.json`。这个文件将会在生产模式下被静态资产中间件读取,作为建立静态文件终结点到实际物理文件的索引。这个文件也为需要调试`StaticWebAssetsSdk`的程序员提供了重要的调试信息是解决ASP.NET Core中静态资产问题的不二法门。
### 解决方案
现在已经充分了解了`StaticWebAssetsSdk`可以来设计在MSBuild中集成前端工具并生成最终静态资产文件的管线了。
首先来研究过程的步骤,`npm`或者其类似物也使用类似于MSBuild的先还原再构建两步首先需要安装程序中使用到的包然后在运行构建指令构建对应的静态文件构建完成之后还需要将构建产物交给MSBuild中的静态资产处理管线进行进一步的处理。因此设计如下的三个步骤
1. `RestoreClientAssets`这个Target需要运行`npm install`或者类似的指令安装依赖包;
2. `BuildClientAssets`这个Target运行`npm run build`或者类似的指令构建项目;
3. `DefineClientAssets`这个Target调用`DefineStaticWebAssets`等Task声明静态资产文件。
确定好生成步骤之后,声明一下会在生成过程中会用到的,可以提供给用户自定义的属性。安装和构建的相应软件包肯定是需要提供给用户自定义的。在一般情况下,前端工具链把将静态文件生成到`dist`文件夹中。为了符合MSBuild的惯例这里将中间静态文件生成到*obj*文件夹下的`ClientAssets`文件夹中。为了实现这一点,构建过程中的指令就需要支持一个指定生成目录的参数,这个参数也作为一个属性暴露给用户可以自定义。这里就形成了下面三个提供给用户自定义的参数。
```xml
<PropertyGroup>
<ClientAssetsRestoreCommand Condition="'$(ClientAssesRestoreCommand)' == ''">pnpm install</ClientAssetsRestoreCommand>
<ClientAssetsBuildCommand Condition="'$(ClientAssetsBuildCommand)' == ''">pnpm run build</ClientAssetsBuildCommand>
<ClientAssetsBuildOutputParameter Condition="'$(ClientAssetsBuildOutputParameter)' == ''">--output</ClientAssetsBuildOutputParameter>
</PropertyGroup>
```
最终就是完成的构建原始代码了。第一个运行的构建目标`RestoreClientAssets`将会在`DispatchToInnerBuilds`任务运行之后运行这个目标是MSBuild构建管线中一个不论是针对单架构生成还是多架构生成都只会运行一次的目标这样在项目需要同时编译到.NET 8和.NET 10的情况下仍然只会运行前端的安装命令一次。`BuildClientAssets`目标紧接着`RestoreClientAssets`目标的运行而运行,并将所有生成的前端文件添加到`_ClientAssetsBuildOutput`项中。最终的`DefineClientAssets`目标在负责解析项目中的所有静态文件的目标`ResolveWebAssetsConfiguration`运行之前运行,调用`DefineStaticWebAssets`和`DefineStaticWebAssetEndpoints`将前面生成的所有前端静态文件添加到MSBuild的静态文件处理管线中。
```xml
<PropertyGroup>
<_RestoreClientAssetsBeforeTargets Condition="'$(TargetFramework)' == ''">DispatchToInnerBuilds</_RestoreClientAssetsBeforeTargets>
</PropertyGroup>
<Target Name="RestoreClientAssets" BeforeTargets="$(_RestoreClientAssetsBeforeTargets)">
<Message Importance="high" Text="Running $(ClientAssetsRestoreCommand)"/>
<Exec Command="$(ClientAssetsRestoreCommand)"/>
</Target>
<Target Name="BuildClientAssets" DependsOnTargets="RestoreClientAssets" BeforeTargets="AssignTargetPaths">
<PropertyGroup>
<_ClientAssetsOutputFullPath>$([System.IO.Path]::GetFullPath('$(IntermediateOutputPath)ClientAssets'))</_ClientAssetsOutputFullPath>
</PropertyGroup>
<MakeDir Directories="$(_ClientAssetsOutputFullPath"/>
<Exec Command="$(ClientAssetsBuildCommand) -- $(ClientAssetsBuildOutputParameter) $(_ClientAssetsOutputFullPath)"/>
<ItemGroup>
<_ClientAssetsBuildOutput Include="$(IntermediateOutputPath)ClientAssets\**"/>
</ItemGroup>
</Target>
<Target Name="DefineClientAssets" AfterTargets="BuildClientAssets" DependsOnTargets="ResolveWebAssetsConfiguration">
<ItemGroup>
<FileWrites Include="@(_ClientAssetsBuildOutput)"/>
</ItemGroup>
<DefineStaticWebAssets
CandidateAssets="@(_ClientAssetsBuildOutput)"
SourceId="$(PackageId)"
SourceType="Computed"
ContentRoot="$(_ClientAssetsOutputFullPath)"
BasePath="$(StaticWebAssetBasePath)"
>
<Output TaskParameter="Assets" ItemName="StaticWebAsset"/>
<Output TaskParameter="Assets" ItemName="_ClientAssetsStaticWebAsset"/>
</DefineStaticWebAssets>
<DefineStaticWebAssetEndpoints
CandidateAssets="@(_ClientAssetsStaticWebAsset)"
ContentTypeMappings="@(StaticWebAssetContentTypeMapping)"
>
<Output TaskParameter="Endpoints" ItemName="StaticWebAssetEndpoint" />
</DefineStaticWebAssetEndpoints>
</Target>
```
为了测试如下的代码,可以在项目中新建一个`Directory.Build.targets`文件,将上述的内容复制进去进行测试,当然别忘了用`<Project>`标签包裹这一切。

Binary file not shown.

View File

@@ -1,11 +1,11 @@
---
title: 人生代码大作业初体验
date: 2022-07-27T11:34:49.0000000
tags:
- 杂谈
- 随笔
typora-root-url: big-homework
date: 2022-07-27 11:34:49
---
在大学也呆了一年了,终于遇上了第一个需要多人合作的写代码项目。从四月底分组完成,任务部署下来到七月初接近尾声,在这两个多月的时间里,也算是经历了不少,学到了不少。
<!--more-->
@@ -44,7 +44,7 @@ tags:
而且采用 `Git`还有一个好处,采用 `Github``Insight`功能可以轻松的看出大家的贡献值()。
![img](1.webp)
![img](1.png)
## 一些技术上的收获

BIN
YaeBlog/source/posts/big-homework/1.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
YaeBlog/source/posts/big-homework/1.webp (Stored with Git LFS)

Binary file not shown.

View File

@@ -1,12 +1,12 @@
---
title: 建立博客过程的记录
date: 2022-04-08T11:52:32.0000000
typora-root-url: 建立博客过程的记录
date: 2022-04-08 11:52:32
tags:
- 技术笔记
- 技术笔记
---
当我已经在Python的浩瀚大海遨zhengzha了半个暑假后我决定尝试一下传说中程序员专用的学(zhuang)习(bi)手(fangfa)段(fa)——建立自己的个人博客。作为一个半懂不懂的Python程序员心中冒出的第一个想法自然是采用Python的Django作为开发自己的个人博客的手段。然而在阅读了[用Django搭建个人博客](https://www.dusaiphoto.com/article/2/)等的其他人搭建这类动态博客的过程记录之后我便义无反顾的转向了采用javascript开发的博客框架[Hexo](https://hexo.io)<del>说好的Python信仰呢</del>。无他,唯简单尔。
<!--more-->
@@ -131,7 +131,7 @@ Hexo init blog
```
Hexo会以blog为名称创建一个博客文件夹这个文件夹的内容为
![文件夹截图](1.webp)
![文件夹截图](1.png)
`node_modules`文件夹是Hexo需要用到的一些npm依赖包的存放地址`public`文件夹下是由Hexo渲染产生的静态博客文件`scaffolds`文件夹是博客用到的模板文件,在默认情况下应该有`draft.md`,`page.md`,`post.md`三个模板文件。`themes`是Hexo中可以使用的主题文件。主题也是Hexo一个非常方便的设计我们可以方便使用其他人编写的Hexo Themes让自己的博客在不同的风格之间变换。`source`文件夹就是存放我们写作的博客的地方。一般这里面会有两个子文件夹,`_draft`, `_posts`。我们在里面在创建一个`img`文件夹,把自己的头像图片和网站的图标文件都放在里面,在之后的设置的时候使用。
@@ -146,7 +146,7 @@ INFO Hexo is running at http://localhost:4000/ . Press Ctrl+C to stop.
会在本地运行Hexo自带的一台静态博客服务器。我们用浏览器访问http://localhost:4000, 就可以看见Hexo博客的初始界面
![初始截图](2.webp)
![初始截图](2.png)
这便说明安装成功了,~~可以开香槟了~~

BIN
YaeBlog/source/posts/build-blog-record/1.png (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

BIN
YaeBlog/source/posts/build-blog-record/2.png (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -7,7 +7,6 @@ tags:
---
我们编译是这样的,在本平台上编译只要敲三条命令就好了,而交叉编译要考虑的就很多了。
<!--more-->
@@ -46,7 +45,7 @@ tags:
通常一份GNU工具链只能针对一个平台进行编译但是LLVM工具链是一套先天的交叉编译工具链例如对于`llc`工具,使用`llc --version`命令可以看见该编译器可以生成多种目标平台上的汇编代码:
![image-20240824120646587](./build-dotnet-from-source/image-20240824120646587.webp)
![image-20240824120646587](./build-dotnet-from-source/image-20240824120646587.png)
在使用`clang++`时加上`--target=<triple>`指定目标三元组就可以进行交叉编译。
@@ -63,7 +62,7 @@ int main()
}
```
![image-20240824121425007](./build-dotnet-from-source/image-20240824121425007.webp)
![image-20240824121425007](./build-dotnet-from-source/image-20240824121425007.png)
看样子交叉编译也不是开箱即用的。最开始我们猜想系统提供的LLVM工具链没有被配置为交叉编译因此尝试在本地自行编译一套LLVM工具链。
@@ -82,7 +81,7 @@ cmake ../llvm-project.src/llvm \
编译之后的成果会安装到`/usr/local/`目录下,而在`$PATH`环境变量中`/usr/local`位置将在`/usr`目录之前因此调用时将会优先调用我们自行编译的LLVM工具链而不是系统中安装的LLVM工具链。
![image-20240824134158262](./build-dotnet-from-source/image-20240824134158262.webp)
![image-20240824134158262](./build-dotnet-from-source/image-20240824134158262.png)
但是使用这套编译工具链仍然会爆出和之前一样的问题。说明这并不是系统安装LLVM工具链的问题。仔细一想也确实这里提示找不到对应的头文件应该是找不到RISC-V架构之下的头文件——这里的也是交叉编译的主要问题所在虽然LLVM工具链宣称自己是原生支持交叉编译的但是没人宣称说标准库和头文件是原生的。这里我们就需要一个根文件系统来提供这些头文件和各种库文件。
@@ -199,7 +198,7 @@ clang++ --target=riscv64-linux-gnu --sysroot=$ROOTFS_DIR -fuse-ld=lld hello.cpp
第一个问题的回答是Arch Linux安装的LLVM工具是可以交叉编译的。虽然在Arch Linux官方构建LLVM工具链的[构建脚本](https://gitlab.archlinux.org/archlinux/packaging/packages/clang/-/blob/main/PKGBUILD?ref_type=heads)中没有使用`LLVM_TARGETS_TO_BUILD`参数,但是这个参数的默认值是`all`。这一点我们也可以通过实验来验证。
![image-20240824153514149](./build-dotnet-from-source/image-20240824153514149.webp)于是回到编译`llvm`的目录下执行`cat install_manifest.txt | sudo xargs rm`
![image-20240824153514149](./build-dotnet-from-source/image-20240824153514149.png)于是回到编译`llvm`的目录下执行`cat install_manifest.txt | sudo xargs rm`
第二个问题的回答可以使用实验来验证,首先安装`riscv64-linux-gnu-gcc`,然后将根文件系统的位置设置为`/usr/riscv64-linux-gnu`,重新编译上面的你好世界样例。编译之后可以正常执行。
@@ -230,4 +229,4 @@ export ROOTFS_DIR=<rootfs>
但是现在的.NET在RISC-V平台上还是废物一个甚至连`dotnet new`都跑不过,下一步看看能不能运行一下运行时的测试集看看。
![image-20240824214145759](./build-dotnet-from-source/image-20240824214145759.webp)
![image-20240824214145759](./build-dotnet-from-source/image-20240824214145759.png)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,12 +1,12 @@
---
title: C项目中有关头文件的一些问题
date: 2022-05-08T11:35:19.0000000
tags:
- 技术笔记
- C/C++
- 技术笔记
- C/C++
typora-root-url: c-include-problems
date: 2022-05-08 11:35:19
---
最近在完成一门`C`语言课程的大作业,课设老师要求我们将程序分模块的开发。在编写项目头文件的时候,遇到了一些令本菜鸡大开眼界的问题。
<!--more-->
@@ -17,7 +17,7 @@ tags:
我项目的结构大致如图所示:
![](1.webp)
![](1.png)
`include`的头文件目录下有两个头文件,`rail.h``bus.h`,这两个头文件分别定义了两个结构体`rail_node_t``bus_t`
@@ -68,7 +68,7 @@ typedef struct bus bus_t;
项目的`test`文件夹下是单元测试文件夹,但是在编译的时候会报错
![](2.webp)
![](2.png)
大意就是在一个google test内部的头文件中有几个函数找不到定义这个函数都位于`io.h`这个头文件中。

BIN
YaeBlog/source/posts/c-include-problems/1.png (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

BIN
YaeBlog/source/posts/c-include-problems/2.png (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -5,14 +5,13 @@ tags:
- 杂谈
---
2024年的中国计算机大会于10月24日到10月26日在浙江省金华市东阳市横店镇举办而鄙人在下不才我有幸受到实验室资助前去参观学习。
<!--more-->
首先开幕式镇楼。
![image-20241102212738598](./cncc-2024/image-20241102212738598.webp)
![image-20241102212738598](./cncc-2024/image-20241102212738598.png)
## 学术上
@@ -22,11 +21,11 @@ tags:
第一个报告是华为庞加莱实验室秦彬娟老师的《异构智算时代的操作系统演进》。报告高屋建瓴从比较宏观的角度上介绍了当前异构融合操作系统诞生的背景、发展的方向。在报告中重点介绍了一种异构融合操作系统的设计思路通过三层架构基于互联池化技术构建AI时代的融合算力系统。系统中的三层包括1池化基础底层包括多设备的融合和池化设备虚拟化2异构融合核心子系统例如异构融合调度系统、异构融合内存和异构融合存储系统3异构核心服务。总的来说这个报告在一定程度上勾勒出了未来一个异构融合操作系统应有的各项功能但是显然这一操作系统的实现还存在着明显的困难。
![image-20241102211959206](./cncc-2024/image-20241102211959206.webp)
![image-20241102211959206](./cncc-2024/image-20241102211959206.png)
下面一个报告是较为有干货的报告北京航空航天大学刘瀚骋老师的《异构融合OS及多样性内存管理框架》。报告中介绍了一个称作`FMMU`的系统是对于异构融合操作系统中内存管理系统的探索。报告中首先介绍了内存池化技术对于异构融合操作系统的重要性指出分布式共享内存Distributed Shared Memory可能是实现内存池化技术的未来。然后介绍了将部分内存管理中的计算卸载到可编程网络硬件中来加速分布式内存访问的新思路。最后在报告中提到了内存管理技术如何解决错误预测和错误回复的问题。虽然在听的时候没太注意但是现在总结的时候才发现这个报告的思路似乎有点混乱尤其是最后一点和内存管理系统并没有什么直接的关系而且这个内存管理系统似乎不是**异构系统**的内存管理,反而是分布式系统的内存管理。不过总的来说,这个报告还是非常实际的,介绍了不少当前异构融合操作系统中的内存管理面临的问题和解决问题的探索。
![image-20241102212355390](./cncc-2024/image-20241102212355390.webp)
![image-20241102212355390](./cncc-2024/image-20241102212355390.png)
第三个报告是国防科技大学李东升老师的《异构计算环境下的分布式深度学习训练》。报告首先从李老师的主业——并行计算起手,介绍了深度学习训练过程中主要的各种并行方法,例如数据并行、模型并行和混合并行等,指出目前大模型的并行训练存在着计算/存储/通信难的问题。因此提出了一个智能模型训练并行任务划分方法1基于符号算子的计算图定义方法2面向Transformer模型的流水线并行任务划分方法3异构资源感知的流水线并行任务划分方法。然后针对分布式模型训练中通信调度存在的通信墙、数据依赖关系复杂等的问题提出综合词嵌入表的稀疏通信调度技术、流水线并行的P2P通信调度技术、模型计算的统一操作执行引擎和网络链路感知的通信执行引擎的通信调度技术。最后提到了智能模型训练 的内存优化技术针对现有重计算技术re-computing和存储交换swapping技术存在的问题提出了一种面向大型智能模型训练的细粒度内存优化方法`DELTA`
@@ -50,7 +49,7 @@ Plane讨论没有参加。
第二个报告是南京大学冯新宇老师的《基于仓颉语言的嵌入式DSL开发》同时冯新宇老师也是仓颉语言的首席架构师。冯老师的这个报告主要聚焦于仓颉语言提供的嵌入式DSL能力而嵌入式DSL这一设计范式已经在前端开发中展现了不俗的潜力。报告中介绍了嵌入式DSL出现的背景仓颉中为了提供嵌入式DSL而引入的语法糖、仓颉提供的嵌入式DSL工具箱等。虽然仓颉语言是一个主要面向上层应用开发的语言但是仓颉中丰富的DSL能力还是给异构编程模型的设计提供了不少的启发。而且目前在各种深度学习编译器中DSL的应用也非常广泛例如`triton`
![image-20241102212536635](./cncc-2024/image-20241102212536635.webp)
![image-20241102212536635](./cncc-2024/image-20241102212536635.png)
第三个报告是在存算一体的芯片上做数据库的加速第四个报告是OpenHarmony上`ArkTS`程序的静态分析,都没怎么听。

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

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

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