Compare commits
8 Commits
master
...
write-asyn
Author | SHA1 | Date | |
---|---|---|---|
8c2cce59e4 | |||
baf50eeab0 | |||
dc1b97fed4 | |||
9bfe091024 | |||
3a4ada50c6 | |||
05a22a0b29 | |||
6797028cc1 | |||
77e52fa11e |
2
.gitattributes
vendored
2
.gitattributes
vendored
|
@ -1,4 +1,2 @@
|
|||
*.png filter=lfs diff=lfs merge=lfs -text
|
||||
*.jpg filter=lfs diff=lfs merge=lfs -text
|
||||
*.avif filter=lfs diff=lfs merge=lfs -text
|
||||
*.webp filter=lfs diff=lfs merge=lfs -text
|
||||
|
|
|
@ -7,7 +7,7 @@ jobs:
|
|||
Build-Blog-Image:
|
||||
runs-on: archlinux
|
||||
steps:
|
||||
- uses: https://mirrors.rrricardo.top/actions/checkout.git@v4
|
||||
- uses: https://git.rrricardo.top/actions/checkout@v4
|
||||
name: Check out code
|
||||
with:
|
||||
lfs: true
|
||||
|
@ -18,16 +18,12 @@ jobs:
|
|||
- name: Build docker image
|
||||
run: |
|
||||
cd YaeBlog
|
||||
podman build . -t registry.cn-beijing.aliyuncs.com/jackfiled/blog:latest --build-arg COMMIT_ID=$(git rev-parse --short=10 HEAD)
|
||||
- name: Workaround to make sure podman login succeed
|
||||
run: |
|
||||
mkdir /root/.docker
|
||||
docker build . -t registry.cn-beijing.aliyuncs.com/jackfiled/blog:latest
|
||||
- name: Login aliyun docker registry
|
||||
uses: https://mirrors.rrricardo.top/actions/podman-login.git@v1
|
||||
uses: https://git.rrricardo.top/actions/login-action@v3
|
||||
with:
|
||||
registry: registry.cn-beijing.aliyuncs.com
|
||||
username: 初冬的朝阳
|
||||
password: ${{ secrets.ALIYUN_PASSWORD }}
|
||||
auth_file_path: /etc/containers/auth.json
|
||||
- name: Push docker image
|
||||
run: podman push registry.cn-beijing.aliyuncs.com/jackfiled/blog:latest
|
||||
run: docker push registry.cn-beijing.aliyuncs.com/jackfiled/blog:latest
|
||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -484,4 +484,4 @@ $RECYCLE.BIN/
|
|||
*.swp
|
||||
|
||||
# Tailwind auto-generated stylesheet
|
||||
*.g.css
|
||||
output.css
|
||||
|
|
41
YaeBlog.sln
Normal file
41
YaeBlog.sln
Normal 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
|
14
YaeBlog.slnx
14
YaeBlog.slnx
|
@ -1,14 +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/YaeBlog.csproj" />
|
||||
</Solution>
|
|
@ -7,4 +7,6 @@ public interface IEssayScanService
|
|||
public Task<BlogContents> ScanContents();
|
||||
|
||||
public Task SaveBlogContent(BlogContent content, bool isDraft = true);
|
||||
|
||||
public Task<ImageScanResult> ScanImages();
|
||||
}
|
||||
|
|
|
@ -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>();
|
||||
}
|
||||
}
|
|
@ -1,6 +1,4 @@
|
|||
using System.CommandLine;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Commands.Binders;
|
||||
using YaeBlog.Components;
|
||||
using YaeBlog.Extensions;
|
||||
|
@ -21,7 +19,6 @@ public sealed class YaeBlogCommand
|
|||
AddNewCommand(_rootCommand);
|
||||
AddPublishCommand(_rootCommand);
|
||||
AddScanCommand(_rootCommand);
|
||||
AddCompressCommand(_rootCommand);
|
||||
}
|
||||
|
||||
public Task<int> RunAsync(string[] args)
|
||||
|
@ -97,20 +94,22 @@ 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 = DateTime.Now },
|
||||
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>(),
|
||||
|
@ -127,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());
|
||||
}
|
||||
|
@ -151,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);
|
||||
}
|
||||
|
@ -201,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)
|
||||
|
@ -210,17 +202,14 @@ public sealed class YaeBlogCommand
|
|||
return;
|
||||
}
|
||||
|
||||
// 设置发布的时间
|
||||
content.Metadata.Date = DateTime.Now;
|
||||
|
||||
// 将选中的博客文件复制到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)
|
||||
{
|
||||
|
@ -234,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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<link rel="stylesheet" href="YaeBlog.styles.css"/>
|
||||
<link rel="icon" href="images/favicon.ico"/>
|
||||
<link rel="stylesheet" href="globals.css"/>
|
||||
<link rel="stylesheet" href="tailwind.g.css"/>
|
||||
<link rel="stylesheet" href="output.css"/>
|
||||
<HeadOutlet/>
|
||||
</head>
|
||||
|
||||
|
|
|
@ -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}";
|
||||
}
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
FROM mcr.microsoft.com/dotnet/aspnet:9.0
|
||||
|
||||
ARG COMMIT_ID
|
||||
ENV COMMIT_ID=${COMMIT_ID}
|
||||
|
||||
WORKDIR /app
|
||||
COPY bin/Release/net9.0/publish/ ./
|
||||
COPY source/ ./source/
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,15 +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)
|
||||
{
|
||||
IEnumerator<BlogContent> IEnumerable<BlogContent>.GetEnumerator()
|
||||
{
|
||||
return Posts.Concat(Drafts).GetEnumerator();
|
||||
}
|
||||
public ConcurrentBag<BlogContent> Drafts { get; } = drafts;
|
||||
|
||||
public IEnumerator GetEnumerator() => ((IEnumerable<BlogContent>)this).GetEnumerator();
|
||||
public ConcurrentBag<BlogContent> Posts { get; } = posts;
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
3
YaeBlog/Models/ImageScanResult.cs
Normal file
3
YaeBlog/Models/ImageScanResult.cs
Normal file
|
@ -0,0 +1,3 @@
|
|||
namespace YaeBlog.Models;
|
||||
|
||||
public record struct ImageScanResult(List<FileInfo> UnusedImages, List<FileInfo> NotFoundImages);
|
|
@ -1,7 +1,6 @@
|
|||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
using YaeBlog.Abstraction;
|
||||
using YaeBlog.Extensions;
|
||||
using YaeBlog.Models;
|
||||
|
||||
namespace YaeBlog.Processors;
|
||||
|
@ -21,21 +20,20 @@ public sealed class EssayStylesPostRenderProcessor : IPostRenderProcessor
|
|||
|
||||
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" },
|
||||
{ "ul", "list-disc pl-2" }
|
||||
};
|
||||
|
||||
private void ApplyGlobalCssStyles(IDocument document)
|
||||
|
@ -101,45 +99,4 @@ public sealed class EssayStylesPostRenderProcessor : IPostRenderProcessor
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void BeatifyList(IDocument document)
|
||||
{
|
||||
foreach (IElement ulElement in from e in document.All
|
||||
where e.LocalName == "ul"
|
||||
select e)
|
||||
{
|
||||
// 首先给<ul>元素添加样式
|
||||
ulElement.ClassList.Add("list-disc ml-10");
|
||||
|
||||
|
||||
foreach (IElement liElement in from e in ulElement.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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
{
|
||||
|
|
|
@ -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,92 +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)
|
||||
{
|
||||
int endPos = blog.BlogContent.IndexOf("---", 4, StringComparison.Ordinal);
|
||||
if (!blog.BlogContent.StartsWith("---") || endPos is -1 or 0)
|
||||
int endPos = content.IndexOf("---", 4, StringComparison.Ordinal);
|
||||
if (!content.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: '{}' for {}.", 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 {} due to {}, skipping", blog.BlogFile.Name, e);
|
||||
logger.LogWarning("Failed to parser metadata from {} due to {}, skipping", filename, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -136,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"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.");
|
||||
}
|
||||
}
|
|
@ -41,14 +41,14 @@ public partial class RendererService(
|
|||
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 = CalculateReadTime(wordCount),
|
||||
PublishTime = content.Metadata.Date ?? DateTime.Now,
|
||||
HtmlContent = content.Content
|
||||
HtmlContent = content.FileContent
|
||||
};
|
||||
|
||||
if (content.Metadata.Tags is not null)
|
||||
|
@ -156,17 +156,17 @@ public 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();
|
||||
|
@ -182,18 +182,18 @@ public partial class RendererService(
|
|||
|
||||
string description = builder.ToString();
|
||||
|
||||
logger.LogDebug("Description of {} is {}.", content.BlogName,
|
||||
logger.LogDebug("Description of {} is {}.", content.FileName,
|
||||
description);
|
||||
return description;
|
||||
}
|
||||
|
||||
private uint GetWordCount(BlogContent content)
|
||||
{
|
||||
int count = (from c in content.Content
|
||||
int count = (from c in content.FileContent
|
||||
where char.IsLetterOrDigit(c)
|
||||
select c).Count();
|
||||
|
||||
logger.LogDebug("Word count of {} is {}", content.BlogName,
|
||||
logger.LogDebug("Word count of {} is {}", content.FileName,
|
||||
count);
|
||||
return (uint)count;
|
||||
}
|
||||
|
|
|
@ -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"/>
|
||||
|
@ -16,7 +13,7 @@
|
|||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="EnsurePnpmInstalled" BeforeTargets="BeforeBuild">
|
||||
<Target Name="EnsurePnpmInstalled" BeforeTargets="Build">
|
||||
<Message Importance="low" Text="Ensure pnpm is installed..."/>
|
||||
<Exec Command="pnpm --version" ContinueOnError="true">
|
||||
<Output TaskParameter="ExitCode" PropertyName="ErrorCode"/>
|
||||
|
@ -28,13 +25,9 @@
|
|||
<Exec Command="pnpm install"/>
|
||||
</Target>
|
||||
|
||||
<Target Name="TailwindGenerate" AfterTargets="EnsurePnpmInstalled" BeforeTargets="BeforeBuild" Condition="'$(_IsPublishing)' == 'yes'">
|
||||
<Target Name="TailwindGenerate" AfterTargets="EnsurePnpmInstalled">
|
||||
<Message Importance="normal" Text="Generate css files using tailwind..."/>
|
||||
<Exec Command="pnpm tailwindcss -i wwwroot/tailwind.css -o $(IntermediateOutputPath)tailwind.g.css"/>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="$(IntermediateOutputPath)tailwind.g.css" Visible="false" TargetPath="wwwroot/tailwind.g.css"/>
|
||||
</ItemGroup>
|
||||
<Exec Command="pnpm tailwind -i wwwroot/input.css -o wwwroot/output.css"/>
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
884
YaeBlog/source/drafts/how-async-await-works.md
Normal file
884
YaeBlog/source/drafts/how-async-await-works.md
Normal file
|
@ -0,0 +1,884 @@
|
|||
---
|
||||
title: async/await究竟是如何工作的?
|
||||
tags:
|
||||
- dotnet
|
||||
- 技术笔记
|
||||
- 译文
|
||||
---
|
||||
|
||||
### 译者按
|
||||
|
||||
如何正确而快速的编写异步运行的代码一直是软件工程界的难题,而C#提出的`async/await`范式无疑是探索道路上的先行者。本篇文章便是翻译自.NET开发者博客上一篇名为“How async/await really works in C#”的文章,希望能够让读者在阅读之后明白`async/await`编程范式的前世今生和`.NET`实现方式。另外,.Net开发者中文博客也翻译了[这篇文章](https://devblogs.microsoft.com/dotnet-ch/async-await%e5%9c%a8-c%e8%af%ad%e8%a8%80%e4%b8%ad%e6%98%af%e5%a6%82%e4%bd%95%e5%b7%a5%e4%bd%9c%e7%9a%84/),一并供读者参考。
|
||||
|
||||
---
|
||||
|
||||
数周前,[.NET开发者博客](https://devblogs.microsoft.com/dotnet/)发布了一篇题为[什么是.NET,为什么你应该选择.NET](https://devblogs.microsoft.com/dotnet/why-dotnet/)的文章。文章中从宏观上概览了整个`dotnet`生态系统,总结了系统中的各个部分和其中的设计决定;文章还承诺在未来推出一系列的深度文章介绍涉及到的方方面面。这篇文章便是这系列文章中的第一篇,深入介绍C#和.NET中`async/await`的历史、设计决定和实现细节。
|
||||
|
||||
对于`async/await`的支持大约在十年前就提供了。在这段时间里,`async/await`语法大幅改变了编写可扩展.NET代码的方式,同时该语法使得在不了解`async/await`工作原理的情况下使用它提供的功能编写异步代码也是十分容易和常见的。以下面的**同步**方法为例:(因为这个方法的调用者在整个操作完成之前、将控制权返回给它之前都不能进行任何操作,所以这个方法被称为**同步**)
|
||||
|
||||
```csharp
|
||||
// 将数据同步地从源复制到目的地
|
||||
public void CopyStreamToStream(Stream source, Stream destination)
|
||||
{
|
||||
var buffer = new byte[0x1000];
|
||||
int numRead;
|
||||
while ((numRead = source.Read(buffer, 0, buffer.Length)) != 0)
|
||||
{
|
||||
destination.Write(buffer, 0, numRead);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在这个方法的基础上,你只需要修改几个关键词、改变几个方法的名称,就可以得到一个**异步**的方法(因为这个方法将很快,往往实在所有的工作完成之前,就会将控制权返回给它的调用者,所以被称作异步方法)。
|
||||
|
||||
```csharp
|
||||
// 将数据异步地从源复制到目的地
|
||||
public async Task CopyStreamToStreamAsync(Stream source, Stream destination)
|
||||
{
|
||||
var buffer = new byte[0x1000];
|
||||
int numRead;
|
||||
while ((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) != 0)
|
||||
{
|
||||
await destination.WriteAsync(buffer, 0, numRead);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
有着几乎相同的语法,类似的控制流结构,但是现在这个方法在执行过程中不会阻塞,有着完全不同的底层执行模型,而且C#编译器和核心库帮你完成所有这些复杂的工作。
|
||||
|
||||
尽管在不了解底层原理的基础上使用这类技术是十分普遍的,但是我们坚持认为了解这些事务的运行原理将会帮助我们更好的利用它们。之于`async/await`,了解这些原理将在你需要深入探究时十分有用,例如当你需要调试一段错误的代码或者优化某段正确运行代码的运行效率时。在这篇文章中,我们将深入了解`async/await`具体如何在语言、编译器和库层面运行,然后你将更好地利用这些优秀的设计。
|
||||
|
||||
为了更好的理解这一切,我们将回到没有`async/await`的时代,看看在没有它们的情况下最优秀的异步代码是如何编写的。平心而论,这些代码看上去并不好。
|
||||
|
||||
### 原初的历史
|
||||
|
||||
回到.NET框架1.0时代,当时流行的异步编程范式是**异步编程模型**,“Asynchronous Programming Model”,也被称作`APM`范式、`Being/End`范式或者`IAsyncResult`范式。从宏观上来看,这种范式是相当简单的。例如对于一个同步操作`DoStuff`:
|
||||
|
||||
```csharp
|
||||
class Handler
|
||||
{
|
||||
public int DoStuff(string arg);
|
||||
}
|
||||
```
|
||||
|
||||
在这种编程模型下会存在两个相关的方法:一个`BeginStuff`方法和一个`EndStuff`方法:
|
||||
|
||||
```csharp
|
||||
class Handler
|
||||
{
|
||||
public int DoStuff(string arg);
|
||||
|
||||
public IAsyncResult BeginDoStuff(string arg, AsyncCallback? callback, object? state);
|
||||
public int EndDoStuff(IAsyncResult asyncResult);
|
||||
}
|
||||
```
|
||||
|
||||
`BeginStuff`方法首先会接受所有`DoStuff`方法会接受的参数,同时其会接受一个`AsyncCallback`回调和一个**不透明**的状态对象`state`,而且这两个参数都可以为空。这个“开始”方法将负责异步操作的初始化,而且如果提供了回调函数,这个函数还会负责在异步操作完成之后调用这个回调函数,因此这个回调函数也常常被称为初始化操作的“下一步”。开始方法还会负责构建一个实现了`IAsyncResult`接口的对象,这个对象中的`AsyncState`属性由可选的`state`参数提供:
|
||||
|
||||
```csharp
|
||||
namespace System
|
||||
{
|
||||
public interface IAsyncResult
|
||||
{
|
||||
object? AsyncState { get; }
|
||||
WaitHandle AsyncWaitHandle { get; }
|
||||
bool IsCompleted { get; }
|
||||
bool CompletedSynchronously { get; }
|
||||
}
|
||||
|
||||
public delegate void AsyncCallback(IAsyncResult ar);
|
||||
}
|
||||
```
|
||||
|
||||
这个`IAsynResult`实例将会被开始方法返回,在调用`AsyncCallback`时这个实例也会被传递过去。当准备好使用该异步操作的结果时,调用者也会将这个`IAsyncResult`实例传递给结束方法,同时结束方法也会负责保证这个异步操作完成,如果没有完成该方法就会阻塞代码的运行直到完成。结束方法会返回异步操作的结果,异步操作过程中引发的各种错误和异常也会通过该方法传递出来。因此,对于下面这种同步的操作:
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
int i = handler.DoStuff(arg);
|
||||
Use(i);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
... // 在这里处理DoStuff方法和Use方法中引发的各种异常
|
||||
}
|
||||
```
|
||||
|
||||
可以使用开始/结束方法改写为异步运行的形式:
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
handler.BeginDoStuff(arg, iar =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Handler handler = (Handler)iar.AsyncState!;
|
||||
int i = handler.EndDoStuff(iar);
|
||||
Use(i);
|
||||
}
|
||||
catch (Exception e2)
|
||||
{
|
||||
... // 处理从EndDoStuff方法和Use方法中引发的各种异常
|
||||
}
|
||||
}, handler);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
... // 处理从同步调用BeginDoStuff方法引发的各种异常
|
||||
}
|
||||
```
|
||||
|
||||
对于熟悉使用含有回调`API`语言的开发者来说,这样的代码应该会显得相当眼熟。
|
||||
|
||||
但是事情在这里变得更加复杂了。例如,这段代码存在“栈堆积”`stack dive`的问题。栈堆积就是代码在重复的调用方法中使得栈越来越深,直到发生栈溢出的现象。如果“异步”操作同步完成,开始方法将会使同步的调用回调方法,这就意味着对于开始方法的调用就会直接调用回调方法。同时考虑到“异步”方法同步完成却是一种非常常见的现象,它们只是承诺会异步的完成操作而不是只被允许异步的完成。例如一个对于某个网络操作的异步操作,比如读取一个套接字,如果你只需要从一次操作中读取少量的数据,例如在一次回答中只需要读取少量响应头的数据,你可能会直接读取大量数据存储在缓冲区中。相比于每次使用都使用系统调用但是只读取少量的数据,你一次读取了大量数据在缓冲区中,并在缓冲区失效之前都是从缓冲区中读取,这样就减少了需要调用昂贵的系统调用来和套接字交互的次数。像这样的缓冲区可能在你进行任何异步调用之后存在,例如第一次操作异步的完成对于缓冲区的填充,之后的若干次“异步”操作都不需要同I/O进行任何交互而直接通过与缓冲区的同步交互完成,直到缓冲区失效之后再次异步的填充缓冲区。因此当开始方法进行上述的一次调用时,开始方法会发现操作同步地完成了,因此开始方法同步地调用回调方法。此时,你有一个调用了开始方法的栈帧和一个调用了回调方法的栈帧。想想看如果回调方法再次调用了开始方法会发生什么?如果开始方法和回调方法都是被同步调用的,现在你就会在站上得到多个重复的栈帧,如此重复下去直到将栈上的空间耗尽。
|
||||
|
||||
这并不是杞人忧天,使用下面这段代码就可以很容易的复现这个问题:
|
||||
|
||||
```csharp
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
using Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
listener.Bind(new IPEndPoint(IPAddress.Loopback, 0));
|
||||
listener.Listen();
|
||||
|
||||
using Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
client.Connect(listener.LocalEndPoint!);
|
||||
|
||||
using Socket server = listener.Accept();
|
||||
_ = server.SendAsync(new byte[100_000]);
|
||||
|
||||
var mres = new ManualResetEventSlim();
|
||||
byte[] buffer = new byte[1];
|
||||
|
||||
var stream = new NetworkStream(client);
|
||||
|
||||
void ReadAgain()
|
||||
{
|
||||
stream.BeginRead(buffer, 0, 1, iar =>
|
||||
{
|
||||
if (stream.EndRead(iar) != 0)
|
||||
{
|
||||
ReadAgain(); // uh oh!
|
||||
}
|
||||
else
|
||||
{
|
||||
mres.Set();
|
||||
}
|
||||
}, null);
|
||||
};
|
||||
ReadAgain();
|
||||
|
||||
mres.Wait();
|
||||
```
|
||||
|
||||
在代码中我们建立一个简单的客户端套接字和一个简单的服务端套接字并让它们连接。服务端会向客户端发送十万字节的信息,而客户端会使用开始/结束方法尝试去“异步的”接收这些信息(需要注意这样做是十分低效的,在教学实例之外的地方都不应该这样编写代码)。传递给`BeingRead`的回调函数通过调用`EndRead`方法停止读取,如果在读取过程中读取到数据(意味着还没有读取完成),就通过对于本地方法`ReadAgain`的递归调用来再次调用`BeingRead`方法继续读取。值得指出的是,在.NET Core中套接字操作比原来在.NET Framework中的版本快上许多,同时如果操作系统可以同步的完成这些操作,那么.NET Core中的操作也会同步完成(需要注意操作系统内核也有一个缓冲区来完成套接字接收操作)。因此,运行这段代码就会出现栈溢出。
|
||||
|
||||
鉴于这个问题非常容易出现,因此`APM`模型中内建了缓解这个问题的方法。容易想到有两种方法可以缓解这个问题:
|
||||
|
||||
1. 不允许`AsyncCallback`被同步调用。如果该回调方法始终都是被异步调用的,即使操作是异步完成的,栈堆叠的方法也就不存在了。但是这样做会降低性能,因为同步完成的操作(或者快到难以注意到的操作)是相当的常见的,强制这些操作的回调排队完成会增加相当可观的开销。
|
||||
2. 引入一个机制让调用者而不是回调函数在工作异步完成时完成剩余的工作。在这种情况下,我们就避免了引入额外的栈帧,在不增加栈深度的情况下完成了余下的工作。
|
||||
|
||||
`APM`模型使用了第二种方法。为了实现这个方法,`IAsyncResult`接口提供了另外两个成员:`IsCompleted`和`CompletedSynchronusly`。`IsCompeleted`成员告诉我们操作是否完成,在程序中可以反复检查这个成员直到它从`false`变成`true`。相对的,`CompletedSynchronously`在运行过程中不会变化,(或者它存在一个还未被发现的`bug`会导致这个值变化,笑),这个值的主要作用是判断后续的工作是应该由开始方法的调用者还是`AsyncCallback`来进行。如果`CompletedSynchronously`的值是`false`,说明这个操作是异步进行的,所有后续的工作应该由回调函数来进行处理;毕竟,如果工作是异步完成的,开始方法的调用者不能知道工作是何时完成的(如果开始方法的调用者调用了结束方法,那么结束方法就会阻塞直到工作完成)。反之,如果`CompletedSynchronously`的值是`true`,如果此时使用回调方法处理后续的工作就会引发栈堆叠问题,因为此时回调方法会在栈上比开始它更低的位置上进行后续的操作。因此任何在意栈堆叠问题的实现需要关注`CompletedSynchronously`的值,当为真的时候,让开始方法的调用者处理后续的工作,而回调方法在此时不应处理任何工作。这也是为什么`CompletedSynchronously`的值不能改变——开始方法的调用者和回调方法需要相同的值来保证后续工作在任何情况下都进行且只进行一次。
|
||||
|
||||
因此我们之前的`DoStuff`实例就需要被修改为:
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
IAsyncResult ar = handler.BeginDoStuff(arg, iar =>
|
||||
{
|
||||
if (!iar.CompletedSynchronously)
|
||||
{
|
||||
try
|
||||
{
|
||||
Handler handler = (Handler)iar.AsyncState!;
|
||||
int i = handler.EndDoStuff(iar);
|
||||
Use(i);
|
||||
}
|
||||
catch (Exception e2)
|
||||
{
|
||||
... // handle exceptions from EndDoStuff and Use
|
||||
}
|
||||
}
|
||||
}, handler);
|
||||
if (ar.CompletedSynchronously)
|
||||
{
|
||||
int i = handler.EndDoStuff(ar);
|
||||
Use(i);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
... // handle exceptions that emerge synchronously from BeginDoStuff and possibly EndDoStuff/Use
|
||||
}
|
||||
```
|
||||
|
||||
这里的代码已经~~显得冗长~~,而且我们还只研究了如何使用这种范式,还没有涉及如何实现这种范式。尽管大部分的开发者并不需要在这些子调用(例如实现`Socket.BeginReceive/EndReceive`这些方法去和操作系统交互),但是很多开发者需要组合这些操作(从一个“较大的”的异步操作调用多个异步操作),而这不仅需要使用其他的开始/结束方法,还需要自行实现你自己的开始/结束方法,这样你才能在其他的地方使用这个操作。同时,你还会注意到在上述的`DoStuff`范例中没有任何的控制流代码。如果需要引入一些控制流代码——即使是一个简单的循环——这也会立刻变成~~抖M才会编写的代码~~,同时也给无数的博客作者提供水`CSDN`的好题材。
|
||||
|
||||
所以让我们现在就来写一篇`CSDN`,给出一个完成的实例。在文章的开头我展示了一个`CopyStreamToStream`方法,这个方式会将一个流中的数据复制到另外一个流中(就是`Stream.CopyTo`方法所完成的工作,但是为了说明,让我们假设这个方法并不存在):
|
||||
|
||||
```csharp
|
||||
public void CopyStreamToStream(Stream source, Stream destination)
|
||||
{
|
||||
var buffer = new byte[0x1000];
|
||||
int numRead;
|
||||
while ((numRead = source.Read(buffer, 0, buffer.Length)) != 0)
|
||||
{
|
||||
destination.Write(buffer, 0, numRead);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
直白的说,我们只需要不停的从一个流中读取数据然后写入到另外一个流中,直到我们没法从第一个流中读取到任何数据。现在让我们使用`APM`模型使用这个操作的异步模式吧:
|
||||
|
||||
```csharp
|
||||
public IAsyncResult BeginCopyStreamToStream(
|
||||
Stream source, Stream destination,
|
||||
AsyncCallback callback, object state)
|
||||
{
|
||||
var ar = new MyAsyncResult(state);
|
||||
var buffer = new byte[0x1000];
|
||||
|
||||
Action<IAsyncResult?> readWriteLoop = null!;
|
||||
readWriteLoop = iar =>
|
||||
{
|
||||
try
|
||||
{
|
||||
for (bool isRead = iar == null; ; isRead = !isRead)
|
||||
{
|
||||
if (isRead)
|
||||
{
|
||||
iar = source.BeginRead(buffer, 0, buffer.Length, static readResult =>
|
||||
{
|
||||
if (!readResult.CompletedSynchronously)
|
||||
{
|
||||
((Action<IAsyncResult?>)readResult.AsyncState!)(readResult);
|
||||
}
|
||||
}, readWriteLoop);
|
||||
|
||||
if (!iar.CompletedSynchronously)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int numRead = source.EndRead(iar!);
|
||||
if (numRead == 0)
|
||||
{
|
||||
ar.Complete(null);
|
||||
callback?.Invoke(ar);
|
||||
return;
|
||||
}
|
||||
|
||||
iar = destination.BeginWrite(buffer, 0, numRead, writeResult =>
|
||||
{
|
||||
if (!writeResult.CompletedSynchronously)
|
||||
{
|
||||
try
|
||||
{
|
||||
destination.EndWrite(writeResult);
|
||||
readWriteLoop(null);
|
||||
}
|
||||
catch (Exception e2)
|
||||
{
|
||||
ar.Complete(e);
|
||||
callback?.Invoke(ar);
|
||||
}
|
||||
}
|
||||
}, null);
|
||||
|
||||
if (!iar.CompletedSynchronously)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
destination.EndWrite(iar);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ar.Complete(e);
|
||||
callback?.Invoke(ar);
|
||||
}
|
||||
};
|
||||
|
||||
readWriteLoop(null);
|
||||
|
||||
return ar;
|
||||
}
|
||||
|
||||
public void EndCopyStreamToStream(IAsyncResult asyncResult)
|
||||
{
|
||||
if (asyncResult is not MyAsyncResult ar)
|
||||
{
|
||||
throw new ArgumentException(null, nameof(asyncResult));
|
||||
}
|
||||
|
||||
ar.Wait();
|
||||
}
|
||||
|
||||
private sealed class MyAsyncResult : IAsyncResult
|
||||
{
|
||||
private bool _completed;
|
||||
private int _completedSynchronously;
|
||||
private ManualResetEvent? _event;
|
||||
private Exception? _error;
|
||||
|
||||
public MyAsyncResult(object? state) => AsyncState = state;
|
||||
|
||||
public object? AsyncState { get; }
|
||||
|
||||
public void Complete(Exception? error)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
_completed = true;
|
||||
_error = error;
|
||||
_event?.Set();
|
||||
}
|
||||
}
|
||||
|
||||
public void Wait()
|
||||
{
|
||||
WaitHandle? h = null;
|
||||
lock (this)
|
||||
{
|
||||
if (_completed)
|
||||
{
|
||||
if (_error is not null)
|
||||
{
|
||||
throw _error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
h = _event ??= new ManualResetEvent(false);
|
||||
}
|
||||
|
||||
h.WaitOne();
|
||||
if (_error is not null)
|
||||
{
|
||||
throw _error;
|
||||
}
|
||||
}
|
||||
|
||||
public WaitHandle AsyncWaitHandle
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
return _event ??= new ManualResetEvent(_completed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool CompletedSynchronously
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (_completedSynchronously == 0)
|
||||
{
|
||||
_completedSynchronously = _completed ? 1 : -1;
|
||||
}
|
||||
|
||||
return _completedSynchronously == 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsCompleted
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
return _completed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
~~Yowsers~~。即使写完了这些繁文缛节,这实际上仍然不是一个完美的实现。例如,`IAsyncResult`的实现会在每次操作时上锁,而不是在任何可能的时候都使用无锁的实现;异常也是以原始的模型存储,如果使用`ExceptionDispatchInfo`可以让异常在传播的过程中含有调用栈的信息,在每次操作中都分配了大量的空间来存储变量(例如在每次`BeingWrite`调用时都会分配一片空间来存储委托),如此等等。现在想象这就是你每次编写方法时需要做的工作,每次当你需要编写一个可重用的异步方法来使用另外一个异步方法时,你需要自己完成上述所有的工作。而且如果你需要编写使用多个不同的`IAsyncResult`的可重用代码——就像在`async/await`范式中`Task.WhenAll`所完成的那样,难度又上升了一个等级;每个不同操作都会实现并暴露针对相关的`API`,这让编写一套逻辑代码并简单的复用它们也变得不可能(尽管一些库作者可能会通过提供一层针对回调方法的新抽象来方便开发者编写需要访问暴露`API`的回调方法)。
|
||||
|
||||
上述这些复杂性也说明只有很少的一部分人尝试过这样编写代码,而且对于这些人来说,`bug`也往往如影随形。而且这并不是一个`APM`范式的黑点,这是所有使用基于回调的异步方法都具有的缺点。我们已经十分习惯现代语言都有的控制流结构所带来的强大和便利,因此使用会破坏这种结构的基于回调的异步方式会带来大量的复杂性也是可以理解的。同时,也没有任何主流的语言提供了更好的替代。
|
||||
|
||||
我们需要一种更好的办法,一个既继承了我们在`APM`范式中所学习到所有经验也规避了其所有的各种缺点的方式。一个有趣的点是,`APM`范式只是一种编程范式,运行时、核心库和编译器在使用或者实现这种范式的过程中没有提供任何协助。
|
||||
|
||||
### 基于事件的异步范式
|
||||
|
||||
在.NET Framework 2.0中提供了一系列的`API`来实现一种不同的异步编程范式,当时设想这种范式的主要应用场景是客户端应用程序。这种基于事件的异步范式,也被称作`EAP`范式,也是以提供一系列成员的方式提供的,包含一个用于初始化异步操作的方式和一个监听异步操作是否完成的事件。因此上述的`DoStuff`示例可能会暴露如下的一系列成员:
|
||||
|
||||
```csharp
|
||||
class Handler
|
||||
{
|
||||
public int DoStuff(string arg);
|
||||
|
||||
public void DoStuffAsync(string arg, object? userToken);
|
||||
public event DoStuffEventHandler? DoStuffCompleted;
|
||||
}
|
||||
|
||||
public delegate void DoStuffEventHandler(object sender, DoStuffEventArgs e);
|
||||
|
||||
public class DoStuffEventArgs : AsyncCompletedEventArgs
|
||||
{
|
||||
public DoStuffEventArgs(int result, Exception? error, bool canceled, object? userToken) :
|
||||
base(error, canceled, usertoken) => Result = result;
|
||||
|
||||
public int Result { get; }
|
||||
}
|
||||
```
|
||||
|
||||
首先通过`DoStuffCompleted`事件注册需要在完成异步操作时进行的工作然后调用`DoStuff`方法,这个方法将初始化异步操作,一旦异步操作完成,`DoStuffCompleted`事件将会被调用者引发。已经注册的回调方法可以运行剩余的工作,例如验证提供的`userToken`是否是期望的`userToken`,同时我们可以注册多个回调方法在异步操作完成的时候运行。
|
||||
|
||||
这个范式确实让一系列用例的编写更好编写,同时也让一系列用例变得更加复杂(例如上述的`CopyStreamToStream`例子)。这种范式的影响范围并不大,只在一次.NET Framework的更新中引入便匆匆地消失了,除了留下了一系列为了支持这种范式而实现的`API`,例如:
|
||||
|
||||
```csharp
|
||||
class Handler
|
||||
{
|
||||
public int DoStuff(string arg);
|
||||
|
||||
public void DoStuffAsync(string arg, object? userToken);
|
||||
public event DoStuffEventHandler? DoStuffCompleted;
|
||||
}
|
||||
|
||||
public delegate void DoStuffEventHandler(object sender, DoStuffEventArgs e);
|
||||
|
||||
public class DoStuffEventArgs : AsyncCompletedEventArgs
|
||||
{
|
||||
public DoStuffEventArgs(int result, Exception? error, bool canceled, object? userToken) :
|
||||
base(error, canceled, usertoken) => Result = result;
|
||||
|
||||
public int Result { get; }
|
||||
}
|
||||
```
|
||||
|
||||
但是这种编程范式确实在`APM`范式所没有注意到的地方前进了一大步,并且这一点还保留到了我们今天所介绍的模型中:[同步上下文](https://github.com/dotnet/runtime/blob/967a59712996c2cdb8ce2f65fb3167afbd8b01f3/src/libraries/System.Private.CoreLib/src/System/Threading/SynchronizationContext.cs#L6) (`SynchronizationContext`)。
|
||||
|
||||
同步上下文作为一个对于通用调度器的实现,也是在.NET Framework中引入的。在实践中,同步上下文最常用的方法是`Post`,这个方法将一个工作实现传递给上下文所代表的一种调度器。举例来说,一个基础的同步上下文实现是一个线程池`ThreadPool`,因此`Post`方法的典型实现就是`ThreadPool.QueueUserWorkItem`方法,这个方法将让线程池在池中任意的线程上以指定的状态调用指定的委托。然而,同步上下文的巧妙之处不仅在于提供了对于不同调度器的支持,而是提供了一种针对不同的应用模型使用不同调度方法的抽象能力。
|
||||
|
||||
考虑像Windows Forms之类的`UI`框架。对于大多数工作在Windows上的`UI`框架来说,控件往往关联到一个特定的线程,这个线程负责运行一个消息管理中心,这个中心用来运行那些需要同控件交互的工作:只有这个控件有能力来修改控件,任何其他试图同控件进行交互的线程都需要发送消息到这个消息控制中心。Windows Forms通过一系列方法来实现这一点,例如`Control.BeingInvoke`,这类方法将会把提供的委托和参数传递给同这个控件相关联的线程来运行。你可以写出如下的代码:
|
||||
|
||||
```csharp
|
||||
private void button1_Click(object sender, EventArgs e)
|
||||
{
|
||||
ThreadPool.QueueUserWorkItem(_ =>
|
||||
{
|
||||
string message = ComputeMessage();
|
||||
button1.BeginInvoke(() =>
|
||||
{
|
||||
button1.Text = message;
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
这段代码首先将`ComputeMessage`方法交给线程池中的一个线程运行(这样可以保证该方法在运行时`UI`界面不会卡死),当上述工作完成之后,再将一个更新`button1`标签的委托传递给关联到`button1`的线程运行。简单而易于理解。在`WPF`框架中也是类似的逻辑,使用一个被称为`Dispatcher`的类型:
|
||||
|
||||
```csharp
|
||||
private void button1_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ThreadPool.QueueUserWorkItem(_ =>
|
||||
{
|
||||
string message = ComputeMessage();
|
||||
button1.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
button1.Content = message;
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
`.NET MAUI`亦然。但是如果我想将这部分的逻辑封装到一个独立的辅助函数中,例如下面这种:
|
||||
|
||||
```csharp
|
||||
// 调用ComputeMessage然后触发更新逻辑
|
||||
internal static void ComputeMessageAndInvokeUpdate(Action<string> update) { ... }
|
||||
```
|
||||
|
||||
这样我就可以直接:
|
||||
|
||||
```csharp
|
||||
private void button1_Click(object sender, EventArgs e)
|
||||
{
|
||||
ComputeMessageAndInvokeUpdate(message => button1.Text = message);
|
||||
}
|
||||
```
|
||||
|
||||
但是`ComputerMessageAndInvokeUpdate`应该如何实现才能适配各种类型的应用程序呢?难道需要硬编码所有可能涉及的`UI`框架吗?这就是`SynchronizationContext`大显神威的地方,我们可以这样实现这个方法:
|
||||
|
||||
```csharp
|
||||
internal static void ComputeMessageAndInvokeUpdate(Action<string> update)
|
||||
{
|
||||
SynchronizationContext? sc = SynchronizationContext.Current;
|
||||
ThreadPool.QueueUserWorkItem(_ =>
|
||||
{
|
||||
string message = ComputeMessage();
|
||||
if (sc is not null)
|
||||
{
|
||||
sc.Post(_ => update(message), null);
|
||||
}
|
||||
else
|
||||
{
|
||||
update(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
在这个实现中将`SynchronizationContext`作为同`UI`进行交互的调度器之抽象。任何应用程序模型都需要保证在`SynchronizationContext.Current`属性上注册一个继承了`SynchronizationContext`的类,这个就会完成调度相关的工作。例如在`Windows Forms`中:
|
||||
|
||||
```csharp
|
||||
public sealed class WindowsFormsSynchronizationContext : SynchronizationContext, IDisposable
|
||||
{
|
||||
public override void Post(SendOrPostCallback d, object? state) =>
|
||||
_controlToSendTo?.BeginInvoke(d, new object?[] { state });
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
在`WPF`中有:
|
||||
|
||||
```
|
||||
public sealed class DispatcherSynchronizationContext : SynchronizationContext
|
||||
{
|
||||
public override void Post(SendOrPostCallback d, Object state) =>
|
||||
_dispatcher.BeginInvoke(_priority, d, state);
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
`ASP.NET`*曾经*也有过一个实现,尽管Web框架实际上并不关心是哪个线程在运行指定的工作,但是非常关心指定工作和那个请求相关,因此该实现主要负责保证多个线程不会在同时访问同一个`HttpContext`。
|
||||
|
||||
```csharp
|
||||
internal sealed class AspNetSynchronizationContext : AspNetSynchronizationContextBase
|
||||
{
|
||||
public override void Post(SendOrPostCallback callback, Object state) =>
|
||||
_state.Helper.QueueAsynchronous(() => callback(state));
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
这个概念也并不局限于像上面的主流应用程序模型。例如在[xunit](https://github.com/xunit/xunit),一个流行的单元测试框架(`.NET`核心代码仓库也使用了)中也实现了需要自定义的`SynchronizationContext`。例如限制同步运行单元测试时同时运行单元测试数量就可以用`SynchroniaztionContext`实现:
|
||||
|
||||
```
|
||||
public class MaxConcurrencySyncContext : SynchronizationContext, IDisposable
|
||||
{
|
||||
public override void Post(SendOrPostCallback d, object? state)
|
||||
{
|
||||
var context = ExecutionContext.Capture();
|
||||
workQueue.Enqueue((d, state, context));
|
||||
workReady.Set();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`MaxConcurrentSyncContext`中的`Post`方法只是将需要完成的工作压入其内部的工作队列中,这样就能够控制同时多少工作能够并行的运行。
|
||||
|
||||
那么同步上下文这个概念时如何同基于事件的异步范式关联起来的呢?`EAP`范式和同步上下文都是在同一时间引入的,而`EAP`范式要求当异步操作启动的时候,完成事件需要由当前`SynchronizationContext`进行调度。为了简化这个过程(可能反而引入多余的复杂性),在`System.ComponentModel`命名控件中引入了一些帮助程序,具体来说是`AsyncOperation`和`AsyncOperationManager`。其中前者是一个由用户提供的状态对象和捕获到的`SynchronizationContext`组成的元组,后者是一个捕获`SynchronizationContext`和创建`AsyncOperation`对象的工厂类。`EAP`范式会在实现中使用上述帮助类,例如`Ping.SendAsync`会首先调用`AsyncOperationManager.CreateOperationi`来捕获同步上下文,然后当异步操作完成的时候调用`AsyncOperation.PostOperationCompleted`方法来调用捕获到的`SynchronizationContext.Post`方法。
|
||||
|
||||
`SynchronizationContext`还提供了其他一些后面会用到的小工具。这个类暴露了`OperationStarted`和`OperationCompleted`两个方法。这个虚方法在基类中的实现都是空的,并不完成任何工作。但是继承其的实现可能会重载这些来了解运行中的操作。`EAP`的实现就会在每个操作开始和结束的时候调用`OperationStarted`和`OperationCompleted`,来方便可能存在的同步上下文跟踪工作的进度。鉴于在`EAP`范式中启动异步操作的方法往往不会返回任何东西,不能指望可以获得任何帮助你跟踪工作进度的东西,因而可能获得工作进度的同步上下文就显得很有价值了。
|
||||
|
||||
综上所说,我们需要一些比`APM`编程范式更好的东西,而`EAP`范式引入了一些新的东西,但是没有解决我们面对的核心问题,我们仍然需要一些更好的东西。
|
||||
|
||||
### 进入Task时代
|
||||
|
||||
在.NET Framework 4.0中引入了`System.Threading.Tasks.Task`类型。当时`Task`类型还只代表某些异步操作的最终完成(在其他编程框架中可能成称为`promise`或者`future`)。当一个操作开始时,创建一个`Task`来表示这个操作,当这个操作完成之后,操作的结果就会被保存在这个`Task`中。简单而明确。但是`Task`相较于`IAsyncResult`提供的重要特点是其蕴含了一个任务在持续运行的状态。这个特点让你能够随意找到一个`Task`,让它在异步操作完成的时候异步的通知你,而不用你关注任务当前是处在已经完成、没有完成、正在完成等各种状态。为什么这点非常重要?首先想想`APM`范式中存在的两个主要问题:
|
||||
|
||||
1. 你需要对每个操作实现一个自定义的`IAsycResult`实现:库中没有任何内置开箱即用的`IAsycResult`实现。
|
||||
2. 你需要在调用开始方法之前就知道在操作结束的时候需要做什么。这让编写使用任意异步操作的组合代码或者通用运行时非常困难。
|
||||
|
||||
相对的,`Task`提供了一个通用的接口让你在启动一个异步操作之后“接触”这个操作,还提供了针对“持续”的抽象,这样你就不需要为启动异步操作的方法提供一个持续性。任何需要进行异步操作的人都可以产生一个`Task`,任何人需要使用异步操作的人都可以使用一个`Task`,在这个过程中不用自定义任何东西,`Task`成为了沟通异步操作的生产者和消费者之间最重要的桥梁。这一点大大改变了.NET框架。
|
||||
|
||||
现在让我们深入理解`Task`所带来的重要意义。与其直接去研究错综复杂的`Task`源代码,我们将尝试去实现一个`Task`的简单版本。这不会是一个完善的实现,只会完成基础的功能来让我们更好的理解什么是`Task`,即一个负责协调设置和存储完成信号的数据结构。
|
||||
|
||||
开始时`Task`中只有很少的字段:
|
||||
|
||||
```csharp
|
||||
class MyTask
|
||||
{
|
||||
private bool _completed;
|
||||
private Exception? _error;
|
||||
private Action<MyTask>? _continuation;
|
||||
private ExecutionContext? _ec;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
我们首先需要一个字段告诉我们任务是否完成`_completed`,一个字段存储造成任务执行失败的错误`_error`;如果我们需要实现一个泛型的`MyTask<TResult>`,还需要一个`private TResult _result`字段来存储操作运行完成之后的结果。到目前为止的实现和`IAsyncResult`相关的实现非常类似(当然这不是一个巧合)。`_continuation`字段时实现中最重要的字段。在这个简单的实现中,我们只支持一个简单的后续过程,在真正的`Task`实现中是一个`object`类型的字段,这样既可以是一个独立的后续过程,也可以是一个后续过程的列表。这个委托会在任务完成的时候调用。
|
||||
|
||||
让我们继续深入。如上所述,`Task`相较于之前的异步执行模型一个基础的优势是在异步操作开始之后再提供后续需要完成的工作。因此我们需要一个方法来实现这个功能:
|
||||
|
||||
```csharp
|
||||
public void ContinueWith(Action<MyTask> action)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (_completed)
|
||||
{
|
||||
ThreadPool.QueueUserWorkItem(_ => action(this));
|
||||
}
|
||||
else if (_continuation is not null)
|
||||
{
|
||||
throw new InvalidOperationException("Unlike Task, this implementation only supports a single continuation.");
|
||||
}
|
||||
else
|
||||
{
|
||||
_continuation = action;
|
||||
_ec = ExecutionContext.Capture();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如果在调用`ContinueWith`的时候异步操作已经完成,那么就直接将该委托的执行加入执行队列。反之,这个方法就会将存储这个委托,当异步任务完成的时候进行执行(这个方法同时也存储一个被称为`ExecutionContext`的对象,会在后续调用委托的涉及到,我们后续会继续介绍)。
|
||||
|
||||
然后我们需要能够在异步过程完成的时候标记任务已经完成。我们将添加两个方法,一个负责标记任务成功完成,一个负责标记任务报错退出。
|
||||
|
||||
```csharp
|
||||
public void SetResult() => Complete(null);
|
||||
|
||||
public void SetException(Exception error) => Complete(error);
|
||||
|
||||
private void Complete(Exception? error)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (_completed)
|
||||
{
|
||||
throw new InvalidOperationException("Already completed");
|
||||
}
|
||||
|
||||
_error = error;
|
||||
_completed = true;
|
||||
|
||||
if (_continuation is not null)
|
||||
{
|
||||
ThreadPool.QueueUserWorkItem(_ =>
|
||||
{
|
||||
if (_ec is not null)
|
||||
{
|
||||
ExecutionContext.Run(_ec, _ => _continuation(this), null);
|
||||
}
|
||||
else
|
||||
{
|
||||
_continuation(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
我们会存储任何的错误、标记任务已经完成,如果已经注册的任何的后续过程,我们也会引发其进行执行。
|
||||
|
||||
最后我们还需要一个方法将在工作中发生的任何传递出来,(如果是泛型类型,还需要将执行结果返回),为了方便某些特定的场景,我们将允许这个方法阻塞直到异步操作完成(通过调用`ContinueWith`注册一个`ManualResetEventSlim`实现)。
|
||||
|
||||
```csharp
|
||||
public void Wait()
|
||||
{
|
||||
ManualResetEventSlim? mres = null;
|
||||
lock (this)
|
||||
{
|
||||
if (!_completed)
|
||||
{
|
||||
mres = new ManualResetEventSlim();
|
||||
ContinueWith(_ => mres.Set());
|
||||
}
|
||||
}
|
||||
|
||||
mres?.Wait();
|
||||
if (_error is not null)
|
||||
{
|
||||
ExceptionDispatchInfo.Throw(_error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这就是一个基础的`Task`实现。当然需要指出的是实际的`Task`会复杂很多:
|
||||
|
||||
- 支持设置任意数量的后续工作;
|
||||
- 支持配置其的工作行为(例如配置后续工作是应该进入工作队列等待执行还是作为任务完成的一部分同步被调用);
|
||||
- 支持存储多个错误;
|
||||
- 支持取消异步操作;
|
||||
- 一系列的帮助函数(例如`Task.Run`创建一个代表在线程池上运行委托的`Task`)。
|
||||
|
||||
但是这些内容中没有什么奥秘,核心工作原理和我们自行实现的是一样的。
|
||||
|
||||
你可以会注意到我们自行实现的`MyTask`直接公开了`SetResult/SetException`方法,而`Task`没有;这是因为`Task`是以`internal`声明了上述两个方法,同时`System.Threading.Tasks.TaskCompletionSource`类型负责作为一个独立的`Task`生产者和管理任务的完成。这样做的目的并不是出于技术目的,只是将负责控制完成的方法从消费`Task`的方法中分离出来。这样你就可以通过保留`TaskCompletionSource`对象来控制`Task`的完成,不必担心你创建的`Task`在你不知道的地方被完成。(`CancellationToken`和`CanellationTokenSource`也是处于同样的设计考虑,`CancellationToken`是一个包装`CancellationTokenSource`的结构,只暴露了和接受消费信号相关的结构而缺少产生一个取消信号的能力,这样就限制只有`CancellationToeknSource`可以产生取消信号。)
|
||||
|
||||
当前我们也可以像`Task`一样为我们自己的`MyTask`添加各种工具函数。例如我们添加一个`MyTask.WhenAll`:
|
||||
|
||||
```csharp
|
||||
public static MyTask WhenAll(MyTask t1, MyTask t2)
|
||||
{
|
||||
var t = new MyTask();
|
||||
|
||||
int remaining = 2;
|
||||
Exception? e = null;
|
||||
|
||||
Action<MyTask> continuation = completed =>
|
||||
{
|
||||
e ??= completed._error; // just store a single exception for simplicity
|
||||
if (Interlocked.Decrement(ref remaining) == 0)
|
||||
{
|
||||
if (e is not null) t.SetException(e);
|
||||
else t.SetResult();
|
||||
}
|
||||
};
|
||||
|
||||
t1.ContinueWith(continuation);
|
||||
t2.ContinueWith(continuation);
|
||||
|
||||
return t;
|
||||
}
|
||||
```
|
||||
|
||||
然后是一个`MyTask.Run`的示例:
|
||||
|
||||
```csharp
|
||||
public static MyTask Run(Action action)
|
||||
{
|
||||
var t = new MyTask();
|
||||
|
||||
ThreadPool.QueueUserWorkItem(_ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
t.SetResult();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
t.SetException(e);
|
||||
}
|
||||
});
|
||||
|
||||
return t;
|
||||
}
|
||||
```
|
||||
|
||||
还有一个简单的`MyTask.Delay`:
|
||||
|
||||
```csharp
|
||||
public static MyTask Delay(TimeSpan delay)
|
||||
{
|
||||
var t = new MyTask();
|
||||
|
||||
var timer = new Timer(_ => t.SetResult());
|
||||
timer.Change(delay, Timeout.InfiniteTimeSpan);
|
||||
|
||||
return t;
|
||||
}
|
||||
```
|
||||
|
||||
在`Task`横空出世之后,之前的所有异步编程范式都成为了过去式。任何使用过去的编程范式暴露的异步`API`,现在都提供了返回`Task`的方法。
|
||||
|
||||
### 添加Value Task
|
||||
|
||||
直到现在,`Task`都是.NET异步编程中的主力军,在每次新版本发布或者社区发布的新`API`都会返回`Task`或者`Task<TResult>`。但是,`Task`是一个类,而每次创建一个类是都需要分配一次内存。在大多数情况下,为一个会长期存在的异步操作进行一次内存分配时无关紧要的,并不会操作明显的性能影响。但是正如之前所说的,同步完成的异步操作十分创建。例如,`Stream.ReadAsync`会返回一个`Task<int>`,但是如果是在一个类似与`BufferedStream`的实现上调用该方法,那么你的调用由很大概率就会是同步完成的,因为大多数读取只需要从内存中的缓冲区中读取数据而不需要通过系统调用访问`I/O`。在这种情况下还需要分配一个额外的对象显然是不划算的(而且在`APM`范式中也存在这个问题)。对于返回非泛型类型的方法来说,还可以通过返回一个预先分配的已完成单例来缓解这个问题,而且`Task`也提供了一个`Task.CompletedTask`。但是对于泛型的`Task<TResult>`则不行,因为不可能针对每个不同的`TResult`都创建一个对应的单例。那么我们可以如何让这个同步操作更快呢?
|
||||
|
||||
我们可以试图缓存一个常见的`Task<TResult>`。例如`Task<bool>`就非常的常见,而且也只存在两种需要缓存的情况:当结果为真时的一个对象和结果为假时的一个对象。同样的,尽管我们可能不想尝试(也不太可能)去缓存数亿个`Task<int>`对象以覆盖所有可能出现的值,但是鉴于很小的`Int32`值时非常常见的,我们可以尝试去缓存给一些较小的结果,例如从-1到8的结果。 而且对于其他任意的类型来说,`default`就是一个常常出现的值,因此缓存一个结果是`default(TResult)`的`Task`。而且 在最近的.NET版本中添加了一个称作`Task.FromResult`辅助函数,该函数就会完成与上述类似的工作,如果存在可以重复使用的`Task<Result>`单例就返回该单例,反之再创建一个新的`Task`对象。对于其他常常出现的值也也可以设计方法进行缓存。还是以`Stream.ReadAsync`为例子,这个方法常常会在同一个流上调用多次,而且每次读取的值都是允许读取的字节数量`count`。再考虑到使用者往往只需要读取到这个`count`值,因此`Stream.ReadAsync`操作常常会重复返回有着相同`int`值的`Task`对象。为了避免在这种情况下重复的内存分配,许多`Stream`的实现(例如`MemoryStream`)会缓存上一次成功缓存的`Task<int>`对象,如果下一次读取仍然是同步返回的且返回了相同的数值,该方法就会返回上一次读取创建的`Task<int>`对象。但是仍然会存在许多无法覆盖的其他情况,能不能找到一种更加优雅的解决方案来来避免在异步操作同步完成的时候避免创建新的对象,尤其是在性能非常重要的场景下。
|
||||
|
||||
这就是`ValueTask<TResult>`诞生的背景([这篇博客](https://devblogs.microsoft.com/dotnet/understanding-the-whys-whats-and-whens-of-valuetask/)详细测试了`ValueTask<TResult>`的性能)。`ValueTask<TResult>`在诞生之初是`TResult`和`Task<TResult>`的歧视性联合。在这些争论尘埃落定之后,`ValueTask<TResult>`便不是一个立刻可以返回的结果就是一个对未来结果的承诺:
|
||||
|
||||
```csharp
|
||||
public readonly struct ValueTask<TResult>
|
||||
{
|
||||
private readonly Task<TResult>? _task;
|
||||
private readonly TResult _result;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
一个方法可以通过返回`ValueTask<TResult>`来避免在`TResult`已知的情况下创建新的`Task<Result>`对象,当然返回的类型会更大、返回的结果更加不直接。
|
||||
|
||||
当然,实际应用中也存在对性能需求相当高的场合,甚至你会想在操作异步完成的时候也避免`Task<TResult>`对象的分配。例如`Socket`作为整个网络栈的最底层,对于网络中的大多数服务来说`SendAsync`和`ReceiveAsync`都是绝对的热点代码路径,不论是同步操作还是异步操作都是非常常见的(鉴于内核中的缓存,大多数发送请求都会同步完成,部分接受请求会同步完成)。因此对于像`Socket`这类的工具,如果我们可以在异步我弄成和同步完成的情况下都实现无内存分配的调用是十分有意义的。
|
||||
|
||||
这就是`System.Threading.Tasks.Sources.IValueTaskSource<TResult>`产生的背景:
|
||||
|
||||
```csharp
|
||||
public interface IValueTaskSource<out TResult>
|
||||
{
|
||||
ValueTaskSourceStatus GetStatus(short token);
|
||||
void OnCompleted(Action<object?> continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags);
|
||||
TResult GetResult(short token);
|
||||
}
|
||||
```
|
||||
|
||||
该接口允许自行为`ValueTask<TResult>`实现一个“背后“的对象,并且让这个对象提供了获得操作结构的`GetResult`方法和设置操作后续工作的`OnCompleted`。在这个接口出现之后,`ValueTask<TResult>`也小小修改了定义,`Task<TResult>? _task`字段被一个`object? _obj`字段替换了:
|
||||
|
||||
```csharp
|
||||
public readonly struct ValueTask<TResult>
|
||||
{
|
||||
private readonly object? _obj;
|
||||
private readonly TResult _result;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
现在`_obj`字段就可以存储一个`IValueTaskSource<TReuslt>`对象了。而且相较于`Task<TResult>`在完成之后就只能保持完成的状态,不能变回未完成的状态,`IValueTaskSource<TResult>`的实现有着完全的控制权,可以在已完成和未完成的状态之间双向变化。但是`ValueTask<TResult>`要求一个特定的实例只能被使用一次,不能观察到这个实例在使用之后的任何变化,这也是分析规则[CA2012](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2012)存在的意义。这就让让类似于`Socket`的工具为重复的调用建立一个`IValueTaskSource<TResult>`对象池。从实现上来说,`Socket`会至多缓存两个类似的实例,一个用于读取操作一个用于写入操作,因为在99.999%的情况下同时只会有一个发送请求和一个接受请求。
|
||||
|
||||
值得说明的是我只提到了`ValueTask<TResult>`却没有提到`ValueTask`。因为如果只是为了在操作同步完成的时候避免内存分配,非泛型类型的`ValueTask`指挥提供很少的性能提升,因为在同样的条件下可以使用`Task.CompletedTask`。但是如果要考虑在异步完成的时候通过缓存对象避免内存分配,非泛型类型也有作用。因而,在引入`IValueTaskSource<TResult>`的同时,`IValueTaskSource`和`ValueTask`也被引入了。
|
||||
|
||||
到目前我们,我们已经可以利用`Task`,`Task<TResult>`,`ValueTask`,`ValueTask<TResult>`表示各种各样的异步操作,并注册在操作完成之前和之后注册后续的操作。
|
||||
|
||||
但是这些后续操作仍然是回调方法,我们仍然陷入了基于回调的异步控制流程。该怎么办?
|
||||
|
||||
### 迭代器成为大救星
|
||||
|
||||
解决方案的先声实际上在`Task`诞生之前就出现了,在C# 2.0引入迭代器语法的时候。
|
||||
|
||||
你可能会问,迭代器就是`IEnumerable<T>`吗?这是其中的一个。迭代器是一个让编译器将你编写的方法自动实现`IEnumerable<T>`或者`IEnumertor<T>`的语法。例如我可以用迭代器语法编写一个产生斐波那契数列的可遍历对象:
|
||||
|
||||
```csharp
|
||||
public static IEnumerable<int> Fib()
|
||||
{
|
||||
int prev = 0, next = 1;
|
||||
yield return prev;
|
||||
yield return next;
|
||||
|
||||
while (true)
|
||||
{
|
||||
int sum = prev + next;
|
||||
yield return sum;
|
||||
prev = next;
|
||||
next = sum;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这个方法可以直接用`foreach`遍历,也可以和`System.Linq.Enumerable`中提供的各种方法组合,也可以直接用一个`IEnumerator<T>`对象遍历。
|
||||
|
||||
```csharp
|
||||
foreach (int i in Fib())
|
||||
{
|
||||
if (i > 100) break;
|
||||
Console.Write($"{i} ");
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
foreach (int i in Fib().Take(12))
|
||||
{
|
||||
Console.Write($"{i} ");
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
using IEnumerator<int> e = Fib().GetEnumerator();
|
||||
while (e.MoveNext())
|
||||
{
|
||||
int i = e.Current;
|
||||
if (i > 100) break;
|
||||
Console.Write($"{i} ");
|
||||
}
|
||||
```
|
||||
|
|
@ -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上暑假期间那稀疏的提交,我就知道我又摸了一个暑假的鱼。
|
||||

|
||||

|
||||
即使我想写的很多项目都没有被扎实的推进下来,但是学习的一些的C语言还是让我受益匪浅。
|
||||
现在看来,这个假期真是,**学也没有学好,耍也没有耍好**的典型。
|
||||
|
||||
|
|
BIN
YaeBlog/source/posts/2021-final/1.png
(Stored with Git LFS)
Normal file
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)
BIN
YaeBlog/source/posts/2021-final/1.webp
(Stored with Git LFS)
Binary file not shown.
|
@ -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年可以算得上是一事无成的一年,还搞砸了不少的事情。在写代码上进展有限,成绩上大幅倒退,说好的六级英语和大学物理竞赛都没有参加,在年末应对疫情进展的时候更是把“不知所措”这个成语诠释的淋漓尽致。
|
||||
|
||||

|
||||

|
||||
|
||||
关于今年的人际交往和社会关系,我愿意用QQ2022年年终总结中的一张截屏来总结,这张图片透漏出一种无可救药的悲伤。
|
||||
|
||||

|
||||

|
||||
|
||||
## 展望
|
||||
|
||||
|
|
BIN
YaeBlog/source/posts/2022-final/2022-12-30-14-26-19-QQ_Image_1672381538441.jpg
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/2022-final/2022-12-30-14-26-19-QQ_Image_1672381538441.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/2022-final/2022-12-30-14-26-19-QQ_Image_1672381538441.webp
(Stored with Git LFS)
BIN
YaeBlog/source/posts/2022-final/2022-12-30-14-26-19-QQ_Image_1672381538441.webp
(Stored with Git LFS)
Binary file not shown.
BIN
YaeBlog/source/posts/2022-final/2022-12-30-14-28-12-QQ_Image_1672381543836.jpg
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/2022-final/2022-12-30-14-28-12-QQ_Image_1672381543836.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/2022-final/2022-12-30-14-28-12-QQ_Image_1672381543836.webp
(Stored with Git LFS)
BIN
YaeBlog/source/posts/2022-final/2022-12-30-14-28-12-QQ_Image_1672381543836.webp
(Stored with Git LFS)
Binary file not shown.
|
@ -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`渲染几张图当作桌面。
|
||||
|
||||

|
||||

|
||||
|
||||
> 在此感谢所有为此付出过汗水的前辈们,让我这个即将搬入老校区的萌新能提前一睹老校区的风采。
|
||||
|
||||
|
|
BIN
YaeBlog/source/posts/2022-summer-vacation/result1.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/2022-summer-vacation/result1.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/2022-summer-vacation/result1.webp
(Stored with Git LFS)
BIN
YaeBlog/source/posts/2022-summer-vacation/result1.webp
(Stored with Git LFS)
Binary file not shown.
|
@ -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站的时长:
|
||||
|
||||

|
||||

|
||||
|
||||
容易计算得出,我一共看了64天的B站,接近六分之一的时间都在看。虽然我确实有着在干活的时候黑听B站和把B站当作音乐播放器的习惯,但是这个时间未免有点太长了。下一年一定要在这个方面做出一定的改变,将更多的时间放在看书上面去,~~虽然写这句话的时候我就在黑听B站~~。
|
||||
|
||||
|
|
BIN
YaeBlog/source/posts/2023-final/image-20240303165826486.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/2023-final/image-20240303165826486.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/2023-final/image-20240303165826486.webp
(Stored with Git LFS)
BIN
YaeBlog/source/posts/2023-final/image-20240303165826486.webp
(Stored with Git LFS)
Binary file not shown.
|
@ -6,7 +6,6 @@ tags:
|
|||
- 年终总结
|
||||
---
|
||||
|
||||
|
||||
欸,年终总结难道不是应该在新年当天发出吗,什么已经是新年第三天了?!
|
||||
|
||||
然而年末偶遇流感病毒,头疼脑热强如怪物,拼尽全力也无法战胜。
|
||||
|
@ -71,7 +70,7 @@ tags:
|
|||
|
||||
不过我的B站观看时长再度增长30%,这好吗,这不好,~~有这么多时间刷B站,鬼知道你匆匆在哪了~~。
|
||||
|
||||

|
||||

|
||||
|
||||
### 未来
|
||||
|
||||
|
|
BIN
YaeBlog/source/posts/2024-final/image-20250115171809775.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/2024-final/image-20250115171809775.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/2024-final/image-20250115171809775.webp
(Stored with Git LFS)
BIN
YaeBlog/source/posts/2024-final/image-20250115171809775.webp
(Stored with Git LFS)
Binary file not shown.
|
@ -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`功能可以轻松的看出大家的贡献值()。
|
||||
|
||||

|
||||

|
||||
|
||||
## 一些技术上的收获
|
||||
|
||||
|
|
BIN
YaeBlog/source/posts/big-homework/1.png
(Stored with Git LFS)
Normal file
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)
BIN
YaeBlog/source/posts/big-homework/1.webp
(Stored with Git LFS)
Binary file not shown.
|
@ -1,12 +1,12 @@
|
|||
---
|
||||
title: 建立博客过程的记录
|
||||
date: 2022-04-08T11:52:32.0000000
|
||||
typora-root-url: 建立博客过程的记录
|
||||
date: 2022-04-08 11:52:32
|
||||
tags:
|
||||
- 技术笔记
|
||||
- 技术笔记
|
||||
---
|
||||
|
||||
|
||||
|
||||
当我已经在Python的浩瀚大海遨(zheng)游(zha)了半个暑假后,我决定尝试一下传说中程序员专用的学(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为名称创建一个博客文件夹,这个文件夹的内容为
|
||||
|
||||

|
||||

|
||||
|
||||
`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博客的初始界面
|
||||
|
||||

|
||||

|
||||
|
||||
这便说明安装成功了,~~可以开香槟了~~
|
||||
|
||||
|
|
BIN
YaeBlog/source/posts/build-blog-record/1.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/build-blog-record/1.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/build-blog-record/1.webp
(Stored with Git LFS)
BIN
YaeBlog/source/posts/build-blog-record/1.webp
(Stored with Git LFS)
Binary file not shown.
BIN
YaeBlog/source/posts/build-blog-record/2.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/build-blog-record/2.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/build-blog-record/2.webp
(Stored with Git LFS)
BIN
YaeBlog/source/posts/build-blog-record/2.webp
(Stored with Git LFS)
Binary file not shown.
|
@ -7,7 +7,6 @@ tags:
|
|||
---
|
||||
|
||||
|
||||
|
||||
我们编译是这样的,在本平台上编译只要敲三条命令就好了,而交叉编译要考虑的就很多了。
|
||||
|
||||
<!--more-->
|
||||
|
@ -46,7 +45,7 @@ tags:
|
|||
|
||||
通常一份GNU工具链只能针对一个平台进行编译,但是LLVM工具链是一套先天的交叉编译工具链,例如对于`llc`工具,使用`llc --version`命令可以看见该编译器可以生成多种目标平台上的汇编代码:
|
||||
|
||||

|
||||

|
||||
|
||||
在使用`clang++`时加上`--target=<triple>`指定目标三元组就可以进行交叉编译。
|
||||
|
||||
|
@ -63,7 +62,7 @@ int main()
|
|||
}
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
看样子交叉编译也不是开箱即用的。最开始我们猜想系统提供的LLVM工具链没有被配置为交叉编译,因此尝试在本地自行编译一套LLVM工具链。
|
||||
|
||||
|
@ -82,7 +81,7 @@ cmake ../llvm-project.src/llvm \
|
|||
|
||||
编译之后的成果会安装到`/usr/local/`目录下,而在`$PATH`环境变量中`/usr/local`位置将在`/usr`目录之前,因此调用时将会优先调用我们自行编译的LLVM工具链,而不是系统中安装的LLVM工具链。
|
||||
|
||||

|
||||

|
||||
|
||||
但是使用这套编译工具链仍然会爆出和之前一样的问题。说明这并不是系统安装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`。这一点我们也可以通过实验来验证。
|
||||
|
||||
于是回到编译`llvm`的目录下执行`cat install_manifest.txt | sudo xargs rm`。
|
||||
于是回到编译`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`都跑不过,下一步看看能不能运行一下运行时的测试集看看。
|
||||
|
||||

|
||||

|
||||
|
|
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824120646587.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824120646587.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824120646587.webp
(Stored with Git LFS)
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824120646587.webp
(Stored with Git LFS)
Binary file not shown.
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824121425007.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824121425007.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824121425007.webp
(Stored with Git LFS)
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824121425007.webp
(Stored with Git LFS)
Binary file not shown.
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824134158262.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824134158262.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824134158262.webp
(Stored with Git LFS)
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824134158262.webp
(Stored with Git LFS)
Binary file not shown.
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824153514149.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824153514149.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824153514149.webp
(Stored with Git LFS)
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824153514149.webp
(Stored with Git LFS)
Binary file not shown.
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824214145759.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824214145759.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824214145759.webp
(Stored with Git LFS)
BIN
YaeBlog/source/posts/build-dotnet-from-source/image-20240824214145759.webp
(Stored with Git LFS)
Binary file not shown.
|
@ -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:
|
|||
|
||||
我项目的结构大致如图所示:
|
||||
|
||||

|
||||

|
||||
|
||||
在`include`的头文件目录下有两个头文件,`rail.h`和`bus.h`,这两个头文件分别定义了两个结构体`rail_node_t`和`bus_t`。
|
||||
|
||||
|
@ -68,7 +68,7 @@ typedef struct bus bus_t;
|
|||
|
||||
项目的`test`文件夹下是单元测试文件夹,但是在编译的时候会报错
|
||||
|
||||

|
||||

|
||||
|
||||
大意就是在一个google test内部的头文件中有几个函数找不到定义,这个函数都位于`io.h`这个头文件中。
|
||||
|
||||
|
|
BIN
YaeBlog/source/posts/c-include-problems/1.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/c-include-problems/1.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/c-include-problems/1.webp
(Stored with Git LFS)
BIN
YaeBlog/source/posts/c-include-problems/1.webp
(Stored with Git LFS)
Binary file not shown.
BIN
YaeBlog/source/posts/c-include-problems/2.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/c-include-problems/2.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/c-include-problems/2.webp
(Stored with Git LFS)
BIN
YaeBlog/source/posts/c-include-problems/2.webp
(Stored with Git LFS)
Binary file not shown.
|
@ -5,14 +5,13 @@ tags:
|
|||
- 杂谈
|
||||
---
|
||||
|
||||
|
||||
2024年的中国计算机大会于10月24日到10月26日在浙江省金华市东阳市横店镇举办,而鄙人在下不才我,有幸受到实验室资助前去参观学习。
|
||||
|
||||
<!--more-->
|
||||
|
||||
首先开幕式镇楼。
|
||||
|
||||

|
||||

|
||||
|
||||
## 学术上
|
||||
|
||||
|
@ -22,11 +21,11 @@ tags:
|
|||
|
||||
第一个报告是华为庞加莱实验室秦彬娟老师的《异构智算时代的操作系统演进》。报告高屋建瓴,从比较宏观的角度上介绍了当前异构融合操作系统诞生的背景、发展的方向。在报告中重点介绍了一种异构融合操作系统的设计思路:通过三层架构,基于互联池化技术,构建AI时代的融合算力系统。系统中的三层包括:(1)池化基础底层,包括多设备的融合和池化设备虚拟化;(2)异构融合核心子系统,例如异构融合调度系统、异构融合内存和异构融合存储系统;(3)异构核心服务。总的来说,这个报告在一定程度上勾勒出了未来一个异构融合操作系统应有的各项功能,但是显然这一操作系统的实现还存在着明显的困难。
|
||||
|
||||

|
||||

|
||||
|
||||
下面一个报告是较为有干货的报告,北京航空航天大学刘瀚骋老师的《异构融合OS及多样性内存管理框架》。报告中介绍了一个称作`FMMU`的系统,是对于异构融合操作系统中内存管理系统的探索。报告中首先介绍了内存池化技术对于异构融合操作系统的重要性,指出分布式共享内存(Distributed Shared Memory)可能是实现内存池化技术的未来。然后介绍了将部分内存管理中的计算卸载到可编程网络硬件中来加速分布式内存访问的新思路。最后在报告中提到了内存管理技术如何解决错误预测和错误回复的问题。虽然在听的时候没太注意,但是现在总结的时候才发现这个报告的思路似乎有点混乱,尤其是最后一点和内存管理系统并没有什么直接的关系,而且这个内存管理系统似乎不是**异构系统**的内存管理,反而是分布式系统的内存管理。不过总的来说,这个报告还是非常实际的,介绍了不少当前异构融合操作系统中的内存管理面临的问题和解决问题的探索。
|
||||
|
||||

|
||||

|
||||
|
||||
第三个报告是国防科技大学李东升老师的《异构计算环境下的分布式深度学习训练》。报告首先从李老师的主业——并行计算起手,介绍了深度学习训练过程中主要的各种并行方法,例如数据并行、模型并行和混合并行等,指出目前大模型的并行训练存在着计算/存储/通信难的问题。因此,提出了一个智能模型训练并行任务划分方法:(1)基于符号算子的计算图定义方法;(2)面向Transformer模型的流水线并行任务划分方法;(3)异构资源感知的流水线并行任务划分方法。然后针对分布式模型训练中通信调度存在的通信墙、数据依赖关系复杂等的问题,提出综合词嵌入表的稀疏通信调度技术、流水线并行的P2P通信调度技术、模型计算的统一操作执行引擎和网络链路感知的通信执行引擎的通信调度技术。最后提到了智能模型训练 的内存优化技术,针对现有重计算技术(re-computing)和存储交换(swapping)技术存在的问题,提出了一种面向大型智能模型训练的细粒度内存优化方法`DELTA`。
|
||||
|
||||
|
@ -50,7 +49,7 @@ Plane讨论没有参加。
|
|||
|
||||
第二个报告是南京大学冯新宇老师的《基于仓颉语言的嵌入式DSL开发》,同时冯新宇老师也是仓颉语言的首席架构师。冯老师的这个报告主要聚焦于仓颉语言提供的嵌入式DSL能力,而嵌入式DSL这一设计范式已经在前端开发中展现了不俗的潜力。报告中介绍了嵌入式DSL出现的背景,仓颉中为了提供嵌入式DSL而引入的语法糖、仓颉提供的嵌入式DSL工具箱等。虽然仓颉语言是一个主要面向上层应用开发的语言,但是仓颉中丰富的DSL能力还是给异构编程模型的设计提供了不少的启发。而且目前在各种深度学习编译器中DSL的应用也非常广泛,例如`triton`。
|
||||
|
||||

|
||||

|
||||
|
||||
第三个报告是在存算一体的芯片上做数据库的加速,第四个报告是OpenHarmony上`ArkTS`程序的静态分析,都没怎么听。
|
||||
|
||||
|
|
BIN
YaeBlog/source/posts/cncc-2024/image-20241102211959206.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/cncc-2024/image-20241102211959206.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/cncc-2024/image-20241102211959206.webp
(Stored with Git LFS)
BIN
YaeBlog/source/posts/cncc-2024/image-20241102211959206.webp
(Stored with Git LFS)
Binary file not shown.
BIN
YaeBlog/source/posts/cncc-2024/image-20241102212355390.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/cncc-2024/image-20241102212355390.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/cncc-2024/image-20241102212355390.webp
(Stored with Git LFS)
BIN
YaeBlog/source/posts/cncc-2024/image-20241102212355390.webp
(Stored with Git LFS)
Binary file not shown.
BIN
YaeBlog/source/posts/cncc-2024/image-20241102212536635.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/cncc-2024/image-20241102212536635.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/cncc-2024/image-20241102212536635.webp
(Stored with Git LFS)
BIN
YaeBlog/source/posts/cncc-2024/image-20241102212536635.webp
(Stored with Git LFS)
Binary file not shown.
BIN
YaeBlog/source/posts/cncc-2024/image-20241102212738598.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/cncc-2024/image-20241102212738598.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/cncc-2024/image-20241102212738598.webp
(Stored with Git LFS)
BIN
YaeBlog/source/posts/cncc-2024/image-20241102212738598.webp
(Stored with Git LFS)
Binary file not shown.
|
@ -1,12 +1,11 @@
|
|||
---
|
||||
title: 编译MediaPipe框架
|
||||
date: 2022-11-11T22:20:25.0000000
|
||||
tags:
|
||||
- C/C++
|
||||
- 技术笔记
|
||||
- C/C++
|
||||
- 技术笔记
|
||||
date: 2022-11-11 22:20:25
|
||||
---
|
||||
|
||||
|
||||
编译MediaPipe框架。
|
||||
<!--more-->
|
||||
|
||||
|
@ -199,7 +198,7 @@ bazel build -c opt --strip=ALWAYS \
|
|||
|
||||
如果在编译的过程中提示缺失`dx.jar`这个文件而且你用的SDK版本还是高于31的,那可能是SDK中缺失了这个文件,可以将SDk降级到30就含有这个文件了。我使用的解决办法比较离奇,我是将30版本的SDK文件中的这个文件软链接过来,解决了这个问题。
|
||||
|
||||

|
||||

|
||||
|
||||
编译消耗的时间可能比较的长,耐心等待即可。
|
||||
|
||||
|
@ -228,7 +227,7 @@ bazel build -c opt //mediapipe/graphs/pose_tracking:pose_tracking_gpu_binary_gra
|
|||
|
||||
然后还需要从服务器上下载`tflite`文件,`Pose Tracking`这个解决方案需要两个`tflite`文件,第一个是[pose_detection.tflite](https://storage.googleapis.com/mediapipe-assets/pose_detection.tflite),第二个文件则有三个不同的选择,分别对于解决方案中提供的三个质量版本:
|
||||
|
||||

|
||||

|
||||
|
||||
下载地址是[pose_landmark_full.tflite](https://storage.googleapis.com/mediapipe-assets/pose_landmark_full.tflite),[pose_landmark_heavy.tflite](https://storage.googleapis.com/mediapipe-assets/pose_landmark_heavy.tflite)和[pose_landmark_lite.tflite](https://storage.googleapis.com/mediapipe-assets/pose_landmark_lite.tflite)。
|
||||
|
||||
|
|
BIN
YaeBlog/source/posts/compile-mediapipe/2023-01-15-22-05-41-Screenshot_20230115_220521.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/compile-mediapipe/2023-01-15-22-05-41-Screenshot_20230115_220521.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/compile-mediapipe/2023-01-15-22-05-41-Screenshot_20230115_220521.webp
(Stored with Git LFS)
BIN
YaeBlog/source/posts/compile-mediapipe/2023-01-15-22-05-41-Screenshot_20230115_220521.webp
(Stored with Git LFS)
Binary file not shown.
BIN
YaeBlog/source/posts/compile-mediapipe/2023-01-19-20-20-40-Screenshot_20230119_202008.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/compile-mediapipe/2023-01-19-20-20-40-Screenshot_20230119_202008.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/compile-mediapipe/2023-01-19-20-20-40-Screenshot_20230119_202008.webp
(Stored with Git LFS)
BIN
YaeBlog/source/posts/compile-mediapipe/2023-01-19-20-20-40-Screenshot_20230119_202008.webp
(Stored with Git LFS)
Binary file not shown.
|
@ -1,21 +1,20 @@
|
|||
---
|
||||
title: 计算机系统结构——流水线复习
|
||||
date: 2024-06-12T20:27:25.0000000
|
||||
tags:
|
||||
- 计算机系统结构
|
||||
- 学习资料
|
||||
- 计算机系统结构
|
||||
- 学习资料
|
||||
date: 2024-06-12 20:27:25
|
||||
---
|
||||
|
||||
|
||||
让指令的各个执行阶段依次进行运行是一个简单而自然的想法,但是这种方式执行速度慢、运行效率低。因此一个很自然的想法就是将指令重叠起来运行,让执行功能部件被充分的利用起来,这就是**流水线**。
|
||||
|
||||
流水线的表示方法有两种。
|
||||
|
||||

|
||||

|
||||
|
||||
第一种被称作**连接图**,清晰的表达出了流水线内部的逻辑关系。
|
||||
|
||||

|
||||

|
||||
|
||||
> 上图中给出了两个流水线中的概念:通过时间和排空时间。其中通过时间又被称作装入时间,是指第一个任务进入流水线到完成的事件;排空时间则相反,是最后一个任务通过流水线的时间。
|
||||
|
||||
|
@ -41,7 +40,7 @@ tags:
|
|||
- 静态流水线,同一时间内,多功能流水线的各段只能按照同一种功能的方式连接。
|
||||
- 动态流水线,同一时间内,多功能流水线的各种可以按照不同的方式连接,执行不同的功能。
|
||||
|
||||

|
||||

|
||||
|
||||
按照流水线中是否存在反馈回路分类:
|
||||
|
||||
|
@ -59,7 +58,7 @@ tags:
|
|||
- 加速比,同一任务,不使用流水线所使用时间与使用流水线所用时间比。
|
||||
- 效率,流水线设备的利用率。
|
||||
|
||||

|
||||

|
||||
|
||||
在设计流水线的过程中存在若干问题。
|
||||
|
||||
|
@ -69,7 +68,7 @@ tags:
|
|||
|
||||
一个典型的五段流水线MIPS流水线:
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
|
|
BIN
YaeBlog/source/posts/computer-architecture-pipeline/image-20240612184855300.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/computer-architecture-pipeline/image-20240612184855300.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/computer-architecture-pipeline/image-20240612184855300.webp
(Stored with Git LFS)
BIN
YaeBlog/source/posts/computer-architecture-pipeline/image-20240612184855300.webp
(Stored with Git LFS)
Binary file not shown.
BIN
YaeBlog/source/posts/computer-architecture-pipeline/image-20240612184949777.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/computer-architecture-pipeline/image-20240612184949777.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/computer-architecture-pipeline/image-20240612184949777.webp
(Stored with Git LFS)
BIN
YaeBlog/source/posts/computer-architecture-pipeline/image-20240612184949777.webp
(Stored with Git LFS)
Binary file not shown.
BIN
YaeBlog/source/posts/computer-architecture-pipeline/image-20240612190426368.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/computer-architecture-pipeline/image-20240612190426368.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/computer-architecture-pipeline/image-20240612190426368.webp
(Stored with Git LFS)
BIN
YaeBlog/source/posts/computer-architecture-pipeline/image-20240612190426368.webp
(Stored with Git LFS)
Binary file not shown.
BIN
YaeBlog/source/posts/computer-architecture-pipeline/image-20240612192700169.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/computer-architecture-pipeline/image-20240612192700169.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/computer-architecture-pipeline/image-20240612192700169.webp
(Stored with Git LFS)
BIN
YaeBlog/source/posts/computer-architecture-pipeline/image-20240612192700169.webp
(Stored with Git LFS)
Binary file not shown.
BIN
YaeBlog/source/posts/computer-architecture-pipeline/image-20240612193301372.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/computer-architecture-pipeline/image-20240612193301372.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/computer-architecture-pipeline/image-20240612193301372.webp
(Stored with Git LFS)
BIN
YaeBlog/source/posts/computer-architecture-pipeline/image-20240612193301372.webp
(Stored with Git LFS)
Binary file not shown.
|
@ -1,12 +1,12 @@
|
|||
---
|
||||
title: 日用Linux挑战 第0篇 初见Arch Linux
|
||||
date: 2023-01-15T22:23:08.0000000
|
||||
tags:
|
||||
- Linux
|
||||
- 杂谈
|
||||
- Linux
|
||||
- 随笔
|
||||
date: 2023-01-15 22:23:08
|
||||
typora-root-url: daily-linux-0
|
||||
---
|
||||
|
||||
|
||||
在将开发重心移到`WSL`上一年之后,我最终还是决定完全抛弃Windows,转向使用Linux作为我日常使用的主力系统。目前,我已经使用Linux作为主力系统一个月了。
|
||||
|
||||
<!--more-->
|
||||
|
@ -92,7 +92,7 @@ sudo systemctl enable sddm.service
|
|||
|
||||
我目前实现的效果大概长这样:
|
||||
|
||||

|
||||

|
||||
|
||||
颇有一种`Windows`和`MacOS`杂交的风格。
|
||||
|
||||
|
@ -106,7 +106,7 @@ sudo systemctl enable sddm.service
|
|||
|
||||
先上一张`shell`的系统概览截图:
|
||||
|
||||

|
||||

|
||||
|
||||
终端模拟器直接使用的`konsole`,目前没有进行改动。
|
||||
|
||||
|
|
BIN
YaeBlog/source/posts/daily-linux-0/2023-01-12-13-28-38-Screenshot_20230112_132829.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/daily-linux-0/2023-01-12-13-28-38-Screenshot_20230112_132829.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/daily-linux-0/2023-01-12-13-28-38-Screenshot_20230112_132829.webp
(Stored with Git LFS)
BIN
YaeBlog/source/posts/daily-linux-0/2023-01-12-13-28-38-Screenshot_20230112_132829.webp
(Stored with Git LFS)
Binary file not shown.
BIN
YaeBlog/source/posts/daily-linux-0/2023-01-12-13-36-45-Screenshot_20230112_133628.png
(Stored with Git LFS)
Normal file
BIN
YaeBlog/source/posts/daily-linux-0/2023-01-12-13-36-45-Screenshot_20230112_133628.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/daily-linux-0/2023-01-12-13-36-45-Screenshot_20230112_133628.webp
(Stored with Git LFS)
BIN
YaeBlog/source/posts/daily-linux-0/2023-01-12-13-36-45-Screenshot_20230112_133628.webp
(Stored with Git LFS)
Binary file not shown.
|
@ -2,7 +2,7 @@
|
|||
title: 日用Linux挑战 第1篇 问题与挑战
|
||||
tags:
|
||||
- Linux
|
||||
- 杂谈
|
||||
- 随笔
|
||||
date: 2023-03-08 22:37:29
|
||||
---
|
||||
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
---
|
||||
title: 日用Linux挑战 第2篇 Wayland
|
||||
date: 2023-07-23T11:44:34.0000000
|
||||
tags:
|
||||
- 杂谈
|
||||
- Linux
|
||||
- 随笔
|
||||
- Linux
|
||||
date: 2023-07-23 11:44:34
|
||||
typora-root-url: daily-linux-2
|
||||
---
|
||||
|
||||
|
||||
|
||||
使用`Linux`6个月,我成功戒掉了原神。
|
||||
|
||||
<!--more-->
|
||||
|
@ -18,7 +18,7 @@ tags:
|
|||
|
||||
最近恰好被平铺式的窗口管理器种草,又在B站上看见一个动画绚丽的`wayland`合成器——[Hyprland](https://hyprland.org/),当即脑袋一热,就把`kde`干掉,装上了`hyprland`。
|
||||
|
||||

|
||||

|
||||
|
||||
安装`hyprland`的过程非常舒适,`hyprland`被打包为一个单独的二进制文件,使用`pacman`安装之后直接在`tty`下执行:
|
||||
|
||||
|
@ -46,7 +46,7 @@ Hyprland
|
|||
|
||||
各种在学习过程中遇到的工具软件:基本上都工作运行良好。当然因为没有设置缩放的问题而导致字体都很小。因为如果在配置文件中设置缩放之后会导致字体发虚。下面的截图就是我将我的2K显示屏设置为150%缩放的效果,~~虽然在截图中的效果不明显~~。目前在常用软件中唯一让我十分不满意的软件是`wps`,使用体验完全无法和`offices`相提并论,目前我正在研究使用`wine`运行`offices`,如果成功了就再水一篇博客庆祝一下。
|
||||
|
||||

|
||||

|
||||
|
||||
> 最新的进展是使用`wine`没法安装学校提供的`office 2021`,同时我又不愿意使用古老的`office`版本,但是我发现一个称作`onlyoffice`的第三方软件蛮好用的,等我试用一段时间再说。
|
||||
>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user