Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions docs/01-basic/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# (Q1.1)

## 1. 按逗号分割日志的语句,以及如何指定每个字段的意义

本框架借助第三方 CSV 解析库 **CsvHelper** 来完成按逗号分列的工作:

```csharp
using var csv = new CsvReader(logFile, config);
csv.Context.RegisterClassMap<LogRecordMap>();
foreach (var logRecord in csv.GetRecords<LogRecord>()) { ... }
```

其中真正「按逗号把一行切成多个字段」的工作由 `CsvReader` / `csv.GetRecords<LogRecord>()` 在库内部完成(它还能正确处理 `message` 字段两端的双引号以及 JSON 内部出现的逗号)。

「每一行的第几个字段代表何种意义」是通过一个继承自 `ClassMap<LogRecord>` 的映射类 `LogRecordMap` 来指定的,使用 `Map(...).Index(n)` 把 CSV 的第 `n` 列绑定到 `LogRecord` 的对应属性上:

```csharp
internal class LogRecordMap : ClassMap<LogRecord>
{
public LogRecordMap()
{
Map(m => m.LineNo).Index(0); // 第 0 列 -> LineNo
Map(m => m.Timestamp).Index(1); // 第 1 列 -> Timestamp
Map(m => m.PodName).Index(2); // 第 2 列 -> PodName
Map(m => m.Message).Index(3); // 第 3 列 -> Message
}
}
```

即:`Index(0)` 对应 `lineno`、`Index(1)` 对应 `timestamp`、`Index(2)` 对应 `pod-name`、`Index(3)` 对应 `message`。随后通过 `csv.Context.RegisterClassMap<LogRecordMap>()` 让 CsvHelper 读取时按这个映射把每列填入 `LogRecord`。

## 2. 在哪个方法内、用哪几条语句判断日志种类

在 `Parser/LineParser.cs` 的 `ParseLine(LogRecord logRecord)` 方法内判断。先用 `JsonDocument` 把 `message` 当作 JSON 解析,再读取其中的 `event` 字段,用 `switch` 表达式根据其取值分流到不同的工厂方法:

```csharp
using (var doc = JsonDocument.Parse(logRecord.Message))
{
var root = doc.RootElement;
if (root.TryGetProperty("event", out var eventElement))
{
return eventElement.GetString() switch
{
"call" => LineParser.CreateCall(logRecord),
"request" => LineParser.CreateRequest(logRecord),
"internal" => LineParser.CreateInternal(logRecord),
_ => throw new FormatException(...)
};
}
...
}
```

也就是说,判断种类的语句是 `root.TryGetProperty("event", out var eventElement)` 配合 `eventElement.GetString() switch { "call" => ..., "request" => ..., "internal" => ... }`。

## 3. 确定种类后调用哪个库方法解析 JSON

确定种类后,在对应的工厂方法(如 `CreateCall` / `CreateRequest` / `CreateInternal`)中调用 `System.Text.Json` 提供的:

```csharp
JsonSerializer.Deserialize<CallMessage>(logRecord.Message, options)
```

把 JSON 字符串反序列化成一个强类型的 `record`(如 `CallMessage`)。

### 3.1 如何防止日志中字段缺失

通过在反序列化目标 `record` 的每个属性上标注 `[property: JsonRequired]` 特性,例如:

```csharp
private record CallMessage(
[property: JsonRequired] string Severity,
[property: JsonRequired] string RequestId,
[property: JsonRequired] string TargetService,
[property: JsonRequired] int DurationMs
);
```

`[JsonRequired]` 告诉 JSON 序列化器这些属性是必需的:当 JSON 中缺少对应键时,`JsonSerializer.Deserialize` 会抛出 `JsonException`,从而把「字段缺失」这一异常情况暴露出来。此外,反序列化结果后还跟了一个 `?? throw new FormatException(...)`,用于在结果为 `null` 时也抛出异常,进一步兜底:

```csharp
var callMessage = JsonSerializer.Deserialize<CallMessage>(logRecord.Message, options)
?? throw new FormatException(...);
```

### 3.2 如何让 JSON 解析器完成「烤串命名法 → 大驼峰命名法」的转换

通过配置 `JsonSerializerOptions` 的 `PropertyNamingPolicy`:

```csharp
private static JsonSerializerOptions options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower,
};
```

`JsonNamingPolicy.KebabCaseLower` 作为命名策略,会在反序列化时把 C# 属性名(大驼峰,如 `RequestId`、`TargetService`、`DurationMs`)转换成小写烤串形式(`request-id`、`target-service`、`duration-ms`)再去和 JSON 中的键匹配。这样就在不修改 C# 属性名的前提下,完成了 `abc-def` 与 `AbcDef` 两种命名法之间的映射。

# (Q1.2)

以一个 Call 事件为例,调用 `KeyValueVisitor.Dump(entry)`(其中 `entry` 的静态类型是 `LogEntry`,实际运行时类型是 `CallLogEntry`)后,方法调用链如下(.NET 内置库方法略):

+ `Dictionary<string, string> KeyValueVisitor.Dump(LogEntry entry)`
- 内部执行 `return entry.Accept(this);`,由于 `entry` 的运行时类型是 `CallLogEntry`,发生多态分派,调用 `CallLogEntry` 中被 override 的 `Accept`
+ `TResult CallLogEntry.Accept<TResult>(ILogEntryVisitor<TResult> visitor)`
- 内部执行 `return visitor.Visit(this);`,此处 `this` 的编译时类型是 `CallLogEntry`,于是通过重载分派选中 `KeyValueVisitor.Visit(CallLogEntry entry)`
+ `Dictionary<string, string> KeyValueVisitor.Visit(CallLogEntry entry)`
- 构造并返回保存了 `LineNo`、`Timestamp`、`PodName`、`Severity`、`EventType`、`RequestId`、`TargetService`、`DurationMs` 的 `Dictionary<string, string>`

这里正是访问者模式的「双重分派(double dispatch)」:第一重由 `entry.Accept(this)` 按 `entry` 的**运行时类型**分派到 `CallLogEntry.Accept`;第二重由 `visitor.Visit(this)` 按 `this` 的**编译时类型**(`CallLogEntry`)分派到 `KeyValueVisitor.Visit(CallLogEntry)` 的重载,从而对外部屏蔽了具体子类,却仍能对每种日志执行不同的行为。

# (Q1.3.b)
根据TODO框架和guidance.md完成任务××.AI能给出达成任务要求的代码并自行测试验证。有时候AI会有过度、无效兜底的问题,在这次作业中基本没有出现
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/02-multithreading/QQ_1785484373806.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/02-multithreading/QQ_1785484396152-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/02-multithreading/QQ_1785484396152-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/02-multithreading/QQ_1785484396152-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/02-multithreading/QQ_1785484396152.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/02-multithreading/QQ_1785484923024.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/02-multithreading/QQ_1785485348097.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/02-multithreading/QQ_1785485430494.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/02-multithreading/QQ_1785485472946.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/02-multithreading/QQ_1785485519059.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
188 changes: 188 additions & 0 deletions docs/02-multithreading/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# 02-multithreading 实验报告

## 一、功能介绍

本节在 `01-basic` 的单文件日志解析基础上,实现了一个**目录级别的并行日志分析器**,并配有一个简易的交互式控制台界面。整体由三部分组成:

| 文件 | 任务 | 作用 |
| :--- | :--- | :--- |
| `LogAnalyzer/WorkQueue.cs` | T2.1 | 基于非线程安全 `Queue<T>` 自造的**线程安全阻塞队列** |
| `LogAnalyzer/LogFileAnalyzer.cs` | T2.2 | 扫描目录、调度多线程并行解析、保存结果 |
| `LocalCli/Program.cs` | T2.3 | 与用户交互的控制台菜单,串联上述能力 |

### 1. 线程安全队列 `WorkQueue<T>`(T2.1)

共享变量为内部的 `Queue<T> _items` 与「是否结束放入」标记 `_isCompleted`,两者统一用 `lock(_items)` 这同一个互斥量保护。这是一个带「结束放入」语义的无限容量生产者—消费者问题:

- `Enqueue`:加锁后入队,并 `Monitor.Pulse`(signal)唤醒一个等待中的消费者;若已 `CompleteAdding` 则抛 `InvalidOperationException`。
- `CompleteAdding`:置位 `_isCompleted`,并 `Monitor.PulseAll`(broadcast)唤醒**全部**正在等待的消费者,使其能够正常退出而不是永远阻塞。
- `TryDequeue`:加锁后用 **`while`** 循环判断「队列空 且 未结束」才 `Monitor.Wait`;被唤醒后重新检查条件。队列非空则取出返回 `true`,否则(空且已结束)返回 `false` 并把 `item` 置为 `default`。

### 2. 并行日志分析 `LogFileAnalyzer`(T2.2)

- **目录扫描**:`ChangeDirectory` 中 `Directory.EnumerateFiles(directoryPath, "*.log", SearchOption.TopDirectoryOnly)` 扫描当前目录下所有 `.log` 文件,并把每个文件以 `NotAnalyzed` 状态登记进 `_analysisResults`。
- **状态位 `_isAnalyzing`**:`AnalyzeFiles` 进入分析前置 `true`,在 `try/finally` 的 `finally` 里**加锁**复位为 `false`,保证异常时也能复位。该标志保证同一时刻只允许一个分析任务进行,其余并发请求抛 `InvalidOperationException`。
- **`RunWorkers`**:先把 `State == NotAnalyzed` 的文件筛选出来,跳过已 `Succeeded`/`Failed` 的文件以节省计算资源,未知文件抛 `InvalidOperationException`,用主线程作为生产者把待解析文件 `Enqueue` 进 `WorkQueue` 后 `CompleteAdding`;再开启 `degreeOfParallelism` 个 worker 线程(入口方法 `WorkerMain`),最后 `Join` 等待全部 worker 结束。
- `degreeOfParallelism == 0` 表示取 `Environment.ProcessorCount`;并按 `[1, 文件数]` 夹取,避免开多余空转线程。
- **`WorkerMain`**:每个消费者循环 `TryDequeue` 取文件,用各自的 `LogFileParser` + `StreamReader` 解析;解析成功 → `Succeeded`,解析抛异常 → `Failed`(写 `ErrorMessage`、空 `Entries`)。最后**加 `_syncRoot` 锁**把结果写回共享的 `_analysisResults` 字典。
- `parser.Parse(reader).ToList()` 中的 `ToList()` 用于强制立即求值:`Parse` 是 `yield return` 的惰性迭代器,若不立刻物化,异常会推迟到 `try` 之外才发生而无法被捕获。
- **`TryGetAnalysisResult`**:加锁查 `_analysisResults`,存在则返回 `true` 并输出结果,否则返回 `false`。

### 3. 控制台交互 `LocalCli/Program.cs`(T2.3)

菜单提供 6 个功能,`LogFileAnalyzer` 对错误输入,CLI 层把这些异常兜住并提示用户重新输入,保证非法输入不会让程序崩溃。

| 选项 | 功能 | 实现 |
| :--: | :--- | :--- |
| —— | `InputDirectory` | 输入目录构造 `analyzer`;目录不存在→提示重输(`ChangeDirectory` 返回 `false`),路径非法→捕获 `ArgumentException` 提示重输 |
| 1 | `ShowLogFiles` | 调用 `GetLogFiles()` 列出目录中全部 `.log` 文件名 |
| 2 | `AnalyzeFiles` | 输入逗号分隔的文件名,`Split` 时去空白,调用 `AnalyzeFiles(0, ...)`;捕获 `ArgumentException`/`InvalidOperationException` |
| 3 | `AnalyzeAll` | 调用 `AnalyzeAll(0)` 分析全部;捕获 `InvalidOperationException` |
| 4 | `GetAnalysisResult` | 输入文件名查结果:不存在→提示;`NotAnalyzed`→提示先分析;`Succeeded`→用 `KeyValueVisitor.Dump` 逐条输出;`Failed`→输出 `ErrorMessage` |
| 5 | ChangeDirectory | 重新输入目录(复用 `InputDirectory`) |
| 6 | Exit | 退出 |

非法的菜单输入(非数字 / 超出范围)会被 `int.Parse` 的异常捕获或 `default` 分支拦截,提示重输,不会崩溃。

---

## 二、功能演示

### 启动 + 查看日志文件列表

![alt text](./0b487ce620ea8067dee251d1968508c4.png)

### 分析指定文件

![alt text](./QQ_1785484396152-3.png)

### 查看分析成功的结果,查询尚未分析的文件

![alt text](./e87b65691415cb6e03e2e985cded5bfb.png)

### 分析全部 + 查看失败文件的错误信息

![alt text](./QQ_1785484923024.png)


## 三、鲁棒性测试

### 不存在的目录

![alt text](./QQ_1785485348097.png)

### 非法的菜单输入

![alt text](./QQ_1785485472946.png)

### 分析不存在的文件

![alt text](./QQ_1785485519059.png)

### 查询不存在 / 未分析的文件

![alt text](./QQ_1785485430494.png)

---

## 四、问答题

### (Q2.1)

共享变量有两个:

- `Queue<T> _items`:真正存放元素的内部队列;
- `bool _isCompleted`:标记是否已结束放入(`CompleteAdding` 是否被调用过)。

两者都通过**以 `_items` 这个引用对象本身作为互斥量**来保护——所有对它们的读写都放在 `lock(_items)` 临界区内:

```csharp
public void Enqueue(T item)
{
lock (_items)
{
if (_isCompleted) throw new InvalidOperationException(...);
_items.Enqueue(item);
Monitor.Pulse(_items);
}
}
```
同步关系(消费者等待 / 生产者唤醒)也建立在同一个 `_items` 上:消费者 `Monitor.Wait(_items)` 释放锁并休眠,生产者用 `Monitor.Pulse`(signal)/ `Monitor.PulseAll`(broadcast)唤醒。这是 C# `Monitor` 实现的 MESA 模型条件变量。

`LogFileAnalyzer` 中的共享变量有:

- `string? _currentDirectory`:当前日志目录;
- `bool _isAnalyzing`:是否正在分析;
- `Dictionary<string, FileInfo> _logFiles`:文件名到 `FileInfo` 的映射;
- `Dictionary<string, AnalysisResult> _analysisResults`:文件名到分析结果的映射。

它们统一由一个专用的互斥量对象 `private readonly object _syncRoot = new();` 保护,所有访问都放在 `lock(_syncRoot)` 内(`ChangeDirectory`、`GetLogFiles`、`TryGetAnalysisResult`,以及 worker 写回结果时):

```csharp
lock (_syncRoot)
{
_analysisResults[file.Name] = result;
}
```

`RunWorkers` 中还有一个局部构造的 `WorkQueue<FileInfo>` 实例,被主线程(生产者)和各 worker(消费者)共享,但它由 `WorkQueue` **内部自己的 `_items` 锁**保护,属于另一套独立的互斥机制,与 `_syncRoot` 无关。`IsAnalyzing`、`IsCompleted` 等属性的 getter 也都通过加锁读取,避免读到未同步的值。

用 `if` 而非 `while` 在虚假唤醒下的后果:

以无限容量生产者—消费者为例,若消费者写成:

```csharp
lock (mtx)
{
if (buffer == 0) // 用 if
{
Monitor.Wait(mtx);
}
buffer -= 1; // 直接取用商品
}
```

当发生虚假唤醒(`Wait` 在没有人 `Pulse` 的情况下自行返回)时:

- 线程被唤醒后**不再重新检查** `buffer == 0`,直接执行 `buffer -= 1`;
- 但此时 `buffer` 仍为 0(根本没生产出商品),于是「取走了一个不存在的商品」,`buffer` 变成 −1,状态不变量被破坏,出现逻辑错误。

更严重的是多消费者下的竞争:MESA 模型中,线程被 `Pulse` 唤醒后并不会立即拿到锁,而要重新去抢锁;在它重新拿到锁之前,另一个消费者可能已经把唯一的商品取走了(也可能是纯粹的虚假唤醒)。若用 `if`,这个被唤醒的消费者不会再检查条件,照样去取商品,于是出现「一个商品被消费两次」或「消费了空仓库」的错误。

而用 `while`:

```csharp
while (buffer == 0) { Monitor.Wait(mtx); }
```

被唤醒后会**再次判断条件**,若仓库仍为空(无论是虚假唤醒还是被别的消费者抢先),就继续 `Wait`,只有确实非空时才取用——这才是正确的。


### (Q2.2)

在 `ChangeDirectory` 方法中,这段代码完成了扫描:

```csharp
var logFiles = Directory.EnumerateFiles(directoryPath, "*.log", SearchOption.TopDirectoryOnly)
.Select(filePath => Path.GetFileName(filePath))
.OrderBy(fileName => fileName);
foreach (var fileName in logFiles)
{
_logFiles.Add(fileName, new FileInfo(Path.Join(_currentDirectory, fileName)));
_analysisResults.Add(fileName, new AnalysisResult(...));
}
```

其中真正「扫描目录里所有 `.log` 文件」的是 `Directory.EnumerateFiles(directoryPath, "*.log", SearchOption.TopDirectoryOnly)`:第一个参数是目录、第二个是匹配模式 `*.log`、第三个 `SearchOption.TopDirectoryOnly` 表示只扫描当前目录(不进入子目录);后面的 `Select`/`OrderBy` 只是把得到的路径取出文件名并排序。

若要递归扫描所有子目录,把第三个参数改为 `SearchOption.AllDirectories`,即可让 `EnumerateFiles` 递归遍历所有子目录、子子目录……:

```csharp
Directory.EnumerateFiles(directoryPath, "*.log", SearchOption.AllDirectories)
```

一个需要注意的细节:当前代码用 `Path.GetFileName(filePath)`(仅文件名)作为 `_logFiles` / `_analysisResults` 的键。非递归时文件名不会重复,没有问题;但改为递归后,不同子目录下可能存在同名文件(例如两个子目录里都有 `20260701.log`),会造成字典键冲突、后扫到的覆盖先扫到的。因此递归版本更适合改用「相对路径」作为键,例如 `Path.GetRelativePath(directoryPath, filePath)`,以避免重名冲突。

### (Q2.3)

根据TODO框架和guidance.md完成任务××.AI能给出达成任务要求的代码并自行测试验证。有时候AI会有过度、无效兜底的问题,在这次作业中基本没有出现
Binary file added docs/03-async-grpc/QQ_1785571614496.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/03-async-grpc/QQ_1785571631493.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/03-async-grpc/QQ_1785571665044.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/03-async-grpc/QQ_1785571809037.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/03-async-grpc/QQ_1785572331363.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/03-async-grpc/QQ_1785572349080.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/03-async-grpc/QQ_1785572368644.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/03-async-grpc/QQ_1785572407446.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/03-async-grpc/QQ_1785572418459.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/03-async-grpc/QQ_1785762240394.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading