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
95 changes: 95 additions & 0 deletions docs/01-basic/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# 01-basic 问答题报告

## (Q1.1) 代码框架分析

### 1. 哪条语句将日志按逗号分割?如何指定第几个字段代表何种意义?

`LogFileParser.cs` 中的这一句把每行日志按逗号分割:

```csharp
var fields = line.Split(',');
```

分割后按**位置约定**对应字段意义:`fields[0]` 是 `lineno`、`fields[1]` 是 `timestamp`、`fields[2]` 是 `pod-name`、`fields[3]` 是 `message`,随后用它们构造 `LogRecord`:

```csharp
var logRecord = new LogRecord(
LineNo: int.Parse(fields[0]),
Timestamp: fields[1],
PodName: fields[2],
Message: Unescape(fields[3]) // message 内的 CSV 引号转义还原
);
```

也就是说,字段含义不是写在数据里的,而是框架代码里"位置 → 属性名"的固定映射。

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

在 `LineParser.ParseLine` 方法内。先用 `JsonDocument.Parse` 把 `message` 解析成只读 DOM,再用 `TryGetProperty("event", ...)` 取出 `event` 字段,然后用 `switch` 表达式按其值分派到 `CreateCall` / `CreateRequest` / `CreateInternal`:

```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(...)
};
}
}
```

### 3. 确定日志种类后,调用了哪个库方法解析 JSON?

调用 `System.Text.Json.JsonSerializer.Deserialize<T>(json, options)`,把 message 反序列化为强类型的 `CallMessage` / `RequestMessage` / `InternalMessage` record。

**如何防止字段缺失?** 三个层面:

1. record 的属性标注了 `[property: JsonRequired]`——缺失该键时 `Deserialize` 会抛 `JsonException`(测试 T1.2.5/T1.2.6 正是验证这一点);
2. record 属性是非可空类型(如 `int DurationMs`),值为 `null` 时同样失败;
3. 反序列化结果用 `?? throw new FormatException(...)` 兜底,防止整个 message 不是合法 JSON 对象时返回 `null`。

**命名法转换(kebab-case → 大驼峰)** 是通过共享的序列化选项完成的:

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

`PropertyNamingPolicy = KebabCaseLower` 告诉序列化器:CLR 属性 `RequestId` 对应 JSON 键 `request-id`、`TargetService` 对应 `target-service`,反序列化时即完成双向映射。

## (Q1.2) Dump 的方法调用链

以 Call 事件为例,调用 `KeyValueVisitor.Dump(entry)` 后:

+ `Dictionary<string, string> KeyValueVisitor.Dump(LogEntry entry)`
+ `TResult LogEntry.Accept<TResult>(ILogEntryVisitor<TResult> visitor)` —— 实际派发到 `CallLogEntry.Accept<TResult>`
+ `TResult CallLogEntry.Accept<TResult>(ILogEntryVisitor<TResult> visitor)`
+ `Dictionary<string, string> KeyValueVisitor.Visit(CallLogEntry entry)`

关键点:`Dump` 内只有一句 `entry.Accept(this)`。静态类型是 `LogEntry`,但 `Accept` 是虚方法(record 的 override),实际执行的是**运行时具体类型** `CallLogEntry.Accept`,它再回调 `visitor.Visit(this)`,此时 `this` 的静态类型已经是 `CallLogEntry`,于是重载决议选中 `Visit(CallLogEntry)` 重载。这就是访问者模式的"**双重分派**":第一次分派由 `Accept` 的虚机制完成(选具体 entry 类型),第二次由 `Visit` 的重载决议完成(选具体访问逻辑),数据结构与操作就此解耦。

## (Q1.3.b) AI 使用情况

本次作业我使用了 AI 辅助。

**提示词要点**:我向 AI 提供了仓库中 `LineParser.cs`、`LogEntries.cs`、`KeyValueVisitor.cs` 的完整源码与任务文档节选,要求"仿照 Call 类型的既有实现,补全 Request / Internal 两种日志的解析、Accept 与 Visit 实现,保持与 Call 完全一致的代码风格(JsonRequired record + switch 分派 + 同样的异常消息格式),不要改动无关代码"。

**AI 解答比我强的地方**:

1. **速度**。三种类型的解析在结构上是同构的,AI 数秒内就给出了与参考实现风格一致的完整代码;若我自己查 `System.Text.Json` 文档里的 `JsonNamingPolicy` 枚举、`JsonRequired` 特性的确切用法,至少多花半小时。
2. **API 记忆的准确性**。`JsonNamingPolicy.KebabCaseLower`、`[property: JsonRequired]` 这类 API 名字手写容易拼错或用成旧 API(如 ` camelCase` 策略),AI 一次给对。

**AI 不如我的地方**:

1. **契约细节**。AI 初版对 Internal 日志的 `ExceptionName: message` 切分用了 `Split(':')`,没有意识到异常信息本身可能含冒号——正确做法是 `IndexOf(": ")` 只切第一个分隔符。这个 bug 是我对照测试样例 `InternalLogExampleFailed`(缺失冒号+空格的样例必须抛异常)时发现并改正的。
2. **上下文一致性**。AI 生成的字典键(如 `"DurationMs"`)需要人工逐一对照讲义里的键表核对,它并不会主动去校验"文档约定了哪些键"这类跨文件契约。

结论:AI 适合干"同构复制 + API 检索"的活,而"跨文档契约核对 + 边界情况(分隔符歧义、失败样例)"仍必须靠人。
59 changes: 59 additions & 0 deletions docs/02-multithreading/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# 02-multithreading 报告

## 功能实现说明

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

用 `lock (_items)` + `Monitor` 条件变量实现生产者-消费者队列:

- `Enqueue`:持锁入队后 `Monitor.Pulse` 唤醒一个等待的消费者;`CompleteAdding` 之后再入队抛 `InvalidOperationException`;
- `TryDequeue`:持锁后 **while** 循环检查(而非 if,防虚假唤醒):队列空且未完成 → `Monitor.Wait` 挂起等待;队列空且已完成 → 返回 false;否则出队返回;
- `CompleteAdding`:置 `_isCompleted = true` 后 `Monitor.PulseAll` 唤醒**所有**等待者,让它们重新检查条件并退出。

T2.1.3 用 8 线程 × 50000 条数据验证了 FIFO 语义与线程安全(每个生产者的序列在消费端仍严格递增)。

### T2.2 LogFileAnalyzer(多线程并行分析)

- `AnalyzeFiles`:进入临界区校验文件名合法后置 `_isAnalyzing = true`(`finally` 中持锁复位,防异常泄漏状态);
- `RunWorkers`:持锁过滤出 `NotAnalyzed` 状态的文件(未知文件抛 `InvalidOperationException`)→ 全部入队后 `CompleteAdding` → 创建 `min(并行度, 文件数)` 个后台线程跑 `WorkerMain` → `Join` 等待全部结束;
- `WorkerMain`:循环 `TryDequeue` 领任务,`parser.Parse(reader).ToList()` 解析成功 → `Succeeded` 结果;任何异常被捕获 → `Failed` 结果(`ex.Message` 存入 `ErrorMessage`,不使 worker 崩溃);写回 `_analysisResults` 前持 `_syncRoot` 锁。

T2.2.5 验证了多线程对多文件分析的加速比。

### T2.3 LocalCli 控制台界面

实现了菜单 1-6 全部功能:列文件 / 指定文件分析 / 全部分析 / 查询结果(NotAnalyzed 提示未分析、Succeeded 用 `KeyValueVisitor.Dump` 逐条输出键值对、Failed 输出错误消息)/ 换目录 / 退出。所有用户输入路径都包了 try-catch(`ArgumentException`/`InvalidOperationException`),非法输入只提示不崩溃。

运行截图与鲁棒性测试截图见 `screenshots/` 目录(后续运行 LocalCli 后补充)。

## 问答题

### (Q2.1) 临界区与共享变量

**WorkQueue<T> 的共享变量**:`_items`(`Queue<T>`,队列本体)和 `_isCompleted`(完成标志)。二者都只在与 `_items` 互斥的临界区内读写——`Enqueue`/`TryDequeue`/`CompleteAdding`/`IsCompleted` 全部 `lock (_items)`。锁本身还兼任条件变量:消费者用 `Monitor.Wait(_items)` 挂起,生产者用 `Pulse`/`PulseAll` 唤醒,等待时自动放锁、被唤醒后重新竞争锁,因此不会死锁互斥量。

**LogFileAnalyzer 的共享变量**:`_currentDirectory`、`_isAnalyzing`、`_logFiles`、`_analysisResults` 四个字段,全部以 `_syncRoot`(私有 object)作互斥量保护。`AnalyzeAll` 先持锁拷贝文件名列表再放锁执行,`WorkerMain` 写结果时再短暂持锁,减小临界区粒度。

**if 改 while 的后果(虚假唤醒)**:无限容量生产者-消费者中,消费者伪码 `if (queue empty) wait(); dequeue();` 若被虚假唤醒(无人 signal 的情况下 `wait` 返回),消费者会**跳过重新检查**直接执行 `dequeue()`——此时队列可能仍为空,导致从空队列取元素(返回错误数据或抛异常);若用 `while (queue empty) wait();`,虚假唤醒后条件重新判定仍为真,再次进入等待,逻辑不受影响。这就是条件变量必须配合循环使用的原因。

### (Q2.2) 目录扫描代码

扫描全部 `.log` 文件的是 `ChangeDirectory` 中的:

```csharp
var logFiles = Directory.EnumerateFiles(directoryPath, "*.log", SearchOption.TopDirectoryOnly)
.Select(filePath => Path.GetFileName(filePath))
.OrderBy(fileName => fileName);
```

若要递归扫描所有子目录,把 `SearchOption.TopDirectoryOnly` 改为 `SearchOption.AllDirectories` 即可(`.NET 8+` 也可用 `EnumerationOptions` 控制忽略权限错误等细节)。

### (Q2.3.b) AI 使用情况

**提示词**:向 AI 提供了 WorkQueue/LogFileAnalyzer 的完整框架源码,要求"实现生产者-消费者语义的阻塞队列,TryDequeue 必须用 while 循环防虚假唤醒;RunWorkers 按 TODO 注释的语义补全,保持锁约定(_syncRoot 保护结果字典)"。

**使用方式**:介于"讲解框架"与"写部分代码"之间——锁与条件变量的语义是我先理解的,AI 主要负责把语义翻译成符合框架风格的 C# 代码。

**AI 的错误**:一次生成中 `TryDequeue` 用了 `if`,被我按 Q2.1 同样的虚假唤醒理由要求改成 `while`;另一次在 `RunWorkers` 过滤文件时漏了 `CompleteAdding()`(消费者会在空队列上永久 Wait),对照 T2.2 测试挂起超时的现象定位后补上。

**难度评价**:适中偏上。C# 的 `lock`/`Monitor` 语义与操作系统课的互斥量/条件变量一一对应,理论不新;难在并发 bug 不可复现,必须靠测试压力验证(T2.2.5 跑了 54 秒)。
Loading
Loading