Compare commits
2 Commits
blog/async
...
726d39bcc9
| Author | SHA1 | Date | |
|---|---|---|---|
| 726d39bcc9 | |||
| f71a59f228 |
@@ -12,10 +12,7 @@ indent_style = space
|
|||||||
indent_size = 4
|
indent_size = 4
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
[{project.json,appsettings.json,appsettings.*.json}]
|
[project.json]
|
||||||
indent_size = 2
|
|
||||||
|
|
||||||
[*.{yaml,yml}]
|
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
# C# and Visual Basic files
|
# C# and Visual Basic files
|
||||||
|
|||||||
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -1,5 +1,2 @@
|
|||||||
*.png filter=lfs diff=lfs merge=lfs -text
|
*.png filter=lfs diff=lfs merge=lfs -text
|
||||||
*.jpg filter=lfs diff=lfs merge=lfs -text
|
*.jpg filter=lfs diff=lfs merge=lfs -text
|
||||||
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
|
||||||
*.avif filter=lfs diff=lfs merge=lfs -text
|
|
||||||
*.webp filter=lfs diff=lfs merge=lfs -text
|
|
||||||
|
|||||||
@@ -7,24 +7,23 @@ jobs:
|
|||||||
Build-Blog-Image:
|
Build-Blog-Image:
|
||||||
runs-on: archlinux
|
runs-on: archlinux
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code.
|
- uses: https://git.rrricardo.top/actions/checkout@v4
|
||||||
uses: http://github-mirrors.infra.svc.cluster.local/actions/checkout.git@v4
|
name: Check out code
|
||||||
with:
|
with:
|
||||||
lfs: true
|
lfs: true
|
||||||
- name: Build project.
|
- name: Build project
|
||||||
run: |
|
run: |
|
||||||
git submodule update --init
|
cd YaeBlog
|
||||||
podman pull mcr.azure.cn/dotnet/aspnet:10.0
|
dotnet publish
|
||||||
pwsh build.ps1 build
|
- name: Build docker image
|
||||||
- name: Workaround to make sure podman-login working.
|
|
||||||
run: |
|
run: |
|
||||||
mkdir -p /root/.docker
|
cd YaeBlog
|
||||||
- name: Login tencent cloud docker registry.
|
docker build . -t registry.cn-beijing.aliyuncs.com/jackfiled/blog:latest
|
||||||
uses: http://github-mirrors.infra.svc.cluster.local/actions/podman-login.git@v1
|
- name: Login aliyun docker registry
|
||||||
|
uses: https://git.rrricardo.top/actions/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ccr.ccs.tencentyun.com
|
registry: registry.cn-beijing.aliyuncs.com
|
||||||
username: 100044380877
|
username: 初冬的朝阳
|
||||||
password: ${{ secrets.TENCENT_REGISTRY_PASSWORD }}
|
password: ${{ secrets.ALIYUN_PASSWORD }}
|
||||||
auth_file_path: /etc/containers/auth.json
|
- name: Push docker image
|
||||||
- name: Push docker image.
|
run: docker push registry.cn-beijing.aliyuncs.com/jackfiled/blog:latest
|
||||||
run: podman push ccr.ccs.tencentyun.com/jackfiled/blog:latest
|
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -184,7 +184,6 @@ DocProject/Help/html
|
|||||||
|
|
||||||
# Click-Once directory
|
# Click-Once directory
|
||||||
publish/
|
publish/
|
||||||
out/
|
|
||||||
|
|
||||||
# Publish Web Output
|
# Publish Web Output
|
||||||
*.[Pp]ublish.xml
|
*.[Pp]ublish.xml
|
||||||
@@ -483,6 +482,3 @@ $RECYCLE.BIN/
|
|||||||
|
|
||||||
# Vim temporary swap files
|
# Vim temporary swap files
|
||||||
*.swp
|
*.swp
|
||||||
|
|
||||||
# Tailwind auto-generated stylesheet
|
|
||||||
*.g.css
|
|
||||||
|
|||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
|||||||
[submodule "third-party/BlazorSvgComponents"]
|
|
||||||
path = third-party/BlazorSvgComponents
|
|
||||||
url = https://git.rrricardo.top/jackfiled/BlazorSvgComponents.git
|
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using YaeBlog.Models;
|
using YaeBlog.Core.Models;
|
||||||
|
|
||||||
namespace YaeBlog.Abstraction;
|
namespace YaeBlog.Core.Abstractions;
|
||||||
|
|
||||||
public interface IEssayContentService
|
public interface IEssayContentService
|
||||||
{
|
{
|
||||||
public IEnumerable<BlogEssay> Essays { get; }
|
public IReadOnlyDictionary<string, BlogEssay> Essays { get; }
|
||||||
|
|
||||||
public int Count { get; }
|
|
||||||
|
|
||||||
public IReadOnlyDictionary<EssayTag, List<BlogEssay>> Tags { get; }
|
public IReadOnlyDictionary<EssayTag, List<BlogEssay>> Tags { get; }
|
||||||
|
|
||||||
@@ -18,8 +16,6 @@ public interface IEssayContentService
|
|||||||
|
|
||||||
public bool TryAdd(BlogEssay essay);
|
public bool TryAdd(BlogEssay essay);
|
||||||
|
|
||||||
public bool TryGetEssay(string filename, [NotNullWhen(true)] out BlogEssay? essay);
|
|
||||||
|
|
||||||
public void RefreshTags();
|
public void RefreshTags();
|
||||||
|
|
||||||
public void Clear();
|
public void Clear();
|
||||||
12
YaeBlog.Core/Abstractions/IEssayScanService.cs
Normal file
12
YaeBlog.Core/Abstractions/IEssayScanService.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using YaeBlog.Core.Models;
|
||||||
|
|
||||||
|
namespace YaeBlog.Core.Abstractions;
|
||||||
|
|
||||||
|
public interface IEssayScanService
|
||||||
|
{
|
||||||
|
public Task<BlogContents> ScanContents();
|
||||||
|
|
||||||
|
public Task SaveBlogContent(BlogContent content, bool isDraft = true);
|
||||||
|
|
||||||
|
public Task<ImageScanResult> ScanImages();
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using YaeBlog.Models;
|
using YaeBlog.Core.Models;
|
||||||
|
|
||||||
namespace YaeBlog.Abstraction;
|
namespace YaeBlog.Core.Abstractions;
|
||||||
|
|
||||||
public interface IPostRenderProcessor
|
public interface IPostRenderProcessor
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using YaeBlog.Models;
|
using YaeBlog.Core.Models;
|
||||||
|
|
||||||
namespace YaeBlog.Abstraction;
|
namespace YaeBlog.Core.Abstractions;
|
||||||
|
|
||||||
public interface IPreRenderProcessor
|
public interface IPreRenderProcessor
|
||||||
{
|
{
|
||||||
8
YaeBlog.Core/Components/_Imports.razor
Normal file
8
YaeBlog.Core/Components/_Imports.razor
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
@using System.Net.Http
|
||||||
|
@using System.Net.Http.Json
|
||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||||
|
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
|
@using Microsoft.JSInterop
|
||||||
34
YaeBlog.Core/Extensions/ServiceCollectionExtensions.cs
Normal file
34
YaeBlog.Core/Extensions/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using Markdig;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
using YamlDotNet.Serialization.NamingConventions;
|
||||||
|
|
||||||
|
namespace YaeBlog.Core.Extensions;
|
||||||
|
|
||||||
|
public static class ServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddMarkdig(this IServiceCollection collection)
|
||||||
|
{
|
||||||
|
MarkdownPipelineBuilder builder = new();
|
||||||
|
|
||||||
|
builder.UseAdvancedExtensions();
|
||||||
|
|
||||||
|
collection.AddSingleton<MarkdownPipeline>(_ => builder.Build());
|
||||||
|
|
||||||
|
return collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IServiceCollection AddYamlParser(this IServiceCollection collection)
|
||||||
|
{
|
||||||
|
DeserializerBuilder deserializerBuilder = new();
|
||||||
|
deserializerBuilder.WithNamingConvention(CamelCaseNamingConvention.Instance);
|
||||||
|
deserializerBuilder.IgnoreUnmatchedProperties();
|
||||||
|
collection.AddSingleton(deserializerBuilder.Build());
|
||||||
|
|
||||||
|
SerializerBuilder serializerBuilder = new();
|
||||||
|
serializerBuilder.WithNamingConvention(CamelCaseNamingConvention.Instance);
|
||||||
|
collection.AddSingleton(serializerBuilder.Build());
|
||||||
|
|
||||||
|
return collection;
|
||||||
|
}
|
||||||
|
}
|
||||||
50
YaeBlog.Core/Extensions/WebApplicationBuilderExtensions.cs
Normal file
50
YaeBlog.Core/Extensions/WebApplicationBuilderExtensions.cs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
using AngleSharp;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using YaeBlog.Core.Abstractions;
|
||||||
|
using YaeBlog.Core.Models;
|
||||||
|
using YaeBlog.Core.Processors;
|
||||||
|
using YaeBlog.Core.Services;
|
||||||
|
|
||||||
|
namespace YaeBlog.Core.Extensions;
|
||||||
|
|
||||||
|
public static class WebApplicationBuilderExtensions
|
||||||
|
{
|
||||||
|
public static WebApplicationBuilder AddYaeBlog(this WebApplicationBuilder builder)
|
||||||
|
{
|
||||||
|
builder.Services.Configure<BlogOptions>(builder.Configuration.GetSection(BlogOptions.OptionName));
|
||||||
|
|
||||||
|
builder.Services.AddHttpClient();
|
||||||
|
|
||||||
|
builder.Services.AddMarkdig();
|
||||||
|
builder.Services.AddYamlParser();
|
||||||
|
builder.Services.AddSingleton<IConfiguration>(_ => Configuration.Default);
|
||||||
|
builder.Services.AddSingleton<IEssayScanService, EssayScanService>();
|
||||||
|
builder.Services.AddSingleton<RendererService>();
|
||||||
|
builder.Services.AddSingleton<IEssayContentService, EssayContentService>();
|
||||||
|
builder.Services.AddTransient<ImagePostRenderProcessor>();
|
||||||
|
builder.Services.AddTransient<CodeBlockPostRenderProcessor>();
|
||||||
|
builder.Services.AddTransient<TablePostRenderProcessor>();
|
||||||
|
builder.Services.AddTransient<HeadlinePostRenderProcessor>();
|
||||||
|
builder.Services.AddTransient<BlogOptions>(provider =>
|
||||||
|
provider.GetRequiredService<IOptions<BlogOptions>>().Value);
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WebApplicationBuilder AddServer(this WebApplicationBuilder builder)
|
||||||
|
{
|
||||||
|
builder.Services.AddHostedService<BlogHostedService>();
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WebApplicationBuilder AddWatcher(this WebApplicationBuilder builder)
|
||||||
|
{
|
||||||
|
builder.Services.AddTransient<BlogChangeWatcher>();
|
||||||
|
builder.Services.AddHostedService<BlogHotReloadService>();
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
using YaeBlog.Abstraction;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using YaeBlog.Processors;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using YaeBlog.Services;
|
using YaeBlog.Core.Abstractions;
|
||||||
|
using YaeBlog.Core.Processors;
|
||||||
|
using YaeBlog.Core.Services;
|
||||||
|
|
||||||
namespace YaeBlog.Extensions;
|
namespace YaeBlog.Core.Extensions;
|
||||||
|
|
||||||
public static class WebApplicationExtensions
|
public static class WebApplicationExtensions
|
||||||
{
|
{
|
||||||
public static void UseYaeBlog(this WebApplication application)
|
public static void UseYaeBlog(this WebApplication application)
|
||||||
{
|
{
|
||||||
application.UsePostRenderProcessor<ImagePostRenderProcessor>();
|
application.UsePostRenderProcessor<ImagePostRenderProcessor>();
|
||||||
|
application.UsePostRenderProcessor<CodeBlockPostRenderProcessor>();
|
||||||
|
application.UsePostRenderProcessor<TablePostRenderProcessor>();
|
||||||
application.UsePostRenderProcessor<HeadlinePostRenderProcessor>();
|
application.UsePostRenderProcessor<HeadlinePostRenderProcessor>();
|
||||||
application.UsePostRenderProcessor<EssayStylesPostRenderProcessor>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UsePreRenderProcessor<T>(this WebApplication application) where T : IPreRenderProcessor
|
private static void UsePreRenderProcessor<T>(this WebApplication application) where T : IPreRenderProcessor
|
||||||
10
YaeBlog.Core/Models/AboutInfo.cs
Normal file
10
YaeBlog.Core/Models/AboutInfo.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace YaeBlog.Core.Models;
|
||||||
|
|
||||||
|
public class AboutInfo
|
||||||
|
{
|
||||||
|
public required string Introduction { get; set; }
|
||||||
|
|
||||||
|
public required string Description { get; set; }
|
||||||
|
|
||||||
|
public required string AvatarImage { get; set; }
|
||||||
|
}
|
||||||
10
YaeBlog.Core/Models/BlogContent.cs
Normal file
10
YaeBlog.Core/Models/BlogContent.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace YaeBlog.Core.Models;
|
||||||
|
|
||||||
|
public class BlogContent
|
||||||
|
{
|
||||||
|
public required string FileName { get; init; }
|
||||||
|
|
||||||
|
public required MarkdownMetadata Metadata { get; init; }
|
||||||
|
|
||||||
|
public required string FileContent { get; set; }
|
||||||
|
}
|
||||||
10
YaeBlog.Core/Models/BlogContents.cs
Normal file
10
YaeBlog.Core/Models/BlogContents.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace YaeBlog.Core.Models;
|
||||||
|
|
||||||
|
public sealed class BlogContents(ConcurrentBag<BlogContent> drafts, ConcurrentBag<BlogContent> posts)
|
||||||
|
{
|
||||||
|
public ConcurrentBag<BlogContent> Drafts { get; } = drafts;
|
||||||
|
|
||||||
|
public ConcurrentBag<BlogContent> Posts { get; } = posts;
|
||||||
|
}
|
||||||
52
YaeBlog.Core/Models/BlogEssay.cs
Normal file
52
YaeBlog.Core/Models/BlogEssay.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
namespace YaeBlog.Core.Models;
|
||||||
|
|
||||||
|
public class BlogEssay : IComparable<BlogEssay>
|
||||||
|
{
|
||||||
|
public required string Title { get; init; }
|
||||||
|
|
||||||
|
public required string FileName { get; init; }
|
||||||
|
|
||||||
|
public required DateTime PublishTime { get; init; }
|
||||||
|
|
||||||
|
public required string Description { get; init; }
|
||||||
|
|
||||||
|
public required uint WordCount { get; init; }
|
||||||
|
|
||||||
|
public required string ReadTime { get; init; }
|
||||||
|
|
||||||
|
public List<string> Tags { get; } = [];
|
||||||
|
|
||||||
|
public required string HtmlContent { get; init; }
|
||||||
|
|
||||||
|
public BlogEssay WithNewHtmlContent(string newHtmlContent)
|
||||||
|
{
|
||||||
|
var essay = new BlogEssay
|
||||||
|
{
|
||||||
|
Title = Title,
|
||||||
|
FileName = FileName,
|
||||||
|
PublishTime = PublishTime,
|
||||||
|
Description = Description,
|
||||||
|
WordCount = WordCount,
|
||||||
|
ReadTime = ReadTime,
|
||||||
|
HtmlContent = newHtmlContent
|
||||||
|
};
|
||||||
|
essay.Tags.AddRange(Tags);
|
||||||
|
|
||||||
|
return essay;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CompareTo(BlogEssay? other)
|
||||||
|
{
|
||||||
|
if (other is null)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PublishTime.CompareTo(other.PublishTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{Title}-{PublishTime}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace YaeBlog.Models;
|
namespace YaeBlog.Core.Models;
|
||||||
|
|
||||||
public class BlogHeadline(string title, string selectorId)
|
public class BlogHeadline(string title, string selectorId)
|
||||||
{
|
{
|
||||||
26
YaeBlog.Core/Models/BlogOptions.cs
Normal file
26
YaeBlog.Core/Models/BlogOptions.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
namespace YaeBlog.Core.Models;
|
||||||
|
|
||||||
|
public class BlogOptions
|
||||||
|
{
|
||||||
|
public const string OptionName = "Blog";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 博客markdown文件的根目录
|
||||||
|
/// </summary>
|
||||||
|
public required string Root { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 博客正文的广而告之
|
||||||
|
/// </summary>
|
||||||
|
public required string Announcement { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 博客的起始年份
|
||||||
|
/// </summary>
|
||||||
|
public required int StartYear { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 博客的友链
|
||||||
|
/// </summary>
|
||||||
|
public required List<FriendLink> Links { get; set; }
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
|
|
||||||
namespace YaeBlog.Models;
|
namespace YaeBlog.Core.Models;
|
||||||
|
|
||||||
public class EssayTag(string tagName) : IEquatable<EssayTag>
|
public class EssayTag(string tagName) : IEquatable<EssayTag>
|
||||||
{
|
{
|
||||||
27
YaeBlog.Core/Models/FriendLink.cs
Normal file
27
YaeBlog.Core/Models/FriendLink.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
namespace YaeBlog.Core.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 友链模型类
|
||||||
|
/// </summary>
|
||||||
|
public class FriendLink
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 友链名称
|
||||||
|
/// </summary>
|
||||||
|
public required string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 友链的简单介绍
|
||||||
|
/// </summary>
|
||||||
|
public required string Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 友链地址
|
||||||
|
/// </summary>
|
||||||
|
public required string Link { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 头像地址
|
||||||
|
/// </summary>
|
||||||
|
public required string AvatarImage { get; set; }
|
||||||
|
}
|
||||||
3
YaeBlog.Core/Models/ImageScanResult.cs
Normal file
3
YaeBlog.Core/Models/ImageScanResult.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace YaeBlog.Core.Models;
|
||||||
|
|
||||||
|
public record struct ImageScanResult(List<FileInfo> UnusedImages, List<FileInfo> NotFoundImages);
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
namespace YaeBlog.Models;
|
namespace YaeBlog.Core.Models;
|
||||||
|
|
||||||
public class MarkdownMetadata
|
public class MarkdownMetadata
|
||||||
{
|
{
|
||||||
public string? Title { get; set; }
|
public string? Title { get; set; }
|
||||||
|
|
||||||
public string? Date { get; set; }
|
public DateTime? Date { get; set; }
|
||||||
|
|
||||||
public string? UpdateTime { get; set; }
|
|
||||||
|
|
||||||
public List<string>? Tags { get; set; }
|
public List<string>? Tags { get; set; }
|
||||||
}
|
}
|
||||||
29
YaeBlog.Core/Processors/CodeBlockPostRenderProcessor.cs
Normal file
29
YaeBlog.Core/Processors/CodeBlockPostRenderProcessor.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using AngleSharp;
|
||||||
|
using AngleSharp.Dom;
|
||||||
|
using YaeBlog.Core.Abstractions;
|
||||||
|
using YaeBlog.Core.Models;
|
||||||
|
|
||||||
|
namespace YaeBlog.Core.Processors;
|
||||||
|
|
||||||
|
public class CodeBlockPostRenderProcessor : IPostRenderProcessor
|
||||||
|
{
|
||||||
|
public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
|
||||||
|
{
|
||||||
|
BrowsingContext context = new(Configuration.Default);
|
||||||
|
IDocument document = await context.OpenAsync(
|
||||||
|
req => req.Content(essay.HtmlContent));
|
||||||
|
|
||||||
|
IEnumerable<IElement> preElements = from e in document.All
|
||||||
|
where e.LocalName == "pre"
|
||||||
|
select e;
|
||||||
|
|
||||||
|
foreach (IElement element in preElements)
|
||||||
|
{
|
||||||
|
element.ClassList.Add("p-3 text-bg-secondary rounded-1");
|
||||||
|
}
|
||||||
|
|
||||||
|
return essay.WithNewHtmlContent(document.DocumentElement.OuterHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name => nameof(CodeBlockPostRenderProcessor);
|
||||||
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
using AngleSharp;
|
using AngleSharp;
|
||||||
using AngleSharp.Dom;
|
using AngleSharp.Dom;
|
||||||
using YaeBlog.Abstraction;
|
using Microsoft.Extensions.Logging;
|
||||||
using YaeBlog.Models;
|
using YaeBlog.Core.Abstractions;
|
||||||
|
using YaeBlog.Core.Models;
|
||||||
|
|
||||||
namespace YaeBlog.Processors;
|
namespace YaeBlog.Core.Processors;
|
||||||
|
|
||||||
public class HeadlinePostRenderProcessor(
|
public class HeadlinePostRenderProcessor(
|
||||||
AngleSharp.IConfiguration angleConfiguration,
|
IConfiguration angleConfiguration,
|
||||||
IEssayContentService essayContentService,
|
IEssayContentService essayContentService,
|
||||||
ILogger<HeadlinePostRenderProcessor> logger) : IPostRenderProcessor
|
ILogger<HeadlinePostRenderProcessor> logger) : IPostRenderProcessor
|
||||||
{
|
{
|
||||||
@@ -67,7 +68,7 @@ public class HeadlinePostRenderProcessor(
|
|||||||
logger.LogWarning("Failed to add headline of {}.", essay.FileName);
|
logger.LogWarning("Failed to add headline of {}.", essay.FileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
return essay with { HtmlContent = document.DocumentElement.OuterHtml };
|
return essay.WithNewHtmlContent(document.DocumentElement.OuterHtml);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static BlogHeadline ParserHeadlineElement(IElement element)
|
private static BlogHeadline ParserHeadlineElement(IElement element)
|
||||||
@@ -1,28 +1,24 @@
|
|||||||
using AngleSharp;
|
using AngleSharp;
|
||||||
using AngleSharp.Dom;
|
using AngleSharp.Dom;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using YaeBlog.Abstraction;
|
using YaeBlog.Core.Abstractions;
|
||||||
using YaeBlog.Core.Exceptions;
|
using YaeBlog.Core.Exceptions;
|
||||||
using YaeBlog.Models;
|
using YaeBlog.Core.Models;
|
||||||
|
|
||||||
namespace YaeBlog.Processors;
|
namespace YaeBlog.Core.Processors;
|
||||||
|
|
||||||
/// <summary>
|
public class ImagePostRenderProcessor(ILogger<ImagePostRenderProcessor> logger,
|
||||||
/// 图片地址路径后处理器
|
|
||||||
/// 将本地图片地址修改为图片API地址
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger"></param>
|
|
||||||
/// <param name="options"></param>
|
|
||||||
public class ImagePostRenderProcessor(
|
|
||||||
ILogger<ImagePostRenderProcessor> logger,
|
|
||||||
IOptions<BlogOptions> options)
|
IOptions<BlogOptions> options)
|
||||||
: IPostRenderProcessor
|
: IPostRenderProcessor
|
||||||
{
|
{
|
||||||
|
private static readonly IConfiguration s_configuration = Configuration.Default;
|
||||||
|
|
||||||
private readonly BlogOptions _options = options.Value;
|
private readonly BlogOptions _options = options.Value;
|
||||||
|
|
||||||
public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
|
public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
|
||||||
{
|
{
|
||||||
BrowsingContext context = new(Configuration.Default);
|
BrowsingContext context = new(s_configuration);
|
||||||
IDocument html = await context.OpenAsync(
|
IDocument html = await context.OpenAsync(
|
||||||
req => req.Content(essay.HtmlContent));
|
req => req.Content(essay.HtmlContent));
|
||||||
|
|
||||||
@@ -36,27 +32,23 @@ public class ImagePostRenderProcessor(
|
|||||||
if (attr is not null)
|
if (attr is not null)
|
||||||
{
|
{
|
||||||
logger.LogDebug("Found image link: '{}'", attr.Value);
|
logger.LogDebug("Found image link: '{}'", attr.Value);
|
||||||
attr.Value = GenerateImageLink(attr.Value, essay.FileName, essay.IsDraft);
|
attr.Value = GenerateImageLink(attr.Value, essay.FileName);
|
||||||
}
|
}
|
||||||
|
element.ClassList.Add("essay-image");
|
||||||
}
|
}
|
||||||
|
return essay.WithNewHtmlContent(html.DocumentElement.OuterHtml);
|
||||||
return essay with { HtmlContent = html.DocumentElement.OuterHtml };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Name => nameof(ImagePostRenderProcessor);
|
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))
|
if (!filename.Contains(essayFilename))
|
||||||
{
|
{
|
||||||
filename = Path.Combine(essayFilename, filename);
|
filename = Path.Combine(essayFilename, filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
filename = isDraft
|
filename = Path.Combine(_options.Root, "posts", filename);
|
||||||
? Path.Combine(_options.Root, "drafts", filename)
|
|
||||||
: Path.Combine(_options.Root, "posts", filename);
|
|
||||||
|
|
||||||
if (!Path.Exists(filename))
|
if (!Path.Exists(filename))
|
||||||
{
|
{
|
||||||
@@ -65,7 +57,7 @@ public class ImagePostRenderProcessor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
string imageLink = "api/files/" + filename;
|
string imageLink = "api/files/" + filename;
|
||||||
logger.LogDebug("Generate image link '{link}' for image file '{filename}'.",
|
logger.LogDebug("Generate image link '{}' for image file '{}'.",
|
||||||
imageLink, filename);
|
imageLink, filename);
|
||||||
|
|
||||||
return imageLink;
|
return imageLink;
|
||||||
34
YaeBlog.Core/Processors/TablePostRenderProcessor.cs
Normal file
34
YaeBlog.Core/Processors/TablePostRenderProcessor.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using AngleSharp;
|
||||||
|
using AngleSharp.Dom;
|
||||||
|
using AngleSharp.Html.Dom;
|
||||||
|
using YaeBlog.Core.Abstractions;
|
||||||
|
using YaeBlog.Core.Models;
|
||||||
|
|
||||||
|
namespace YaeBlog.Core.Processors;
|
||||||
|
|
||||||
|
public class TablePostRenderProcessor: IPostRenderProcessor
|
||||||
|
{
|
||||||
|
public async Task<BlogEssay> ProcessAsync(BlogEssay essay)
|
||||||
|
{
|
||||||
|
BrowsingContext browsingContext = new(Configuration.Default);
|
||||||
|
IDocument document = await browsingContext.OpenAsync(
|
||||||
|
req => req.Content(essay.HtmlContent));
|
||||||
|
|
||||||
|
IEnumerable<IHtmlTableElement> tableElements = from item in document.All
|
||||||
|
where item.LocalName == "table"
|
||||||
|
select item as IHtmlTableElement;
|
||||||
|
|
||||||
|
foreach (IHtmlTableElement element in tableElements)
|
||||||
|
{
|
||||||
|
IHtmlDivElement divElement = document.CreateElement<IHtmlDivElement>();
|
||||||
|
divElement.InnerHtml = element.OuterHtml;
|
||||||
|
divElement.ClassList.Add("py-2", "table-wrapper");
|
||||||
|
|
||||||
|
element.Replace(divElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
return essay.WithNewHtmlContent(document.DocumentElement.OuterHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name => nameof(TablePostRenderProcessor);
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Logging;
|
||||||
using YaeBlog.Models;
|
using Microsoft.Extensions.Options;
|
||||||
|
using YaeBlog.Core.Models;
|
||||||
|
|
||||||
namespace YaeBlog.Services;
|
namespace YaeBlog.Core.Services;
|
||||||
|
|
||||||
public sealed class BlogChangeWatcher : IDisposable
|
public sealed class BlogChangeWatcher : IDisposable
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
namespace YaeBlog.Services;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace YaeBlog.Core.Services;
|
||||||
|
|
||||||
public class BlogHostedService(
|
public class BlogHostedService(
|
||||||
ILogger<BlogHostedService> logger,
|
ILogger<BlogHostedService> logger,
|
||||||
@@ -6,12 +9,14 @@ public class BlogHostedService(
|
|||||||
{
|
{
|
||||||
public async Task StartAsync(CancellationToken cancellationToken)
|
public async Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
logger.LogInformation("Failed to load cache, render essays.");
|
logger.LogInformation("Welcome to YaeBlog!");
|
||||||
|
|
||||||
await rendererService.RenderAsync();
|
await rendererService.RenderAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken)
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
logger.LogInformation("YaeBlog stopped!\nHave a nice day!");
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
35
YaeBlog.Core/Services/BlogHotReloadService.cs
Normal file
35
YaeBlog.Core/Services/BlogHotReloadService.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using YaeBlog.Core.Abstractions;
|
||||||
|
|
||||||
|
namespace YaeBlog.Core.Services;
|
||||||
|
|
||||||
|
public sealed class BlogHotReloadService(
|
||||||
|
RendererService rendererService,
|
||||||
|
IEssayContentService essayContentService,
|
||||||
|
BlogChangeWatcher watcher,
|
||||||
|
ILogger<BlogHotReloadService> logger) : BackgroundService
|
||||||
|
{
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
logger.LogInformation("BlogHotReloadService is starting.");
|
||||||
|
|
||||||
|
await rendererService.RenderAsync();
|
||||||
|
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
logger.LogDebug("Watching file changes...");
|
||||||
|
string? changFile = await watcher.WaitForChange(stoppingToken);
|
||||||
|
|
||||||
|
if (changFile is null)
|
||||||
|
{
|
||||||
|
logger.LogInformation("BlogHotReloadService is stopping.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("{} changed, re-rendering.", changFile);
|
||||||
|
essayContentService.Clear();
|
||||||
|
await rendererService.RenderAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,36 +1,23 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using YaeBlog.Abstraction;
|
using YaeBlog.Core.Abstractions;
|
||||||
using YaeBlog.Models;
|
using YaeBlog.Core.Models;
|
||||||
|
|
||||||
namespace YaeBlog.Services;
|
namespace YaeBlog.Core.Services;
|
||||||
|
|
||||||
public class EssayContentService : IEssayContentService
|
public class EssayContentService : IEssayContentService
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, BlogEssay> _essays = new();
|
private readonly ConcurrentDictionary<string, BlogEssay> _essays = new();
|
||||||
|
|
||||||
private readonly List<BlogEssay> _sortedEssays = [];
|
|
||||||
|
|
||||||
private readonly Dictionary<EssayTag, List<BlogEssay>> _tags = [];
|
private readonly Dictionary<EssayTag, List<BlogEssay>> _tags = [];
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, BlogHeadline> _headlines = new();
|
private readonly ConcurrentDictionary<string, BlogHeadline> _headlines = new();
|
||||||
|
|
||||||
public bool TryAdd(BlogEssay essay)
|
public bool TryAdd(BlogEssay essay) => _essays.TryAdd(essay.FileName, essay);
|
||||||
{
|
|
||||||
_sortedEssays.Add(essay);
|
|
||||||
return _essays.TryAdd(essay.FileName, essay);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryAddHeadline(string filename, BlogHeadline headline) => _headlines.TryAdd(filename, headline);
|
public bool TryAddHeadline(string filename, BlogHeadline headline) => _headlines.TryAdd(filename, headline);
|
||||||
|
|
||||||
public IEnumerable<BlogEssay> Essays => _sortedEssays;
|
public IReadOnlyDictionary<string, BlogEssay> Essays => _essays;
|
||||||
|
|
||||||
public int Count => _sortedEssays.Count;
|
|
||||||
|
|
||||||
public bool TryGetEssay(string filename, [NotNullWhen(true)] out BlogEssay? essay)
|
|
||||||
{
|
|
||||||
return _essays.TryGetValue(filename, out essay);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IReadOnlyDictionary<EssayTag, List<BlogEssay>> Tags => _tags;
|
public IReadOnlyDictionary<EssayTag, List<BlogEssay>> Tags => _tags;
|
||||||
|
|
||||||
210
YaeBlog.Core/Services/EssayScanService.cs
Normal file
210
YaeBlog.Core/Services/EssayScanService.cs
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using YaeBlog.Core.Abstractions;
|
||||||
|
using YaeBlog.Core.Exceptions;
|
||||||
|
using YaeBlog.Core.Models;
|
||||||
|
using YamlDotNet.Core;
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
|
||||||
|
namespace YaeBlog.Core.Services;
|
||||||
|
|
||||||
|
public partial class EssayScanService(
|
||||||
|
ISerializer yamlSerializer,
|
||||||
|
IDeserializer yamlDeserializer,
|
||||||
|
IOptions<BlogOptions> blogOptions,
|
||||||
|
ILogger<EssayScanService> logger) : IEssayScanService
|
||||||
|
{
|
||||||
|
private readonly BlogOptions _blogOptions = blogOptions.Value;
|
||||||
|
|
||||||
|
public async Task<BlogContents> ScanContents()
|
||||||
|
{
|
||||||
|
ValidateDirectory(_blogOptions.Root, out DirectoryInfo drafts, out DirectoryInfo posts);
|
||||||
|
|
||||||
|
return new BlogContents(
|
||||||
|
await ScanContentsInternal(drafts),
|
||||||
|
await ScanContentsInternal(posts));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveBlogContent(BlogContent content, bool isDraft = true)
|
||||||
|
{
|
||||||
|
ValidateDirectory(_blogOptions.Root, out DirectoryInfo drafts, out DirectoryInfo posts);
|
||||||
|
|
||||||
|
FileInfo targetFile = isDraft
|
||||||
|
? 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
await using StreamWriter writer = targetFile.CreateText();
|
||||||
|
|
||||||
|
await writer.WriteAsync("---\n");
|
||||||
|
await writer.WriteAsync(yamlSerializer.Serialize(content.Metadata));
|
||||||
|
await writer.WriteAsync("---\n");
|
||||||
|
|
||||||
|
if (isDraft)
|
||||||
|
{
|
||||||
|
await writer.WriteLineAsync("<!--more-->");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await writer.WriteAsync(content.FileContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ConcurrentBag<BlogContent>> ScanContentsInternal(DirectoryInfo directory)
|
||||||
|
{
|
||||||
|
// 扫描以md结果的但是不是隐藏文件的文件
|
||||||
|
IEnumerable<FileInfo> markdownFiles = from file in directory.EnumerateFiles()
|
||||||
|
where file.Extension == ".md" && !file.Name.StartsWith('.')
|
||||||
|
select file;
|
||||||
|
|
||||||
|
ConcurrentBag<(string, string)> fileContents = [];
|
||||||
|
|
||||||
|
await Parallel.ForEachAsync(markdownFiles, async (file, token) =>
|
||||||
|
{
|
||||||
|
using StreamReader reader = file.OpenText();
|
||||||
|
fileContents.Add((file.Name, await reader.ReadToEndAsync(token)));
|
||||||
|
});
|
||||||
|
|
||||||
|
ConcurrentBag<BlogContent> contents = [];
|
||||||
|
|
||||||
|
await Task.Run(() =>
|
||||||
|
{
|
||||||
|
foreach ((string filename, string content) in fileContents)
|
||||||
|
{
|
||||||
|
int endPos = content.IndexOf("---", 4, StringComparison.Ordinal);
|
||||||
|
if (!content.StartsWith("---") || endPos is -1 or 0)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Failed to parse metadata from {}, skipped.", filename);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string metadataString = content[4..endPos];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
MarkdownMetadata metadata = yamlDeserializer.Deserialize<MarkdownMetadata>(metadataString);
|
||||||
|
logger.LogDebug("Scan metadata title: '{}' for {}.", metadata.Title, filename);
|
||||||
|
|
||||||
|
contents.Add(new BlogContent
|
||||||
|
{
|
||||||
|
FileName = filename[..^3], Metadata = metadata, FileContent = content[(endPos + 3)..]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (YamlException e)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Failed to parser metadata from {} due to {}, skipping", filename, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ImageScanResult> ScanImages()
|
||||||
|
{
|
||||||
|
BlogContents contents = await ScanContents();
|
||||||
|
ValidateDirectory(_blogOptions.Root, out DirectoryInfo drafts, out DirectoryInfo posts);
|
||||||
|
|
||||||
|
List<FileInfo> unusedFiles = [];
|
||||||
|
List<FileInfo> notFoundFiles = [];
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
Regex imageRegex = ImageRegex();
|
||||||
|
ConcurrentBag<FileInfo> unusedImage = [];
|
||||||
|
ConcurrentBag<FileInfo> notFoundImage = [];
|
||||||
|
|
||||||
|
Parallel.ForEach(contents, content =>
|
||||||
|
{
|
||||||
|
MatchCollection result = imageRegex.Matches(content.FileContent);
|
||||||
|
DirectoryInfo imageDirectory = new(Path.Combine(root.FullName, content.FileName));
|
||||||
|
|
||||||
|
Dictionary<string, bool> usedDictionary;
|
||||||
|
|
||||||
|
if (imageDirectory.Exists)
|
||||||
|
{
|
||||||
|
usedDictionary = (from file in imageDirectory.EnumerateFiles()
|
||||||
|
select new KeyValuePair<string, bool>(file.FullName, false)).ToDictionary();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
usedDictionary = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Match match in result)
|
||||||
|
{
|
||||||
|
string imageName = match.Groups[1].Value;
|
||||||
|
|
||||||
|
FileInfo usedFile = imageName.Contains(content.FileName)
|
||||||
|
? new FileInfo(Path.Combine(root.FullName, imageName))
|
||||||
|
: new FileInfo(Path.Combine(root.FullName, content.FileName, imageName));
|
||||||
|
|
||||||
|
if (usedDictionary.TryGetValue(usedFile.FullName, out _))
|
||||||
|
{
|
||||||
|
usedDictionary[usedFile.FullName] = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
notFoundImage.Add(usedFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (KeyValuePair<string, bool> pair in usedDictionary.Where(p => !p.Value))
|
||||||
|
{
|
||||||
|
unusedImage.Add(new FileInfo(pair.Key));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Task.FromResult(new ImageScanResult(unusedImage.ToList(), notFoundImage.ToList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex(@"\!\[.*?\]\((.*?)\)")]
|
||||||
|
private static partial Regex ImageRegex();
|
||||||
|
|
||||||
|
private void ValidateDirectory(string root, out DirectoryInfo drafts, out DirectoryInfo posts)
|
||||||
|
{
|
||||||
|
root = Path.Combine(Environment.CurrentDirectory, root);
|
||||||
|
DirectoryInfo rootDirectory = new(root);
|
||||||
|
|
||||||
|
if (!rootDirectory.Exists)
|
||||||
|
{
|
||||||
|
throw new BlogFileException($"'{root}' is not a directory.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootDirectory.EnumerateDirectories().All(dir => dir.Name != "drafts"))
|
||||||
|
{
|
||||||
|
throw new BlogFileException($"'{root}/drafts' not exists.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootDirectory.EnumerateDirectories().All(dir => dir.Name != "posts"))
|
||||||
|
{
|
||||||
|
throw new BlogFileException($"'{root}/posts' not exists.");
|
||||||
|
}
|
||||||
|
|
||||||
|
drafts = new DirectoryInfo(Path.Combine(root, "drafts"));
|
||||||
|
posts = new DirectoryInfo(Path.Combine(root, "posts"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,13 +3,14 @@ using System.Diagnostics;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Markdig;
|
using Markdig;
|
||||||
using YaeBlog.Abstraction;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using YaeBlog.Core.Abstractions;
|
||||||
using YaeBlog.Core.Exceptions;
|
using YaeBlog.Core.Exceptions;
|
||||||
using YaeBlog.Models;
|
using YaeBlog.Core.Models;
|
||||||
|
|
||||||
namespace YaeBlog.Services;
|
namespace YaeBlog.Core.Services;
|
||||||
|
|
||||||
public sealed partial class RendererService(
|
public partial class RendererService(
|
||||||
ILogger<RendererService> logger,
|
ILogger<RendererService> logger,
|
||||||
IEssayScanService essayScanService,
|
IEssayScanService essayScanService,
|
||||||
MarkdownPipeline markdownPipeline,
|
MarkdownPipeline markdownPipeline,
|
||||||
@@ -21,54 +22,52 @@ public sealed partial class RendererService(
|
|||||||
|
|
||||||
private readonly List<IPostRenderProcessor> _postRenderProcessors = [];
|
private readonly List<IPostRenderProcessor> _postRenderProcessors = [];
|
||||||
|
|
||||||
public async Task RenderAsync(bool includeDrafts = false)
|
public async Task RenderAsync()
|
||||||
{
|
{
|
||||||
_stopwatch.Start();
|
_stopwatch.Start();
|
||||||
logger.LogInformation("Render essays start.");
|
logger.LogInformation("Render essays start.");
|
||||||
|
|
||||||
BlogContents contents = await essayScanService.ScanContents();
|
BlogContents contents = await essayScanService.ScanContents();
|
||||||
List<BlogContent> posts = contents.Posts.ToList();
|
List<BlogContent> posts = contents.Posts.ToList();
|
||||||
if (includeDrafts)
|
|
||||||
{
|
|
||||||
posts.AddRange(contents.Drafts);
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerable<BlogContent> preProcessedContents = await PreProcess(posts);
|
IEnumerable<BlogContent> preProcessedContents = await PreProcess(posts);
|
||||||
ConcurrentBag<BlogEssay> essays = [];
|
|
||||||
|
|
||||||
Parallel.ForEach(preProcessedContents, content =>
|
List<BlogEssay> essays = [];
|
||||||
|
await Task.Run(() =>
|
||||||
{
|
{
|
||||||
(uint wordCount, string readTime) = GetWordCount(content);
|
foreach (BlogContent content in preProcessedContents)
|
||||||
DateTimeOffset publishDate = content.Metadata.Date is null
|
{
|
||||||
? DateTimeOffset.Now
|
uint wordCount = GetWordCount(content);
|
||||||
: DateTimeOffset.Parse(content.Metadata.Date);
|
BlogEssay essay = new()
|
||||||
// 如果不存在最后的更新时间,就把更新时间设置为发布时间
|
{
|
||||||
DateTimeOffset updateTime = content.Metadata.UpdateTime is null
|
Title = content.Metadata.Title ?? content.FileName,
|
||||||
? publishDate
|
FileName = content.FileName,
|
||||||
: DateTimeOffset.Parse(content.Metadata.UpdateTime);
|
Description = GetDescription(content),
|
||||||
string description = GetDescription(content);
|
WordCount = wordCount,
|
||||||
List<string> tags = content.Metadata.Tags ?? [];
|
ReadTime = CalculateReadTime(wordCount),
|
||||||
|
PublishTime = content.Metadata.Date ?? DateTime.Now,
|
||||||
|
HtmlContent = content.FileContent
|
||||||
|
};
|
||||||
|
|
||||||
string originalHtml = Markdown.ToHtml(content.Content, markdownPipeline);
|
if (content.Metadata.Tags is not null)
|
||||||
|
{
|
||||||
BlogEssay essay = new(
|
essay.Tags.AddRange(content.Metadata.Tags);
|
||||||
content.Metadata.Title ?? content.BlogName, content.BlogName, content.IsDraft, publishDate, updateTime,
|
}
|
||||||
description, wordCount, readTime, tags, originalHtml);
|
|
||||||
logger.LogDebug("Render essay: {}", essay);
|
|
||||||
|
|
||||||
essays.Add(essay);
|
essays.Add(essay);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
IEnumerable<BlogEssay> postProcessedEssays = await PostProcess(essays);
|
ConcurrentBag<BlogEssay> postProcessEssays = [];
|
||||||
|
Parallel.ForEach(essays, essay =>
|
||||||
foreach (BlogEssay essay in postProcessedEssays)
|
|
||||||
{
|
{
|
||||||
if (!essayContentService.TryAdd(essay))
|
BlogEssay newEssay =
|
||||||
{
|
essay.WithNewHtmlContent(Markdown.ToHtml(essay.HtmlContent, markdownPipeline));
|
||||||
throw new BlogFileException($"There are at least two essays with filename '{essay.FileName}'.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
postProcessEssays.Add(newEssay);
|
||||||
|
logger.LogDebug("Render markdown file {}.", newEssay);
|
||||||
|
});
|
||||||
|
|
||||||
|
await PostProcess(postProcessEssays);
|
||||||
essayContentService.RefreshTags();
|
essayContentService.RefreshTags();
|
||||||
|
|
||||||
_stopwatch.Stop();
|
_stopwatch.Stop();
|
||||||
@@ -119,10 +118,8 @@ public sealed partial class RendererService(
|
|||||||
return processedContents;
|
return processedContents;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IEnumerable<BlogEssay>> PostProcess(IEnumerable<BlogEssay> essays)
|
private async Task PostProcess(IEnumerable<BlogEssay> essays)
|
||||||
{
|
{
|
||||||
ConcurrentBag<BlogEssay> processedContents = [];
|
|
||||||
|
|
||||||
await Parallel.ForEachAsync(essays, async (essay, _) =>
|
await Parallel.ForEachAsync(essays, async (essay, _) =>
|
||||||
{
|
{
|
||||||
foreach (IPostRenderProcessor processor in _postRenderProcessors)
|
foreach (IPostRenderProcessor processor in _postRenderProcessors)
|
||||||
@@ -130,34 +127,32 @@ public sealed partial class RendererService(
|
|||||||
essay = await processor.ProcessAsync(essay);
|
essay = await processor.ProcessAsync(essay);
|
||||||
}
|
}
|
||||||
|
|
||||||
processedContents.Add(essay);
|
if (!essayContentService.TryAdd(essay))
|
||||||
|
{
|
||||||
|
throw new BlogFileException(
|
||||||
|
$"There are two essays with the same name: '{essay.FileName}'.");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
List<BlogEssay> result = processedContents.ToList();
|
|
||||||
result.Sort();
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[GeneratedRegex(@"(?<!\\)[^\#\*_\-\+\`{}\[\]!~]+")]
|
[GeneratedRegex(@"(?<!\\)[^\#\*_\-\+\`{}\[\]!~]+")]
|
||||||
// private static partial Regex DescriptionPattern();
|
private static partial Regex DescriptionPattern();
|
||||||
private static partial Regex DescriptionPattern { get; }
|
|
||||||
|
|
||||||
private string GetDescription(BlogContent content)
|
private string GetDescription(BlogContent content)
|
||||||
{
|
{
|
||||||
const string delimiter = "<!--more-->";
|
const string delimiter = "<!--more-->";
|
||||||
int pos = content.Content.IndexOf(delimiter, StringComparison.Ordinal);
|
int pos = content.FileContent.IndexOf(delimiter, StringComparison.Ordinal);
|
||||||
bool breakSentence = false;
|
bool breakSentence = false;
|
||||||
|
|
||||||
if (pos == -1)
|
if (pos == -1)
|
||||||
{
|
{
|
||||||
// 自动截取前50个字符
|
// 自动截取前50个字符
|
||||||
pos = content.Content.Length < 50 ? content.Content.Length : 50;
|
pos = content.FileContent.Length < 50 ? content.FileContent.Length : 50;
|
||||||
breakSentence = true;
|
breakSentence = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
string rawContent = content.Content[..pos];
|
string rawContent = content.FileContent[..pos];
|
||||||
MatchCollection matches = DescriptionPattern.Matches(rawContent);
|
MatchCollection matches = DescriptionPattern().Matches(rawContent);
|
||||||
|
|
||||||
StringBuilder builder = new();
|
StringBuilder builder = new();
|
||||||
foreach (Match match in matches)
|
foreach (Match match in matches)
|
||||||
@@ -172,21 +167,28 @@ public sealed partial class RendererService(
|
|||||||
|
|
||||||
string description = builder.ToString();
|
string description = builder.ToString();
|
||||||
|
|
||||||
logger.LogDebug("Description of {name} is {desc}.", content.BlogName,
|
logger.LogDebug("Description of {} is {}.", content.FileName,
|
||||||
description);
|
description);
|
||||||
return description;
|
return description;
|
||||||
}
|
}
|
||||||
|
|
||||||
private (uint, string) GetWordCount(BlogContent content)
|
private uint GetWordCount(BlogContent content)
|
||||||
{
|
{
|
||||||
uint count = MarkdownWordCounter.CountWord(content);
|
int count = (from c in content.FileContent
|
||||||
|
where char.IsLetterOrDigit(c)
|
||||||
|
select c).Count();
|
||||||
|
|
||||||
logger.LogDebug("Word count of {blog} is {count}", content.BlogName,
|
logger.LogDebug("Word count of {} is {}", content.FileName,
|
||||||
count);
|
count);
|
||||||
// 据说语文教学大纲规定,中国高中生阅读现代文的速度是600字每分钟
|
return (uint)count;
|
||||||
uint second = count / 10;
|
}
|
||||||
TimeSpan span = new(0, 0, (int)second);
|
|
||||||
|
|
||||||
return (count, span.ToString("mm'分'ss'秒'"));
|
private static string CalculateReadTime(uint wordCount)
|
||||||
|
{
|
||||||
|
// 据说语文教学大纲规定,中国高中生阅读现代文的速度是600字每分钟
|
||||||
|
int second = (int)wordCount / 10;
|
||||||
|
TimeSpan span = new(0, 0, second);
|
||||||
|
|
||||||
|
return span.ToString("mm'分 'ss'秒'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
25
YaeBlog.Core/YaeBlog.Core.csproj
Normal file
25
YaeBlog.Core/YaeBlog.Core.csproj
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AngleSharp" Version="1.1.0" />
|
||||||
|
<PackageReference Include="Markdig" Version="0.34.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.6" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||||
|
<PackageReference Include="YamlDotNet" Version="13.7.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="wwwroot\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
47
YaeBlog.sln
Normal file
47
YaeBlog.sln
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.0.31903.59
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YaeBlog.Core", "YaeBlog.Core\YaeBlog.Core.csproj", "{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YaeBlog", "YaeBlog\YaeBlog.csproj", "{20438EFD-8DDE-43AF-92E2-76495C29233C}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".gitea", ".gitea", "{9B5AAA29-37D8-454A-8D8F-3E6B6BCF38E6}"
|
||||||
|
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
|
||||||
|
{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{1671A8AE-78F6-4641-B97D-D8ABA5E9CBEF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{20438EFD-8DDE-43AF-92E2-76495C29233C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{20438EFD-8DDE-43AF-92E2-76495C29233C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{20438EFD-8DDE-43AF-92E2-76495C29233C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{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
|
||||||
23
YaeBlog.slnx
23
YaeBlog.slnx
@@ -1,23 +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="build.ps1" />
|
|
||||||
<File Path="LICENSE" />
|
|
||||||
<File Path="README.md" />
|
|
||||||
</Folder>
|
|
||||||
<Folder Name="/src/">
|
|
||||||
<Project Path="src/YaeBlog.Tests/YaeBlog.Tests.csproj" />
|
|
||||||
<Project Path="src/YaeBlog/YaeBlog.csproj" />
|
|
||||||
</Folder>
|
|
||||||
<Folder Name="/third-party/" />
|
|
||||||
<Folder Name="/third-party/BlazorSvgComponents/" />
|
|
||||||
<Folder Name="/third-party/BlazorSvgComponents/src/">
|
|
||||||
<Project Path="third-party/BlazorSvgComponents/src/BlazorSvgComponents/BlazorSvgComponents.csproj" />
|
|
||||||
</Folder>
|
|
||||||
</Solution>
|
|
||||||
36
YaeBlog/Commands/Binders/BlogOptionsBinder.cs
Normal file
36
YaeBlog/Commands/Binders/BlogOptionsBinder.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using System.CommandLine.Binding;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using YaeBlog.Core.Models;
|
||||||
|
|
||||||
|
namespace YaeBlog.Commands.Binders;
|
||||||
|
|
||||||
|
public sealed class BlogOptionsBinder : BinderBase<IOptions<BlogOptions>>
|
||||||
|
{
|
||||||
|
protected override IOptions<BlogOptions> GetBoundValue(BindingContext bindingContext)
|
||||||
|
{
|
||||||
|
bindingContext.AddService<IOptions<BlogOptions>>(_ =>
|
||||||
|
{
|
||||||
|
FileInfo settings = new(Path.Combine(Environment.CurrentDirectory, "appsettings.json"));
|
||||||
|
if (!settings.Exists)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Failed to load YaeBlog configurations.");
|
||||||
|
}
|
||||||
|
|
||||||
|
using StreamReader reader = settings.OpenText();
|
||||||
|
using JsonDocument document = JsonDocument.Parse(reader.ReadToEnd());
|
||||||
|
JsonElement root = document.RootElement;
|
||||||
|
JsonElement optionSection = root.GetProperty(BlogOptions.OptionName);
|
||||||
|
|
||||||
|
BlogOptions? result = optionSection.Deserialize<BlogOptions>();
|
||||||
|
if (result is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Failed to load YaeBlog configuration in appsettings.json.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new OptionsWrapper<BlogOptions>(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
return bindingContext.GetRequiredService<IOptions<BlogOptions>>();
|
||||||
|
}
|
||||||
|
}
|
||||||
32
YaeBlog/Commands/Binders/EssayScanServiceBinder.cs
Normal file
32
YaeBlog/Commands/Binders/EssayScanServiceBinder.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using System.CommandLine.Binding;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using YaeBlog.Core.Abstractions;
|
||||||
|
using YaeBlog.Core.Models;
|
||||||
|
using YaeBlog.Core.Services;
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
using YamlDotNet.Serialization.NamingConventions;
|
||||||
|
|
||||||
|
namespace YaeBlog.Commands.Binders;
|
||||||
|
|
||||||
|
public sealed class EssayScanServiceBinder : BinderBase<IEssayScanService>
|
||||||
|
{
|
||||||
|
protected override IEssayScanService GetBoundValue(BindingContext bindingContext)
|
||||||
|
{
|
||||||
|
bindingContext.AddService<IEssayScanService>(provider =>
|
||||||
|
{
|
||||||
|
DeserializerBuilder deserializerBuilder = new();
|
||||||
|
deserializerBuilder.WithNamingConvention(CamelCaseNamingConvention.Instance);
|
||||||
|
deserializerBuilder.IgnoreUnmatchedProperties();
|
||||||
|
|
||||||
|
SerializerBuilder serializerBuilder = new();
|
||||||
|
serializerBuilder.WithNamingConvention(CamelCaseNamingConvention.Instance);
|
||||||
|
|
||||||
|
IOptions<BlogOptions> options = provider.GetRequiredService<IOptions<BlogOptions>>();
|
||||||
|
ILogger<EssayScanService> logger = provider.GetRequiredService<ILogger<EssayScanService>>();
|
||||||
|
|
||||||
|
return new EssayScanService(serializerBuilder.Build(), deserializerBuilder.Build(), options, logger);
|
||||||
|
});
|
||||||
|
|
||||||
|
return bindingContext.GetRequiredService<IEssayScanService>();
|
||||||
|
}
|
||||||
|
}
|
||||||
18
YaeBlog/Commands/Binders/LoggerBinder.cs
Normal file
18
YaeBlog/Commands/Binders/LoggerBinder.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System.CommandLine.Binding;
|
||||||
|
|
||||||
|
namespace YaeBlog.Commands.Binders;
|
||||||
|
|
||||||
|
public sealed class LoggerBinder<T> : BinderBase<ILogger<T>>
|
||||||
|
{
|
||||||
|
protected override ILogger<T> GetBoundValue(BindingContext bindingContext)
|
||||||
|
{
|
||||||
|
bindingContext.AddService(_ => LoggerFactory.Create(builder => builder.AddConsole()));
|
||||||
|
bindingContext.AddService<ILogger<T>>(provider =>
|
||||||
|
{
|
||||||
|
ILoggerFactory factory = provider.GetRequiredService<ILoggerFactory>();
|
||||||
|
return factory.CreateLogger<T>();
|
||||||
|
});
|
||||||
|
|
||||||
|
return bindingContext.GetRequiredService<ILogger<T>>();
|
||||||
|
}
|
||||||
|
}
|
||||||
216
YaeBlog/Commands/CommandExtensions.cs
Normal file
216
YaeBlog/Commands/CommandExtensions.cs
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
using System.CommandLine;
|
||||||
|
using YaeBlog.Commands.Binders;
|
||||||
|
using YaeBlog.Components;
|
||||||
|
using YaeBlog.Core.Extensions;
|
||||||
|
using YaeBlog.Core.Models;
|
||||||
|
using YaeBlog.Core.Services;
|
||||||
|
|
||||||
|
namespace YaeBlog.Commands;
|
||||||
|
|
||||||
|
public static class CommandExtensions
|
||||||
|
{
|
||||||
|
public static void AddServeCommand(this RootCommand rootCommand)
|
||||||
|
{
|
||||||
|
Command serveCommand = new("serve", "Start http server.");
|
||||||
|
rootCommand.AddCommand(serveCommand);
|
||||||
|
|
||||||
|
serveCommand.SetHandler(async context =>
|
||||||
|
{
|
||||||
|
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||||
|
|
||||||
|
builder.Services.AddRazorComponents()
|
||||||
|
.AddInteractiveServerComponents();
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
builder.Services.AddBlazorBootstrap();
|
||||||
|
builder.AddYaeBlog();
|
||||||
|
builder.AddServer();
|
||||||
|
|
||||||
|
WebApplication application = builder.Build();
|
||||||
|
|
||||||
|
application.UseStaticFiles();
|
||||||
|
application.UseAntiforgery();
|
||||||
|
application.UseYaeBlog();
|
||||||
|
|
||||||
|
application.MapRazorComponents<App>()
|
||||||
|
.AddInteractiveServerRenderMode();
|
||||||
|
application.MapControllers();
|
||||||
|
|
||||||
|
CancellationToken token = context.GetCancellationToken();
|
||||||
|
await application.RunAsync(token);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void AddWatchCommand(this RootCommand rootCommand)
|
||||||
|
{
|
||||||
|
Command command = new("watch", "Start a blog watcher that re-render when file changes.");
|
||||||
|
rootCommand.AddCommand(command);
|
||||||
|
|
||||||
|
command.SetHandler(async context =>
|
||||||
|
{
|
||||||
|
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||||
|
|
||||||
|
builder.Services.AddRazorComponents()
|
||||||
|
.AddInteractiveServerComponents();
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
builder.Services.AddBlazorBootstrap();
|
||||||
|
builder.AddYaeBlog();
|
||||||
|
builder.AddWatcher();
|
||||||
|
|
||||||
|
WebApplication application = builder.Build();
|
||||||
|
|
||||||
|
application.UseStaticFiles();
|
||||||
|
application.UseAntiforgery();
|
||||||
|
application.UseYaeBlog();
|
||||||
|
|
||||||
|
application.MapRazorComponents<App>()
|
||||||
|
.AddInteractiveServerRenderMode();
|
||||||
|
application.MapControllers();
|
||||||
|
|
||||||
|
CancellationToken token = context.GetCancellationToken();
|
||||||
|
await application.RunAsync(token);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void AddNewCommand(this RootCommand rootCommand)
|
||||||
|
{
|
||||||
|
Command newCommand = new("new", "Create a new blog file and image directory.");
|
||||||
|
rootCommand.AddCommand(newCommand);
|
||||||
|
|
||||||
|
Argument<string> filenameArgument = new(name: "blog name", description: "The created blog filename.");
|
||||||
|
newCommand.AddArgument(filenameArgument);
|
||||||
|
|
||||||
|
newCommand.SetHandler(async (file, _, _, essayScanService) =>
|
||||||
|
{
|
||||||
|
BlogContents contents = await essayScanService.ScanContents();
|
||||||
|
|
||||||
|
if (contents.Posts.Any(content => content.FileName == file))
|
||||||
|
{
|
||||||
|
Console.WriteLine("There exists the same title blog in posts.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await essayScanService.SaveBlogContent(new BlogContent
|
||||||
|
{
|
||||||
|
FileName = file,
|
||||||
|
FileContent = string.Empty,
|
||||||
|
Metadata = new MarkdownMetadata { Title = file, Date = DateTime.Now }
|
||||||
|
});
|
||||||
|
|
||||||
|
Console.WriteLine($"Created new blog '{file}.");
|
||||||
|
}, filenameArgument, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(),
|
||||||
|
new EssayScanServiceBinder());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void AddListCommand(this RootCommand rootCommand)
|
||||||
|
{
|
||||||
|
Command command = new("list", "List all blogs");
|
||||||
|
rootCommand.AddCommand(command);
|
||||||
|
|
||||||
|
command.SetHandler(async (_, _, essyScanService) =>
|
||||||
|
{
|
||||||
|
BlogContents contents = await essyScanService.ScanContents();
|
||||||
|
|
||||||
|
Console.WriteLine($"All {contents.Posts.Count} Posts:");
|
||||||
|
foreach (BlogContent content in contents.Posts.OrderBy(x => x.FileName))
|
||||||
|
{
|
||||||
|
Console.WriteLine($" - {content.FileName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"All {contents.Drafts.Count} Drafts:");
|
||||||
|
foreach (BlogContent content in contents.Drafts.OrderBy(x => x.FileName))
|
||||||
|
{
|
||||||
|
Console.WriteLine($" - {content.FileName}");
|
||||||
|
}
|
||||||
|
}, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void AddScanCommand(this RootCommand rootCommand)
|
||||||
|
{
|
||||||
|
Command command = new("scan", "Scan unused and not found images.");
|
||||||
|
rootCommand.AddCommand(command);
|
||||||
|
|
||||||
|
Option<bool> removeOption =
|
||||||
|
new(name: "--rm", description: "Remove unused images.", getDefaultValue: () => false);
|
||||||
|
command.AddOption(removeOption);
|
||||||
|
|
||||||
|
command.SetHandler(async (_, _, essayScanService, removeOptionValue) =>
|
||||||
|
{
|
||||||
|
ImageScanResult result = await essayScanService.ScanImages();
|
||||||
|
|
||||||
|
if (result.UnusedImages.Count != 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Found unused images:");
|
||||||
|
Console.WriteLine("HINT: use '--rm' to remove unused images.");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (FileInfo image in result.UnusedImages)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" - {image.FullName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removeOptionValue)
|
||||||
|
{
|
||||||
|
foreach (FileInfo image in result.UnusedImages)
|
||||||
|
{
|
||||||
|
image.Delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("Used not existed images:");
|
||||||
|
|
||||||
|
foreach (FileInfo image in result.NotFoundImages)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" - {image.FullName}");
|
||||||
|
}
|
||||||
|
}, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder(), removeOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void AddPublishCommand(this RootCommand rootCommand)
|
||||||
|
{
|
||||||
|
Command command = new("publish", "Publish a new blog file.");
|
||||||
|
rootCommand.AddCommand(command);
|
||||||
|
|
||||||
|
Argument<string> filenameArgument = new(name: "blog name", description: "The published blog filename.");
|
||||||
|
command.AddArgument(filenameArgument);
|
||||||
|
|
||||||
|
command.SetHandler(async (blogOptions, _, essayScanService, filename) =>
|
||||||
|
{
|
||||||
|
BlogContents contents = await essayScanService.ScanContents();
|
||||||
|
|
||||||
|
BlogContent? content = (from blog in contents.Drafts
|
||||||
|
where blog.FileName == filename
|
||||||
|
select blog).FirstOrDefault();
|
||||||
|
|
||||||
|
if (content is null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Target blog does not exist.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将选中的博客文件复制到posts
|
||||||
|
await essayScanService.SaveBlogContent(content, isDraft: false);
|
||||||
|
|
||||||
|
// 复制图片文件夹
|
||||||
|
DirectoryInfo sourceImageDirectory =
|
||||||
|
new(Path.Combine(blogOptions.Value.Root, "drafts", content.FileName));
|
||||||
|
DirectoryInfo targetImageDirectory =
|
||||||
|
new(Path.Combine(blogOptions.Value.Root, "posts", content.FileName));
|
||||||
|
|
||||||
|
if (sourceImageDirectory.Exists)
|
||||||
|
{
|
||||||
|
targetImageDirectory.Create();
|
||||||
|
foreach (FileInfo file in sourceImageDirectory.EnumerateFiles())
|
||||||
|
{
|
||||||
|
file.CopyTo(Path.Combine(targetImageDirectory.FullName, file.Name), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceImageDirectory.Delete(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除原始的文件
|
||||||
|
FileInfo sourceBlogFile = new(Path.Combine(blogOptions.Value.Root, "drafts", content.FileName + ".md"));
|
||||||
|
sourceBlogFile.Delete();
|
||||||
|
}, new BlogOptionsBinder(),
|
||||||
|
new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder(), filenameArgument);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
YaeBlog/Components/App.razor
Normal file
28
YaeBlog/Components/App.razor
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<base href="/"/>
|
||||||
|
<link rel="stylesheet" href="YaeBlog.styles.css"/>
|
||||||
|
<link rel="icon" href="images/favicon.ico"/>
|
||||||
|
<link rel="stylesheet" href="bootstrap.min.css"/>
|
||||||
|
<link rel="stylesheet" href="bootstrap-icons.min.css"/>
|
||||||
|
<link rel="stylesheet" href="_content/Blazor.Bootstrap/blazor.bootstrap.css"/>
|
||||||
|
<link rel="stylesheet" href="globals.css"/>
|
||||||
|
<HeadOutlet/>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<Routes/>
|
||||||
|
|
||||||
|
<script src="_framework/blazor.web.js"></script>
|
||||||
|
<script src="bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="clipboard.min.js"></script>
|
||||||
|
<script>
|
||||||
|
const clipboard = new ClipboardJS('.btn');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
57
YaeBlog/Components/BlogInformationCard.razor
Normal file
57
YaeBlog/Components/BlogInformationCard.razor
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
@using YaeBlog.Core.Abstractions
|
||||||
|
@using YaeBlog.Core.Models
|
||||||
|
|
||||||
|
@inject IEssayContentService Contents
|
||||||
|
@inject BlogOptions Options
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-auto p-4">
|
||||||
|
<Image Src="images/avatar.png" Alt="Ricardo's avatar"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-content-center p-3">
|
||||||
|
<div class="col-auto fs-4">
|
||||||
|
“奇奇怪怪东西的聚合地”
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-content-between px-2 py-1 fs-5">
|
||||||
|
<div class="col-auto">
|
||||||
|
文章
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-auto">
|
||||||
|
<a href="/blog/archives">
|
||||||
|
@(Contents.Essays.Count)
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-content-between px-2 py-1 fs-5">
|
||||||
|
<div class="col-auto">
|
||||||
|
标签
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-auto">
|
||||||
|
<a href="/blog/tags">
|
||||||
|
@(Contents.Tags.Count)
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-content-start fs-5" style="padding-top: 2em">
|
||||||
|
<div class="col-auto">
|
||||||
|
广而告之
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<p style="text-indent: 2em">
|
||||||
|
@(Options.Announcement)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
40
YaeBlog/Components/EssayCard.razor
Normal file
40
YaeBlog/Components/EssayCard.razor
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
@using System.Text.Encodings.Web
|
||||||
|
@using YaeBlog.Core.Models
|
||||||
|
|
||||||
|
<div class="container p-3">
|
||||||
|
<div class="row fs-2 fw-bold py-2 essay-title">
|
||||||
|
<a href="/blog/essays/@(Essay.FileName)" target="_blank">@(Essay.Title)</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row p-2 justify-content-start">
|
||||||
|
<div class="col-auto fw-light">
|
||||||
|
@(Essay.PublishTime.ToString("yyyy-MM-dd"))
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@foreach (string key in Essay.Tags)
|
||||||
|
{
|
||||||
|
<div class="col-auto">
|
||||||
|
<a href="/blog/tags/?tagName=@(UrlEncoder.Default.Encode(key))">
|
||||||
|
# @key
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row p-2">
|
||||||
|
<div class="col">
|
||||||
|
@(Essay.Description)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col border-bottom">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public required BlogEssay Essay { get; set; }
|
||||||
|
}
|
||||||
3
YaeBlog/Components/EssayCard.razor.css
Normal file
3
YaeBlog/Components/EssayCard.razor.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.essay-title a {
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
}
|
||||||
14
YaeBlog/Components/Foonter.razor
Normal file
14
YaeBlog/Components/Foonter.razor
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<div class="row align-items-end text-center">
|
||||||
|
<div class="row">
|
||||||
|
<p class="fs-6">
|
||||||
|
2021 - @(DateTimeOffset.Now.Year) © <a href="https://rrricardo.top" target="_blank">Ricardo Ren</a>,
|
||||||
|
由 <a href="https://dotnet.microsoft.com/zh-cn/" target="_blank">.NET @(Environment.Version)</a> 驱动。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<p class="fs-6">
|
||||||
|
<a href="https://beian.miit.gov.cn" target="_blank">蜀ICP备2022004429号-1</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
36
YaeBlog/Components/LicenseDisclaimer.razor
Normal file
36
YaeBlog/Components/LicenseDisclaimer.razor
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
@using YaeBlog.Core.Models
|
||||||
|
|
||||||
|
@inject BlogOptions Options
|
||||||
|
|
||||||
|
<div class="row px-2 py-4 copyright border border-primary rounded-1 bg-primary-subtle">
|
||||||
|
<div class="col">
|
||||||
|
<div class="row p-1">
|
||||||
|
<div class="col">
|
||||||
|
文章作者:<a href="https://rrricardo.top" target="_blank">Ricardo Ren</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row p-1">
|
||||||
|
<div class="col">
|
||||||
|
文章地址:
|
||||||
|
<a href="/blog/essays/@(EssayAddress)" target="_blank">
|
||||||
|
@($"https://rrricardo.top/blog/essays/{EssayAddress}")
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row p-1">
|
||||||
|
<div class="col">
|
||||||
|
版权声明:本博客所有文章除特别声明外,均采用
|
||||||
|
<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank">CC BY-NC-SA 4.0</a>
|
||||||
|
许可协议,转载请注明来自
|
||||||
|
<a href="https://rrricardo.top/blog/" target="_blank">Ricardo's Blog</a>。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter] public string? EssayAddress { get; set; }
|
||||||
|
}
|
||||||
2
YaeBlog/Components/LicenseDisclaimer.razor.css
Normal file
2
YaeBlog/Components/LicenseDisclaimer.razor.css
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.copyright {
|
||||||
|
}
|
||||||
8
YaeBlog/Dockerfile
Normal file
8
YaeBlog/Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY bin/Release/net8.0/publish/ ./
|
||||||
|
COPY source/ ./source/
|
||||||
|
COPY appsettings.json .
|
||||||
|
|
||||||
|
ENTRYPOINT ["dotnet", "YaeBlog.dll", "serve"]
|
||||||
63
YaeBlog/Layout/BlogLayout.razor
Normal file
63
YaeBlog/Layout/BlogLayout.razor
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
|
@attribute [StreamRendering]
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<div class="row d-none d-xl-flex" style="height: 80px">
|
||||||
|
<div class="px-2 col-9">
|
||||||
|
<a href="/blog/" class="p-2">
|
||||||
|
<h4>Ricardo's Blog</h4>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-3 d-flex justify-content-around align-items-center">
|
||||||
|
<a href="/blog/" class="p-2">
|
||||||
|
<h5>首页</h5>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/blog/archives/" class="p-2">
|
||||||
|
<h5>归档</h5>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/blog/tags/" class="p-2">
|
||||||
|
<h5>标签</h5>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/blog/about/" class="p-2">
|
||||||
|
<h5>关于</h5>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row d-xl-none">
|
||||||
|
<div class="px-2 col-12">
|
||||||
|
<a href="/blog/" class="p-2">
|
||||||
|
<h4>Ricardo's Blog</h4>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-2 col-12 justify-content-end d-flex">
|
||||||
|
<a href="/blog/" class="p-2">
|
||||||
|
<h5>首页</h5>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/blog/archives/" class="p-2">
|
||||||
|
<h5>归档</h5>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/blog/tags/" class="p-2">
|
||||||
|
<h5>标签</h5>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/blog/about/" class="p-2">
|
||||||
|
<h5>关于</h5>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row px-4 py-2">
|
||||||
|
@Body
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Foonter/>
|
||||||
|
</main>
|
||||||
0
YaeBlog/Layout/BlogLayout.razor.css
Normal file
0
YaeBlog/Layout/BlogLayout.razor.css
Normal file
25
YaeBlog/Layout/MainLayout.razor
Normal file
25
YaeBlog/Layout/MainLayout.razor
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<div class="row" style="height: 80px">
|
||||||
|
<div class="px-2 col-8">
|
||||||
|
<a href="/" class="p-2">
|
||||||
|
<h4>Ricardo's Index</h4>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-4 d-flex justify-content-around align-items-center">
|
||||||
|
<a href="mailto://shicangjuner@outlook.com" class="p-2" target="_blank">
|
||||||
|
<h5>E-mail</h5>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row px-4 center">
|
||||||
|
<div class="py-2">
|
||||||
|
@Body
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Foonter/>
|
||||||
|
</main>
|
||||||
8
YaeBlog/Layout/MainLayout.razor.css
Normal file
8
YaeBlog/Layout/MainLayout.razor.css
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.center {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 48em;
|
||||||
|
min-height: calc(100vh - 80px);
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
146
YaeBlog/Pages/About.razor
Normal file
146
YaeBlog/Pages/About.razor
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
@page "/blog/about"
|
||||||
|
@using YaeBlog.Core.Models
|
||||||
|
|
||||||
|
@inject BlogOptions Options
|
||||||
|
|
||||||
|
<PageTitle>
|
||||||
|
关于
|
||||||
|
</PageTitle>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<h1>关于</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col fst-italic py-2">
|
||||||
|
把字刻在石头上!(・’ω’・)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row p-2">
|
||||||
|
<div class="col">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<h3>关于我</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row py-2">
|
||||||
|
<div class="col">
|
||||||
|
计算机科学与技术在读大学生,明光村幼儿园附属大学所属。正处于读书和失业的叠加态。
|
||||||
|
一般在互联网上使用<span class="fst-italic">初冬的朝阳</span>或者<span class="fst-italic">jackfiled</span>的名字活动。
|
||||||
|
<span class="text-decoration-line-through">都是ICP备案过的人了,网名似乎没有太大的用处(</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row py-2">
|
||||||
|
<div class="col">
|
||||||
|
主要是一个C#程序员,目前也在尝试写一点Rust。
|
||||||
|
总体上对于编程语言的态度是“<span>大家都是我的翅膀.jpg</span>”。
|
||||||
|
前后端分离的项目本当上手。
|
||||||
|
常常因为现实的压力而写一些C/C++。
|
||||||
|
<span class="text-decoration-line-through">对于Java和Go的评价很低。</span>
|
||||||
|
日常使用ArchLinux。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row py-2">
|
||||||
|
<div class="col">
|
||||||
|
100%社恐。日常生活是宅在电脑前面自言自语。兴趣活动是读书和看番。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row py-2">
|
||||||
|
<div class="col">
|
||||||
|
常常被人批评没有梦想,这里就随便瞎编一下。
|
||||||
|
成为嵌入式工程师,修好桌面上的<a href="https://www.bilibili.com/video/BV1VA411p7MD">HoloCubic</a>。
|
||||||
|
完成第一个不是课程设计的个人开源项目。
|
||||||
|
遇到能够搭伙过日子的人也算是一大梦想,虽然社恐人根本不知道从何开始的说,
|
||||||
|
<span class="text-decoration-line-through">什么时候天上才能掉美少女?</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row py-2">
|
||||||
|
<div class="col">
|
||||||
|
公开的联系渠道是<a href="mailto:shicangjuner@outlook.com">电子邮件</a>。
|
||||||
|
也可以试试在各大平台搜索上面提到的名字。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row p-2">
|
||||||
|
<div class="col">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<h3>关于本站</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row py-2">
|
||||||
|
<div class="col">
|
||||||
|
本站肇始于2021年下半年,在开始的两年中个人网站和博客是分别的两个网站,个人网站是裸HTML写的,博客是用
|
||||||
|
<a href="https://hexo.io">Hexo</a>渲染的。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row py-2">
|
||||||
|
<div class="col">
|
||||||
|
2024年,我们决定使用.NET技术完全重构两个网站,合二为一。虽然目前这个版本还是一个半成品,但是我们一定会努力的~(确信。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row p-2">
|
||||||
|
<div class="col">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<h3>友链</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row py-2">
|
||||||
|
<div class="col fst-italic">
|
||||||
|
欢迎所有人联系我添加友链!(´。✪ω✪。`)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row py-2">
|
||||||
|
@foreach (FriendLink link in Options.Links)
|
||||||
|
{
|
||||||
|
<div class="col-sm-12 col-md-4 col-lg-3">
|
||||||
|
<a href="@(link.Link)" target="_blank" class="m-3">
|
||||||
|
<div class="row link-item">
|
||||||
|
<div class="col-4">
|
||||||
|
<Image Src="@(link.AvatarImage)" Alt="@(link.Name)" Style="border-radius: 50%"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-8">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-auto fs-5">
|
||||||
|
@(link.Name)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-auto fst-italic">
|
||||||
|
@(link.Description)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
|
||||||
|
}
|
||||||
8
YaeBlog/Pages/About.razor.css
Normal file
8
YaeBlog/Pages/About.razor.css
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.link-item {
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-item:hover {
|
||||||
|
background-color: var(--bs-secondary-bg);
|
||||||
|
}
|
||||||
75
YaeBlog/Pages/Archives.razor
Normal file
75
YaeBlog/Pages/Archives.razor
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
@page "/blog/archives"
|
||||||
|
@using YaeBlog.Core.Abstractions
|
||||||
|
@using YaeBlog.Core.Models
|
||||||
|
|
||||||
|
@inject IEssayContentService Contents
|
||||||
|
|
||||||
|
<PageTitle>
|
||||||
|
归档
|
||||||
|
</PageTitle>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<h1>归档</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col fst-italic py-4">
|
||||||
|
时光图书馆,黑历史集散地。(๑◔‿◔๑)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@foreach (IGrouping<DateTime, KeyValuePair<string, BlogEssay>> group in _essays)
|
||||||
|
{
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<h3>@(group.Key.Year)</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container px-3 py-2">
|
||||||
|
@foreach (KeyValuePair<string, BlogEssay> essay in group)
|
||||||
|
{
|
||||||
|
<div class="row py-1">
|
||||||
|
<div class="col-auto">
|
||||||
|
@(essay.Value.PublishTime.ToString("MM-dd"))
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-auto">
|
||||||
|
<a href="/blog/essays/@(essay.Key)">
|
||||||
|
@(essay.Value.Title)
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private readonly List<IGrouping<DateTime, KeyValuePair<string, BlogEssay>>> _essays = [];
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
base.OnInitialized();
|
||||||
|
|
||||||
|
_essays.AddRange(from essay in Contents.Essays
|
||||||
|
orderby essay.Value.PublishTime descending
|
||||||
|
group essay by new DateTime(essay.Value.PublishTime.Year, 1, 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
0
YaeBlog/Pages/Archives.razor.css
Normal file
0
YaeBlog/Pages/Archives.razor.css
Normal file
116
YaeBlog/Pages/BlogIndex.razor
Normal file
116
YaeBlog/Pages/BlogIndex.razor
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
@page "/blog"
|
||||||
|
@using YaeBlog.Core.Abstractions
|
||||||
|
@using YaeBlog.Core.Models
|
||||||
|
|
||||||
|
@inject IEssayContentService Contents
|
||||||
|
@inject NavigationManager NavigationInstance
|
||||||
|
|
||||||
|
<PageTitle>
|
||||||
|
Ricardo's Blog
|
||||||
|
</PageTitle>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12 col-md-9">
|
||||||
|
@foreach (KeyValuePair<string, BlogEssay> pair in _essays)
|
||||||
|
{
|
||||||
|
<EssayCard Essay="@(pair.Value)"/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="row align-items-center justify-content-center p-3">
|
||||||
|
@if (_page == 1)
|
||||||
|
{
|
||||||
|
<div class="col-auto fw-light">上一页</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="col-auto">
|
||||||
|
<a href="/blog/?page=@(_page - 1)">上一页</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (_page == 1)
|
||||||
|
{
|
||||||
|
<div class="col-auto">
|
||||||
|
1
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<a href="/blog/?page=2">2</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<a href="/blog/?page=3">3</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (_page == _pageCount)
|
||||||
|
{
|
||||||
|
<div class="col-auto">
|
||||||
|
<a href="/blog/?page=@(_pageCount - 2)">@(_pageCount - 2)</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<a href="/blog/?page=@(_pageCount - 1)">@(_pageCount - 1)</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
@(_pageCount)
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="col-auto">
|
||||||
|
<a href="/blog/?page=@(_page - 1)">@(_page - 1)</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
@(_page)
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<a href="/blog/?page=@(_page + 1)">@(_page + 1)</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (_page == _pageCount)
|
||||||
|
{
|
||||||
|
<div class="col-auto fw-light">
|
||||||
|
下一页
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="col-auto">
|
||||||
|
<a href="/blog/?page=@(_page + 1)">下一页</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-12 col-md-3">
|
||||||
|
<BlogInformationCard/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
|
||||||
|
[SupplyParameterFromQuery] private int? Page { get; set; }
|
||||||
|
|
||||||
|
private readonly List<KeyValuePair<string, BlogEssay>> _essays = [];
|
||||||
|
private const int EssaysPerPage = 8;
|
||||||
|
private int _pageCount = 1;
|
||||||
|
private int _page = 1;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
_page = Page ?? 1;
|
||||||
|
_pageCount = Contents.Essays.Count / EssaysPerPage + 1;
|
||||||
|
|
||||||
|
if (EssaysPerPage * _page > Contents.Essays.Count + EssaysPerPage)
|
||||||
|
{
|
||||||
|
NavigationInstance.NavigateTo("/NotFount");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_essays.AddRange(Contents.Essays
|
||||||
|
.OrderByDescending(p => p.Value.PublishTime)
|
||||||
|
.Skip((_page - 1) * EssaysPerPage)
|
||||||
|
.Take(EssaysPerPage));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
7
YaeBlog/Pages/BlogIndex.razor.css
Normal file
7
YaeBlog/Pages/BlogIndex.razor.css
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.essay-title a {
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-more a {
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
}
|
||||||
137
YaeBlog/Pages/Essays.razor
Normal file
137
YaeBlog/Pages/Essays.razor
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
@page "/blog/essays/{BlogKey}"
|
||||||
|
@using System.Text.Encodings.Web
|
||||||
|
@using YaeBlog.Core.Abstractions
|
||||||
|
@using YaeBlog.Core.Models
|
||||||
|
|
||||||
|
@inject IEssayContentService Contents
|
||||||
|
@inject NavigationManager NavigationInstance
|
||||||
|
|
||||||
|
<PageTitle>
|
||||||
|
@(_essay!.Title)
|
||||||
|
</PageTitle>
|
||||||
|
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-auto">
|
||||||
|
<h1 id="title">@(_essay!.Title)</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row px-4 py-1">
|
||||||
|
<div class="col-auto fw-light">
|
||||||
|
@(_essay!.PublishTime.ToString("yyyy-MM-dd"))
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@foreach (string tag in _essay!.Tags)
|
||||||
|
{
|
||||||
|
<div class="col-auto">
|
||||||
|
<a href="/blog/tags/?tagName=@(UrlEncoder.Default.Encode(tag))">
|
||||||
|
# @(tag)
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row px-4 py-1">
|
||||||
|
<div class="col-auto fw-light">
|
||||||
|
总字数:@(_essay!.WordCount)字,预计阅读时间 @(_essay!.ReadTime)。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8 col-md-12">
|
||||||
|
@((MarkupString)_essay!.HtmlContent)
|
||||||
|
|
||||||
|
<LicenseDisclaimer EssayAddress="@BlogKey"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4 col-md-12">
|
||||||
|
<div class="row sticky-lg-top justify-content-center">
|
||||||
|
<div class="col-auto">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-auto">
|
||||||
|
<h3 style="margin-block-start: 1em; margin-block-end: 0.5em">
|
||||||
|
文章目录
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" style="padding-left: 10px">
|
||||||
|
<div class="col-auto">
|
||||||
|
@foreach (BlogHeadline level2 in _headline!.Children)
|
||||||
|
{
|
||||||
|
<div class="row py-1">
|
||||||
|
<div class="col-auto">
|
||||||
|
<a href="@(GenerateSelectorUrl(level2.SelectorId))">@(level2.Title)</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@foreach (BlogHeadline level3 in level2.Children)
|
||||||
|
{
|
||||||
|
<div class="row py-1">
|
||||||
|
<div class="col-auto">
|
||||||
|
<a style="padding-left: 20px" href="@GenerateSelectorUrl(level3.SelectorId)">
|
||||||
|
@(level3.Title)
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@foreach (BlogHeadline level4 in level3.Children)
|
||||||
|
{
|
||||||
|
<div class="row py-1">
|
||||||
|
<div class="col-auto">
|
||||||
|
<a style="padding-left: 40px" href="@(GenerateSelectorUrl(level4.SelectorId))">
|
||||||
|
@(level4.Title)
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (_headline!.Children.Count == 0)
|
||||||
|
{
|
||||||
|
<div class="row">
|
||||||
|
<div class="col fst-italic">
|
||||||
|
坏了(* Ŏ∀Ŏ),没有在文章中识别到目录
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public string? BlogKey { get; set; }
|
||||||
|
|
||||||
|
private BlogEssay? _essay;
|
||||||
|
|
||||||
|
private BlogHeadline? _headline;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
base.OnInitialized();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(BlogKey))
|
||||||
|
{
|
||||||
|
NavigationInstance.NavigateTo("/NotFound");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Contents.Essays.TryGetValue(BlogKey, out _essay))
|
||||||
|
{
|
||||||
|
NavigationInstance.NavigateTo("/NotFound");
|
||||||
|
}
|
||||||
|
|
||||||
|
_headline = Contents.Headlines[BlogKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GenerateSelectorUrl(string selectorId)
|
||||||
|
=> $"/blog/essays/{BlogKey!}#{selectorId}";
|
||||||
|
|
||||||
|
}
|
||||||
0
YaeBlog/Pages/Essays.razor.css
Normal file
0
YaeBlog/Pages/Essays.razor.css
Normal file
57
YaeBlog/Pages/Index.razor
Normal file
57
YaeBlog/Pages/Index.razor
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
@page "/"
|
||||||
|
|
||||||
|
<PageTitle>
|
||||||
|
Ricardo's Index
|
||||||
|
</PageTitle>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row py-4">
|
||||||
|
<div class="col-lg-4 col-12 p-5 p-lg-0">
|
||||||
|
<Image Src="images/avatar.png" Alt="Ricardo's Avatar"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-8 col-12">
|
||||||
|
<div class="container px-3">
|
||||||
|
<div class="row">
|
||||||
|
<h4 class="fw-bold">初冬的朝阳 (Ricardo Ren)</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<p class="fs-5">a.k.a jackfiled</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<p class="fs-5 fst-italic">世界很大,时间很长。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<p class="fs-5">
|
||||||
|
平平无奇的计算机科学与技术学徒,连微小的贡献都没做。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" style="padding-top: 80px">
|
||||||
|
<p class="fs-5">恕我不能亲自为您沏茶(?),还是非常欢迎您能够来到我的主页。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<p class="fs-5">
|
||||||
|
如果您想四处看看,了解一下屏幕对面的人,可以在我的 <a href="/blog/">博客</a> 看看。
|
||||||
|
如果您对于明光村幼儿园某附属技校的计算机教学感兴趣,您可以移步到
|
||||||
|
<a href="https://jackfiled.github.io/wiki/">我的学习笔记</a>,
|
||||||
|
<span class="fs-5 text-decoration-line-through">虽然这笔记我自己也木有看过。</span>
|
||||||
|
如果您想批判一下我的代码,在 <a href="https://github.com/jackfiled" target="_blank">Github</a> 和
|
||||||
|
<a href="https://git.rrricardo.top/jackfiled/" target="_blank">Gitea</a> 都可以找到。
|
||||||
|
</p>
|
||||||
|
<p class="fs-5">
|
||||||
|
如果您真的很闲,也可以四处搜寻一下,也许存在着一些不为人知的彩蛋。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
|
||||||
|
}
|
||||||
0
YaeBlog/Pages/Index.razor.css
Normal file
0
YaeBlog/Pages/Index.razor.css
Normal file
@@ -4,8 +4,8 @@
|
|||||||
啊~ 页面走丢啦~
|
啊~ 页面走丢啦~
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
|
|
||||||
<div>
|
<div class="container">
|
||||||
<h3 class="text-3xl">NotFound!</h3>
|
<h3>NotFound!</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
0
YaeBlog/Pages/NotFound.razor.css
Normal file
0
YaeBlog/Pages/NotFound.razor.css
Normal file
@@ -1,7 +1,7 @@
|
|||||||
@page "/blog/tags/"
|
@page "/blog/tags/"
|
||||||
@using System.Text.Encodings.Web
|
@using System.Text.Encodings.Web
|
||||||
@using YaeBlog.Abstraction
|
@using YaeBlog.Core.Abstractions
|
||||||
@using YaeBlog.Models
|
@using YaeBlog.Core.Models
|
||||||
|
|
||||||
@inject IEssayContentService Contents
|
@inject IEssayContentService Contents
|
||||||
@inject NavigationManager NavigationInstance
|
@inject NavigationManager NavigationInstance
|
||||||
@@ -10,22 +10,24 @@
|
|||||||
@(TagName ?? "标签")
|
@(TagName ?? "标签")
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="container">
|
||||||
<div>
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
@if (TagName is null)
|
@if (TagName is null)
|
||||||
{
|
{
|
||||||
<h1 class="text-4xl">标签</h1>
|
<h1>标签</h1>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<h2 class="text-2xl">@(TagName)</h2>
|
<h2>@(TagName)</h2>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="py-4">
|
<div class="row">
|
||||||
<span class="italic">
|
<div class="col fst-italic py-4">
|
||||||
在野外游荡的指针,走向未知的方向。٩(๑˃̵ᴗ˂̵๑)۶
|
在野外游荡的指针,走向未知的方向。٩(๑˃̵ᴗ˂̵๑)۶
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (TagName is null)
|
@if (TagName is null)
|
||||||
@@ -36,17 +38,19 @@
|
|||||||
Contents.Tags.OrderByDescending(pair => pair.Value.Count))
|
Contents.Tags.OrderByDescending(pair => pair.Value.Count))
|
||||||
{
|
{
|
||||||
<li class="p-2">
|
<li class="p-2">
|
||||||
<div class="flex flex-row">
|
|
||||||
<a href="/blog/tags/?tagName=@(pair.Key.UrlEncodedTagName)">
|
<a href="/blog/tags/?tagName=@(pair.Key.UrlEncodedTagName)">
|
||||||
<div class="text-sky-600 text-lg">
|
<div class="container fs-5">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-auto">
|
||||||
# @(pair.Key.TagName)
|
# @(pair.Key.TagName)
|
||||||
</div>
|
</div>
|
||||||
</a>
|
|
||||||
|
|
||||||
<div class="mx-2 px-1 text-lg bg-gray-300 rounded-lg">
|
<div class="col-auto tag-count">
|
||||||
@(pair.Value.Count)
|
@(pair.Value.Count)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
6
YaeBlog/Pages/Tags.razor.css
Normal file
6
YaeBlog/Pages/Tags.razor.css
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.tag-count {
|
||||||
|
background: var(--bs-secondary-bg);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
13
YaeBlog/Program.cs
Normal file
13
YaeBlog/Program.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System.CommandLine;
|
||||||
|
using YaeBlog.Commands;
|
||||||
|
|
||||||
|
RootCommand rootCommand = new("YaeBlog CLI");
|
||||||
|
|
||||||
|
rootCommand.AddServeCommand();
|
||||||
|
rootCommand.AddNewCommand();
|
||||||
|
rootCommand.AddListCommand();
|
||||||
|
rootCommand.AddWatchCommand();
|
||||||
|
rootCommand.AddScanCommand();
|
||||||
|
rootCommand.AddPublishCommand();
|
||||||
|
|
||||||
|
await rootCommand.InvokeAsync(args);
|
||||||
18
YaeBlog/YaeBlog.csproj
Normal file
18
YaeBlog/YaeBlog.csproj
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\YaeBlog.Core\YaeBlog.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Blazor.Bootstrap" Version="3.0.0-preview.2" />
|
||||||
|
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -6,6 +6,6 @@
|
|||||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
|
@using BlazorBootstrap
|
||||||
|
@using YaeBlog
|
||||||
@using YaeBlog.Components
|
@using YaeBlog.Components
|
||||||
@using BlazorSvgComponents
|
|
||||||
@using BlazorSvgComponents.Models
|
|
||||||
40
YaeBlog/appsettings.json
Normal file
40
YaeBlog/appsettings.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*",
|
||||||
|
"Blog": {
|
||||||
|
"Root": "source",
|
||||||
|
"Announcement": "博客锐意装修中,敬请期待!测试阶段如有问题还请海涵。",
|
||||||
|
"StartYear": 2021,
|
||||||
|
"Links": [
|
||||||
|
{
|
||||||
|
"Name": "Ichirinko",
|
||||||
|
"Description": "这是个大哥",
|
||||||
|
"Link": "https://ichirinko.top",
|
||||||
|
"AvatarImage": "https://ichirinko-blog-img-1.oss-cn-shenzhen.aliyuncs.com/Pic_res/img/202209122110798.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "志田千陽",
|
||||||
|
"Description": "日出多值得",
|
||||||
|
"Link": "https://zzachary.top/",
|
||||||
|
"AvatarImage": "https://zzachary.top/img/ztqy_hub928259802d192ff5718c06370f0f2c4_48203_300x0_resize_q75_box.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "不会写程序的晨旭",
|
||||||
|
"Description": "一个普通大学生",
|
||||||
|
"Link": "https://chenxutalk.top",
|
||||||
|
"AvatarImage": "https://www.chenxutalk.top/img/photo.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "万木长风",
|
||||||
|
"Description": "世界渲染中...",
|
||||||
|
"Link": "https://ryohai.fun",
|
||||||
|
"AvatarImage": "https://ryohai.fun/icon.jpg"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
13
YaeBlog/docker-compose.yaml
Normal file
13
YaeBlog/docker-compose.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
blog:
|
||||||
|
image: registry.cn-beijing.aliyuncs.com/jackfiled/blog:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.blog.rule=Host(`rrricardo.top`) || Host(`www.rrricardo.top`)"
|
||||||
|
- "traefik.http.services.blog.loadbalancer.server.port=8080"
|
||||||
|
- "traefik.http.routers.blog.tls=true"
|
||||||
|
- "traefik.http.routers.blog.tls.certresolver=myresolver"
|
||||||
|
- "com.centurylinklabs.watchtower.enable=true"
|
||||||
0
YaeBlog/source/drafts/.gitkeep
Normal file
0
YaeBlog/source/drafts/.gitkeep
Normal file
@@ -1,13 +1,11 @@
|
|||||||
---
|
---
|
||||||
title: 异构编程模型的昨天、今天与明天
|
title: 异构编程模型的昨天、今天与明天
|
||||||
date: 2024-11-04T22:20:41.2571467+08:00
|
date: 2024-10-16T13:34:49.0270134+08:00
|
||||||
tags:
|
tags:
|
||||||
- 编译原理
|
- 编译原理
|
||||||
- 组会汇报
|
- 组会汇报
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
随着摩尔定律的逐渐失效,将CPU和其他架构的计算设备集成在片上或者通过高速总线互联构建的异构系统成为了高性能计算的主流。但是在这种系统中,上层应用的设计与实现面临着异构系统中各个设备之间体系结构差异过大、缺乏良好的异构抽象以及统一的编程接口和应用程序的优化难度大等困难。
|
随着摩尔定律的逐渐失效,将CPU和其他架构的计算设备集成在片上或者通过高速总线互联构建的异构系统成为了高性能计算的主流。但是在这种系统中,上层应用的设计与实现面临着异构系统中各个设备之间体系结构差异过大、缺乏良好的异构抽象以及统一的编程接口和应用程序的优化难度大等困难。
|
||||||
|
|
||||||
异构并行编程模型便是解决这些编程和执行效率问题的解决方案。
|
异构并行编程模型便是解决这些编程和执行效率问题的解决方案。
|
||||||
@@ -26,11 +24,11 @@ tags:
|
|||||||
|
|
||||||
首先是异构系统中各个设备之间的并行计算能力不同。在同构的并行计算系统中,比如多核CPU中,虽然同一CPU的不同核之间、同一核的不同SIMD部件之间可以承担不同粒度的并行计算任务,但是其并行计算的能力是完全相同的。但是在一个典型的异构计算系统,例如CPU、GPU和FPGA组成的异构系统,不同设备的微架构具有本质差异,其并行计算的模式和能力都完全不同,设备之间的特长也完全不同。这种设备之间并行计算能力的差异使得系统中的任务划分和任务映射不再是均一的,而是具有显著的特异性。这种特点虽然也有利于表达实际应用的特点,但是却给异构并行计算模型的设计带来了巨大的困难。
|
首先是异构系统中各个设备之间的并行计算能力不同。在同构的并行计算系统中,比如多核CPU中,虽然同一CPU的不同核之间、同一核的不同SIMD部件之间可以承担不同粒度的并行计算任务,但是其并行计算的能力是完全相同的。但是在一个典型的异构计算系统,例如CPU、GPU和FPGA组成的异构系统,不同设备的微架构具有本质差异,其并行计算的模式和能力都完全不同,设备之间的特长也完全不同。这种设备之间并行计算能力的差异使得系统中的任务划分和任务映射不再是均一的,而是具有显著的特异性。这种特点虽然也有利于表达实际应用的特点,但是却给异构并行计算模型的设计带来了巨大的困难。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
其次是异构系统中加速设备数据分布可配置、设备间数据通信渠道多样性给数据分布和通信带来的困难。在同构并行系统中,CPU片内的存储是对于软件透明的缓存架构,在片外则是一个共享内存模型,因此在这类系统中,数据仅可能分布在片外的共享存储中,具有存储位置单一的特点,也不需要进行显式的通信操作。但是在异构系统中,不仅在单个加速设备内部可能有软件可分配的快速局部存储,设备之间的连接方式差异也很大。目前,大多个加速设备都是通过PCIe总线的方式同CPU进行连接,这使得加速设备无法通过和CPU相同的方式完成地址映射,存在某一设备无法访问另一设备片外存储的问题。这使得异构系统中数据可以分布在CPU、加速设备的片外存储和加速设备的片内多层次局部存储等多个位置,不仅使得编程模型的数据分布问题变得十分复杂,设备间的通信文件也可能需要显式进行。
|
其次是异构系统中加速设备数据分布可配置、设备间数据通信渠道多样性给数据分布和通信带来的困难。在同构并行系统中,CPU片内的存储是对于软件透明的缓存架构,在片外则是一个共享内存模型,因此在这类系统中,数据仅可能分布在片外的共享存储中,具有存储位置单一的特点,也不需要进行显式的通信操作。但是在异构系统中,不仅在单个加速设备内部可能有软件可分配的快速局部存储,设备之间的连接方式差异也很大。目前,大多个加速设备都是通过PCIe总线的方式同CPU进行连接,这使得加速设备无法通过和CPU相同的方式完成地址映射,存在某一设备无法访问另一设备片外存储的问题。这使得异构系统中数据可以分布在CPU、加速设备的片外存储和加速设备的片内多层次局部存储等多个位置,不仅使得编程模型的数据分布问题变得十分复杂,设备间的通信文件也可能需要显式进行。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
最后是异构系统中多层次数据共享和多范围同步操作带来的同步困难问题。这也可以认为是上个数据同步问题带来的后继问题:在异构系统中数据可能分布在不同位置的条件下,同步操作需要在众多的位置上保证共享数据的一致性,这使得同步操作的范围变得十分复杂。同时,在一些特定的加速设备中,例如GPU,可能还会有局部的硬件同步机制,这更加提高了在异构系统的同步操作的设计和实现难度。
|
最后是异构系统中多层次数据共享和多范围同步操作带来的同步困难问题。这也可以认为是上个数据同步问题带来的后继问题:在异构系统中数据可能分布在不同位置的条件下,同步操作需要在众多的位置上保证共享数据的一致性,这使得同步操作的范围变得十分复杂。同时,在一些特定的加速设备中,例如GPU,可能还会有局部的硬件同步机制,这更加提高了在异构系统的同步操作的设计和实现难度。
|
||||||
|
|
||||||
@@ -48,7 +46,7 @@ tags:
|
|||||||
|
|
||||||
从异构并行编程接口的功能角度上来说也可以分成两类:有些接口屏蔽了较多的异构并行编程细节,通常仅给程序员提供显式异构任务划分的机制,而数据分布和通信、同步等的工作由运行时系统负责完成,也有些接口将多数异构系统的硬件细节通过上述机制暴露给程序员使用,这在给编程带来更大自由度的同时带来了使用上的困难。
|
从异构并行编程接口的功能角度上来说也可以分成两类:有些接口屏蔽了较多的异构并行编程细节,通常仅给程序员提供显式异构任务划分的机制,而数据分布和通信、同步等的工作由运行时系统负责完成,也有些接口将多数异构系统的硬件细节通过上述机制暴露给程序员使用,这在给编程带来更大自由度的同时带来了使用上的困难。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### 异构任务划分机制研究
|
### 异构任务划分机制研究
|
||||||
|
|
||||||
@@ -126,7 +124,7 @@ public class Result
|
|||||||
|
|
||||||
采用显示异步数据分布和通信机制的主要问题是普通程序员一般无法充分利用这些接口获得性能上的提升。这通常使用因为加速设备通常采用了大量的硬件加速机制,例如GPU的全局内存访存合并机制,这使得程序员如果没有为数据分配合理的存储位置或者设定足够多的线程,会使得加速的效果大打折扣。因此出现了针对这类显式控制语言的优化方法,例如`CUDA-lite`,这个运行时允许程序元在CUDA程序中加入简单的制导语句,数据分布的相关工作使用`CUDA-lite`的运行时系统完成,降低了CUDA程序的编写难度。
|
采用显示异步数据分布和通信机制的主要问题是普通程序员一般无法充分利用这些接口获得性能上的提升。这通常使用因为加速设备通常采用了大量的硬件加速机制,例如GPU的全局内存访存合并机制,这使得程序员如果没有为数据分配合理的存储位置或者设定足够多的线程,会使得加速的效果大打折扣。因此出现了针对这类显式控制语言的优化方法,例如`CUDA-lite`,这个运行时允许程序元在CUDA程序中加入简单的制导语句,数据分布的相关工作使用`CUDA-lite`的运行时系统完成,降低了CUDA程序的编写难度。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
总结一下,为了解决异构系统带来的问题,异构并行编程接口具有如下三个特点:
|
总结一下,为了解决异构系统带来的问题,异构并行编程接口具有如下三个特点:
|
||||||
- 异构任务划分机制在传统并行编程模型的基础上增加了"异构特征描述"的维度,用于描述任务在不同设备上的分配情况;
|
- 异构任务划分机制在传统并行编程模型的基础上增加了"异构特征描述"的维度,用于描述任务在不同设备上的分配情况;
|
||||||
@@ -139,7 +137,7 @@ public class Result
|
|||||||
|
|
||||||
异构编程/运行时系统的任务映射机制主要有两种:一类是直接映射,即独立完成并行任务向异构平台映射的工作,另一种是间接映射,即需要借助其他异构编译和运行时系统协助来完成部分任务映射工作。直接映射系统一般在运行时系统中实现,而间接映射通过源到源变换和是运行时分析相结合的方式实现。
|
异构编程/运行时系统的任务映射机制主要有两种:一类是直接映射,即独立完成并行任务向异构平台映射的工作,另一种是间接映射,即需要借助其他异构编译和运行时系统协助来完成部分任务映射工作。直接映射系统一般在运行时系统中实现,而间接映射通过源到源变换和是运行时分析相结合的方式实现。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### 异构编译/运行时优化
|
### 异构编译/运行时优化
|
||||||
|
|
||||||
@@ -232,11 +230,11 @@ private:
|
|||||||
作为对比,一个使用CPU单线程计算的例子如下:
|
作为对比,一个使用CPU单线程计算的例子如下:
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
inline std::vector<int> cpuMatrixMultiply(
|
std::vector<std::vector<int>> matrix_multiply(
|
||||||
const std::vector<int>& a,
|
const std::vector<std::vector<int>>& a,
|
||||||
const std::vector<int>& b)
|
const std::vector<std::vector<int>>& b)
|
||||||
{
|
{
|
||||||
std::vector result(MATRIX_SIZE * MATRIX_SIZE, 0);
|
std::vector result(MATRIX_SIZE, std::vector(MATRIX_SIZE, 0));
|
||||||
|
|
||||||
for (int i = 0; i < MATRIX_SIZE; i++)
|
for (int i = 0; i < MATRIX_SIZE; i++)
|
||||||
{
|
{
|
||||||
@@ -245,10 +243,9 @@ inline std::vector<int> cpuMatrixMultiply(
|
|||||||
int temp = 0;
|
int temp = 0;
|
||||||
for (int k = 0; k < MATRIX_SIZE; k++)
|
for (int k = 0; k < MATRIX_SIZE; k++)
|
||||||
{
|
{
|
||||||
// a[i][j] = a[i][k] * b[k][j] where k in (0..MATRIX_SIZE)
|
temp += a[i][k] * b[k][j];
|
||||||
temp += a[i * MATRIX_SIZE + k] * b[k * MATRIX_SIZE + j];
|
|
||||||
}
|
}
|
||||||
result[i * MATRIX_SIZE + j] = temp;
|
result[i][j] = temp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,7 +255,7 @@ inline std::vector<int> cpuMatrixMultiply(
|
|||||||
|
|
||||||
### OpenMP
|
### OpenMP
|
||||||
|
|
||||||
OpenMP是`Open MultiProcessing`的缩写,是一个使用编译器制导(Directives)来进行共享内存平行计算的框架,在C、C++和Fortran语言的并行编程中得到的了广泛的应用。OpenMP提供了一个简单而灵活的接口,让程序员能够充分释放多核和多处理器系统性能。
|
OpenMP是`Opem MultiProcessing`的缩写,是一个使用编译器制导(Directives)来进行共享内存平行计算的框架,在C、C++和Fortran语言的并行编程中得到的了广泛的应用。OpenMP提供了一个简单而灵活的接口,让程序员能够充分释放多核和多处理器系统性能。
|
||||||
|
|
||||||
OpenMP从上面的介绍来看似乎并不是一个严格的异步并行编程模型,但是第一,OpenMP作为一个经典的并行编程框架,研究价值还是非常高的,其次在一些较新的OpenMP版本中其宣称也能利用NVIDIA GPU进行加速,似乎也能算是一个异构并行编程模型。
|
OpenMP从上面的介绍来看似乎并不是一个严格的异步并行编程模型,但是第一,OpenMP作为一个经典的并行编程框架,研究价值还是非常高的,其次在一些较新的OpenMP版本中其宣称也能利用NVIDIA GPU进行加速,似乎也能算是一个异构并行编程模型。
|
||||||
|
|
||||||
@@ -449,21 +446,19 @@ std::vector<std::vector<int>> cudaCalculateMatrix(const std::vector<std::vector<
|
|||||||
> - 首先是换了一台没有大小核异构设计的计算机进行实验,发现这下两次使用CPU计算的时间差异不大;
|
> - 首先是换了一台没有大小核异构设计的计算机进行实验,发现这下两次使用CPU计算的时间差异不大;
|
||||||
> - 加上了热身的阶段之后,计算时间没有发生明显的变化。
|
> - 加上了热身的阶段之后,计算时间没有发生明显的变化。
|
||||||
>
|
>
|
||||||
> 综上所述,可以认为此现象和异构CPU之间存在着明显的关联,但是缺乏直接证据。
|
> 综上所述,可以认为此现象和异构CPU之间存在这明显的关联,但是缺乏直接证据。
|
||||||
>
|
|
||||||
> 在我们调整了矩阵的数据布局之后,这里提到的实验结果又发生了变化。上面的实验结果是使用二维数据存储矩阵得到的,而在修改为使用一维数组(也就是现在提供的代码)之后,相同的CPU计算代码的计算时间又没有产生明显的变化了。看来这个问题可能和数据布局、CPU缓存等问题相关。
|
|
||||||
|
|
||||||
### OpenCL
|
### OpenCL
|
||||||
|
|
||||||
OpenCL是目前最为典型、发展最好的异构并行编程模型,毕竟其在官网的第一句话就是“为异构系统中并行编程的开放标准“。
|
OpenCL是目前最为典型、发展最好的异构并行编程模型,毕竟其在官网的第一句话就是“为异构系统中并行编程的开放标准“。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
从上图的OpenCL工作原理中可以看出,OpenCL和CUDA类似,也采用了Device-Host类型的编程接口。主机代码通常通过普通的C/C++代码进行编写,编译之后在CPU上执行,而设备代码使用一个特定的C语言方言OpenCL C进行编写,这个方言针对并行编程进行了扩展,并提供了一系列封装好的数学计算函数。
|
从上图的OpenCL工作原理中可以看出,OpenCL和CUDA类似,也采用了Device-Host类型的编程接口。主机代码通常通过普通的C/C++代码进行编写,编译之后在CPU上执行,而设备代码使用一个特定的C语言方言OpenCL C进行编写,这个方言针对并行编程进行了扩展,并提供了一系列封装好的数学计算函数。
|
||||||
|
|
||||||
设备代码上的编译方法有两种:在线编译和离线编译。其中在线编译就是指在程序运行时由对应设备厂商开发的OpenCL驱动将设备代码编译为在对应设备上运行的可执行代码,离线编译则有两种表现形式,第一种是在线编译的扩展版,由驱动编译得到的可执行程序可以通过API获取并保存下来,当下一需要在同一设备上调用时可以直接使用而不是再次编译,第二种则是完全独立的编译过程,在OpenCL程序运行之前使用单独的编译工具编译得到可执行文件。
|
设备代码上的编译方法有两种:在线编译和离线编译。其中在线编译就是指在程序运行时由对应设备厂商开发的OpenCL驱动将设备代码编译为在对应设备上运行的可执行代码,离线编译则有两种表现形式,第一种是在线编译的扩展版,由驱动编译得到的可执行程序可以通过API获取并保存下来,当下一需要在同一设备上调用时可以直接使用而不是再次编译,第二种则是完全独立的编译过程,在OpenCL程序运行之前使用单独的编译工具编译得到可执行文件。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
在提出离线编译之后,为了让驱动编译好的二进制文件可以在不同的设备之间复用,同时也是支持更为丰富的编译器生态系统,OpenCL的提出者Khronos设计了一种跨设备的、可迁移的中间表示形式[SPIRV](https://www.khronos.org/spir/)。这种中间形式的提出使得编程语言的提出者、编译器的开发人员可以直接将语言编译为`SPIRV`内核,这样就可以在任何支持`SPIRV`的OpenCL驱动上运行。下面将会介绍的`SYCL`和`Julia`语言都是基于`SPIRV`的中间语言进行构建的。`SPIRV`中间语言的提出也扩展了可以支持`OpenCL`的设备范围,现在已经有开发者和公司在探索将`SPIRV`编译到`Vulkan`、`DirectX`和`Metal`等传统意义上的图形API。
|
在提出离线编译之后,为了让驱动编译好的二进制文件可以在不同的设备之间复用,同时也是支持更为丰富的编译器生态系统,OpenCL的提出者Khronos设计了一种跨设备的、可迁移的中间表示形式[SPIRV](https://www.khronos.org/spir/)。这种中间形式的提出使得编程语言的提出者、编译器的开发人员可以直接将语言编译为`SPIRV`内核,这样就可以在任何支持`SPIRV`的OpenCL驱动上运行。下面将会介绍的`SYCL`和`Julia`语言都是基于`SPIRV`的中间语言进行构建的。`SPIRV`中间语言的提出也扩展了可以支持`OpenCL`的设备范围,现在已经有开发者和公司在探索将`SPIRV`编译到`Vulkan`、`DirectX`和`Metal`等传统意义上的图形API。
|
||||||
|
|
||||||
@@ -659,7 +654,7 @@ AdaptiveCpp由四个部分组成,分别在不同的C++命名空间中提供。
|
|||||||
|
|
||||||
- AdaptiveCpp Runtime:运行时实际上实现了设备调度、任务图管理和执行、数据管理、后端管理、任务调度和同步等等功能,运行时负责同各种支持后端的运行时交互来实现上述的功能。
|
- AdaptiveCpp Runtime:运行时实际上实现了设备调度、任务图管理和执行、数据管理、后端管理、任务调度和同步等等功能,运行时负责同各种支持后端的运行时交互来实现上述的功能。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- Compiler:考虑到在用户编写的代码中可能使用一些特定后端的方言,因此普通的C++编译器无法正常编译所有的用户代码。因此用户代码的编译是通过一个名为`acpp`的Python脚本驱动的,这个脚本将各个后端的不同编译器暴露为一个统一的编程接口。
|
- Compiler:考虑到在用户编写的代码中可能使用一些特定后端的方言,因此普通的C++编译器无法正常编译所有的用户代码。因此用户代码的编译是通过一个名为`acpp`的Python脚本驱动的,这个脚本将各个后端的不同编译器暴露为一个统一的编程接口。
|
||||||
|
|
||||||
@@ -673,7 +668,7 @@ AdaptiveCpp同时支持多种不同的编译流程。
|
|||||||
|
|
||||||
第一种通用的编译流程显然是泛用性最广的一种编译流程,同时也是AdaptiveCpp推荐的编译流程。
|
第一种通用的编译流程显然是泛用性最广的一种编译流程,同时也是AdaptiveCpp推荐的编译流程。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
下面是一段使用SYCL进行矩阵乘法加速的代码:
|
下面是一段使用SYCL进行矩阵乘法加速的代码:
|
||||||
|
|
||||||
@@ -777,93 +772,20 @@ OpenACC是作为一个标准的形式提供的,实现了该标准的编译器
|
|||||||
| GCC 12 | 支持到OpenACC 2.6 |
|
| GCC 12 | 支持到OpenACC 2.6 |
|
||||||
| [Omni Compiler Project](https://github.com/omni-compiler/omni-compiler) | 源到源编译器,将带有制导的源代码翻译到带有运行时调用的平台代码,近两年没有活跃开发 |
|
| [Omni Compiler Project](https://github.com/omni-compiler/omni-compiler) | 源到源编译器,将带有制导的源代码翻译到带有运行时调用的平台代码,近两年没有活跃开发 |
|
||||||
| [OpenUH](https://github.com/uhhpctools/openuh) | 项目开发者在7年前的最后一次提交了中删除了README中有关OpenACC的内容 |
|
| [OpenUH](https://github.com/uhhpctools/openuh) | 项目开发者在7年前的最后一次提交了中删除了README中有关OpenACC的内容 |
|
||||||
| [OpenArc](https://csmd.ornl.gov/project/openarc-open-accelerator-research-compiler) | 是学术界出品的还在活跃开发的编译器,看上去还做了不少工作的样子,就是OpenACC官网上的链接已经失效了找起来比较麻烦,而且宣称是一个开源编译器,但是获取源代码和二进制文件需要联系他们(美国橡树岭国家实验室)创建账户,这看去对于我们这些Foreign Adversary有些抽象了。 |
|
|
||||||
|
|
||||||
在试验OpenACC时遇到了巨大的困难,不论是使用gcc还是NVIDIA HPC SDK都没有办法实现明显的并行编程加速,多次实验之后都没有找到的问题的所在。这里还是贴一下实验的代码和实验的数据。
|
|
||||||
|
|
||||||
实验中编写的OpenACC加速代码如下:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
static std::vector<int> OpenACCCpuCalculateMatrix(const std::vector<int>& a, const std::vector<int>& b)
|
|
||||||
{
|
|
||||||
constexpr int length = MATRIX_SIZE * MATRIX_SIZE;
|
|
||||||
|
|
||||||
const auto aBuffer = new int[length];
|
|
||||||
const auto bBuffer = new int[length];
|
|
||||||
const auto cBuffer = new int[length];
|
|
||||||
|
|
||||||
for (int i = 0; i < length; i++)
|
|
||||||
{
|
|
||||||
aBuffer[i] = a[i];
|
|
||||||
bBuffer[i] = b[i];
|
|
||||||
cBuffer[i] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma acc enter data copyin(aBuffer[0:length], bBuffer[0:length])
|
|
||||||
#pragma acc enter data create(bBuffer[0:length])
|
|
||||||
#pragma acc data present(aBuffer[0:length], bBuffer[0:length], cBuffer[0:length])
|
|
||||||
{
|
|
||||||
#pragma acc kernels loop independent
|
|
||||||
for (int i = 0; i < MATRIX_SIZE; i++)
|
|
||||||
{
|
|
||||||
#pragma acc loop independent
|
|
||||||
for (int j = 0; j < MATRIX_SIZE; j++)
|
|
||||||
{
|
|
||||||
int temp = 0;
|
|
||||||
#pragma acc loop independent reduction(+:temp)
|
|
||||||
for (int k = 0; k < MATRIX_SIZE; k++)
|
|
||||||
{
|
|
||||||
temp += aBuffer[i * MATRIX_SIZE + k] * bBuffer[k * MATRIX_SIZE + j];
|
|
||||||
}
|
|
||||||
cBuffer[i * MATRIX_SIZE + j] = temp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#pragma acc exit data copyout(cBuffer[0:length])
|
|
||||||
#pragma acc exit data delete(aBuffer[0:length], bBuffer[0:length])
|
|
||||||
|
|
||||||
std::vector result(MATRIX_SIZE * MATRIX_SIZE, 0);
|
|
||||||
|
|
||||||
for (int i = 0; i < length; ++i)
|
|
||||||
{
|
|
||||||
result[i] = cBuffer[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
delete[] aBuffer;
|
|
||||||
delete[] bBuffer;
|
|
||||||
delete[] cBuffer;
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
实验中使用分别使用`NVIDIA HPC SDK`和`GCC`编译运行的结果如下:
|
|
||||||
|
|
||||||
| 编译器 | 类型 | 运行时间 |
|
|
||||||
| -------------- | ------- | -------- |
|
|
||||||
| NVIDIA HPC SDK | OpenACC | 19315ms |
|
|
||||||
| NVIDIA HPC SDK | CPU | 22942ms |
|
|
||||||
| GCC | OpenACC | 19999ms |
|
|
||||||
| GCC | CPU | 22623ms |
|
|
||||||
|
|
||||||
### oneAPI
|
### oneAPI
|
||||||
|
|
||||||
oneAPI是Intel公司提出的一套异构并行编程框架,该框架致力于达成如下几个目标:(1)定义一个跨架构、跨制造商的统一开放软件平台;(2)允许同一套代码可以在不同硬件制造商和加速技术的硬件上运行;(3)提供一套覆盖多个编程领域的库API。为了实现这些目标,oneAPI同上文中已经提到过的开放编程标准SYCL紧密合作,oneAPI也提供了一个SYCL的编译器和运行时;同时oneAPI也提供了一系列API库,包括`oneDPL`、`oneDNN`、`oneTBB`和`oneMKL`等。
|
|
||||||
|
|
||||||

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

|

|
||||||
即使我想写的很多项目都没有被扎实的推进下来,但是学习的一些的C语言还是让我受益匪浅。
|
即使我想写的很多项目都没有被扎实的推进下来,但是学习的一些的C语言还是让我受益匪浅。
|
||||||
现在看来,这个假期真是,**学也没有学好,耍也没有耍好**的典型。
|
现在看来,这个假期真是,**学也没有学好,耍也没有耍好**的典型。
|
||||||
|
|
||||||
BIN
YaeBlog/source/posts/2021-final/1.png
LFS
Normal file
BIN
YaeBlog/source/posts/2021-final/1.png
LFS
Normal file
Binary file not shown.
@@ -1,13 +1,11 @@
|
|||||||
---
|
---
|
||||||
title: 2022年终总结
|
title: 2022年终总结
|
||||||
date: 2022-12-30T14:58:12.0000000
|
|
||||||
tags:
|
tags:
|
||||||
- 杂谈
|
- 随笔
|
||||||
- 年终总结
|
date: 2022-12-30 14:58:12
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
2022是困难的一年。我们需要为2023年做好准备。
|
2022是困难的一年。我们需要为2023年做好准备。
|
||||||
|
|
||||||
<!--more-->
|
<!--more-->
|
||||||
@@ -58,11 +56,11 @@ tags:
|
|||||||
|
|
||||||
小小的总结一下:2022年可以算得上是一事无成的一年,还搞砸了不少的事情。在写代码上进展有限,成绩上大幅倒退,说好的六级英语和大学物理竞赛都没有参加,在年末应对疫情进展的时候更是把“不知所措”这个成语诠释的淋漓尽致。
|
小小的总结一下:2022年可以算得上是一事无成的一年,还搞砸了不少的事情。在写代码上进展有限,成绩上大幅倒退,说好的六级英语和大学物理竞赛都没有参加,在年末应对疫情进展的时候更是把“不知所措”这个成语诠释的淋漓尽致。
|
||||||
|
|
||||||

|

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

|

|
||||||
|
|
||||||
## 展望
|
## 展望
|
||||||
|
|
||||||
BIN
YaeBlog/source/posts/2022-final/2022-12-30-14-26-19-QQ_Image_1672381538441.jpg
LFS
Normal file
BIN
YaeBlog/source/posts/2022-final/2022-12-30-14-26-19-QQ_Image_1672381538441.jpg
LFS
Normal file
Binary file not shown.
BIN
YaeBlog/source/posts/2022-final/2022-12-30-14-28-12-QQ_Image_1672381543836.jpg
LFS
Normal file
BIN
YaeBlog/source/posts/2022-final/2022-12-30-14-28-12-QQ_Image_1672381543836.jpg
LFS
Normal file
Binary file not shown.
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
title: 2022年暑假碎碎念
|
title: 2022年暑假碎碎念
|
||||||
date: 2022-08-22T15:39:13.0000000
|
|
||||||
tags:
|
tags:
|
||||||
- 杂谈
|
- 随笔
|
||||||
|
typora-root-url: 2022-summer-vacation
|
||||||
|
date: 2022-08-22 15:39:13
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
在8个月的漫长寒假的最后两个月,~~也就是俗称的暑假中~~,我都干了些什么?
|
在8个月的漫长寒假的最后两个月,~~也就是俗称的暑假中~~,我都干了些什么?
|
||||||
|
|
||||||
<!--more-->
|
<!--more-->
|
||||||
@@ -32,7 +32,7 @@ tags:
|
|||||||
- 下定决定要参加下一学期的物理竞赛,但是在听了讲座之后直接决定开学再开始学习,~~我知道我在家没法学习,俗称开摆~~
|
- 下定决定要参加下一学期的物理竞赛,但是在听了讲座之后直接决定开学再开始学习,~~我知道我在家没法学习,俗称开摆~~
|
||||||
- 又捡起了`Blender`,并在[Github](https://github.com/tanjian1998/bupt_minecraft)上找到了伟大的前辈们在`Minecraft`里复刻的老校区,希望能用`Blender`渲染几张图当作桌面。
|
- 又捡起了`Blender`,并在[Github](https://github.com/tanjian1998/bupt_minecraft)上找到了伟大的前辈们在`Minecraft`里复刻的老校区,希望能用`Blender`渲染几张图当作桌面。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
> 在此感谢所有为此付出过汗水的前辈们,让我这个即将搬入老校区的萌新能提前一睹老校区的风采。
|
> 在此感谢所有为此付出过汗水的前辈们,让我这个即将搬入老校区的萌新能提前一睹老校区的风采。
|
||||||
|
|
||||||
BIN
YaeBlog/source/posts/2022-summer-vacation/result1.png
LFS
Normal file
BIN
YaeBlog/source/posts/2022-summer-vacation/result1.png
LFS
Normal file
Binary file not shown.
@@ -1,12 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: 2023年年终总结
|
title: 2023年年终总结
|
||||||
date: 2024-02-29T20:18:19.0000000
|
|
||||||
tags:
|
tags:
|
||||||
- 杂谈
|
- 随笔
|
||||||
- 年终总结
|
date: 2024-2-29 20:18:19
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
虽然2023年已经过去了两个月,但是年终总结还是要发的。
|
虽然2023年已经过去了两个月,但是年终总结还是要发的。
|
||||||
|
|
||||||
<!--more-->
|
<!--more-->
|
||||||
@@ -45,7 +43,7 @@ tags:
|
|||||||
|
|
||||||
2023年最令我吃惊的事情是我刷B站的时长:
|
2023年最令我吃惊的事情是我刷B站的时长:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
容易计算得出,我一共看了64天的B站,接近六分之一的时间都在看。虽然我确实有着在干活的时候黑听B站和把B站当作音乐播放器的习惯,但是这个时间未免有点太长了。下一年一定要在这个方面做出一定的改变,将更多的时间放在看书上面去,~~虽然写这句话的时候我就在黑听B站~~。
|
容易计算得出,我一共看了64天的B站,接近六分之一的时间都在看。虽然我确实有着在干活的时候黑听B站和把B站当作音乐播放器的习惯,但是这个时间未免有点太长了。下一年一定要在这个方面做出一定的改变,将更多的时间放在看书上面去,~~虽然写这句话的时候我就在黑听B站~~。
|
||||||
|
|
||||||
BIN
YaeBlog/source/posts/2023-final/image-20240303165826486.png
LFS
Normal file
BIN
YaeBlog/source/posts/2023-final/image-20240303165826486.png
LFS
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user