621 Mysql SslMode=Preferred;using释放;lock时序问题:MvvmToolkit自动绑定语法;属性里构造方法使用-不可变;锁的具体内容,以及不同锁;回调
Mysql安装



问题1
System.ArgumentException
HResult=0x80070057
Message=未找到请求的值“none”。
Source=mscorlib
StackTrace:
在 System.Enum.TryParseEnum(Type enumType, String value, Boolean ignoreCase, EnumResult& parseResult)
在 System.Enum.Parse(Type enumType, String value, Boolean ignoreCase)
在 MySql.Data.MySqlClient.MySqlBaseConnectionStringBuilder.<>c.<.cctor>b__1_2(MySqlBaseConnectionStringBuilder msb, MySqlConnectionStringOption sender, Object value)
在 System.Data.Common.DbConnectionStringBuilder.set_ConnectionString(String value)
在 MySql.Data.MySqlClient.MySqlConnectionStringBuilder…ctor(String connectionString, Boolean isAnalyzed)
在 MySql.Data.MySqlClient.MySqlConnection.set_ConnectionString(String value)
在 WpfApp6.Data.MySqlDetectionRepository.d__4.MoveNext() 在 D:\Download\WpfApp6 (2)\WpfApp6\WpfApp6\Data\MySqlDetectionRepository.cs 中: 第 68 行
版本问题,导致部分配置参数不同,修改App.config中SslMode=Preferred解决
问题2
此异常最初是在此调用堆栈中引发的:
[外部代码]
WpfApp6.Data.MySqlDetectionRepository.SaveAsync(WpfApp6.Models.DetectionResult) (位于 MySqlDetectionRepository.cs 中)
MySql.Data.MySqlClient.MySqlException
HResult=0x80004005
Message=Table ‘xraydetectiondb.detectionresults’ doesn’t exist
Source=MySql.Data
StackTrace:
在 MySql.Data.MySqlClient.MySqlStream.CheckForServerError()
在 MySql.Data.MySqlClient.MySqlStream.d__32.MoveNext()
在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
在 MySql.Data.MySqlClient.NativeDriver.d__58.MoveNext()
在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
在 MySql.Data.MySqlClient.Driver.d__102.MoveNext()
在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
在 MySql.Data.MySqlClient.Driver.d__100.MoveNext()
在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
在 MySql.Data.MySqlClient.MySqlDataReader.d__99.MoveNext()
在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
在 MySql.Data.MySqlClient.MySqlDataReader.d__99.MoveNext()
在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
在 MySql.Data.MySqlClient.MySqlCommand.d__115.MoveNext()
在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
在 MySql.Data.MySqlClient.MySqlCommand.d__115.MoveNext()
在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
在 MySql.Data.MySqlClient.MySqlCommand.d__115.MoveNext()
在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
在 MySql.Data.MySqlClient.MySqlCommand.d__100.MoveNext()
在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
在 System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
在 WpfApp6.Data.MySqlDetectionRepository.d__4.MoveNext() 在 D:\Download\WpfApp6 (2)\WpfApp6\WpfApp6\Data\MySqlDetectionRepository.cs 中: 第 88 行
此异常最初是在此调用堆栈中引发的:
[外部代码]
WpfApp6.Data.MySqlDetectionRepository.SaveAsync(WpfApp6.Models.DetectionResult) (位于 MySqlDetectionRepository.cs 中)
未建表 解决
问题3

下面数据库建立连接已使用using,会自己释放掉,这里直接删除
public void Dispose()
{
// throw new NotImplementedException();
}
项目整体大致流程
用户点"点位扫描"
↓
MainShellVM 发 ScanStartedMessage → 自己收(打日志 + 显示扫描中)
MainShellVM 发 StartScanMessage(TargetX, Y, Exp, ScanId)
↓
AxisEcatVM 收到 StartScanMessage
→ 模拟轴运动(MoveToAsync + 轮询)
→ 每200ms 定时器发 BusStatusMessage + ScanProgressMessage 更新UI
↓ 到位后
AxisEcatVM 发 AxisPositionReadyMessage(X, Y, ScanId)
↓
XrayImageVM 收到 AxisPositionReadyMessage
→ 50ms防抖
→ MockImageProvider.GetImage()
→ 显示在 Image 控件上
MainShellVM ──ScanStartedMessage──▶ MainShellVM (状态栏)
──ScanStartedMessage──▶ XrayImageVM (记录扫描ID)
──StartScanMessage────▶ AxisEcatVM (开始跑)
──StopScanMessage─────▶ AxisEcatVM (停止)
──StopScanMessage─────▶ XrayImageVM (清图)
──UpdateTargetMessage─▶ AxisEcatVM (中途改坐标)
AxisEcatVM ──AxisPositionReady──▶ XrayImageVM (显示图像)
──LogMessage─────────▶ MainShellVM (日志)
──BusStatusMessage───▶ MainShellVM (状态栏)
──ScanProgressMessage▶ MainShellVM (进度条)
问题4
lock时序问题 _currentScanId值变化情况
你的直觉是对的——lock 确实能防止同时读写,但 lock 只是管住同一时刻。
关键是两个 lock 块不是连在一起的,中间有时间差:
// 📦 第1个 lock 块:收到 ScanStartedMessage 时
lock (_debounceLock)
{
_currentScanId = "AAA"; ← 设成了 AAA
CancelAndDisposeCts(); ← 取消上次防抖
}
// ← 这里就解锁了!_currentScanId 暴露在外面
// ⏱️ 中间可能过几秒钟
// 用户又点了一次扫描
// _currentScanId 被改成了 "BBB"
// 📦 第2个 lock 块:收到 AxisPositionReadyMessage(属于"AAA"那次扫描)
lock (_debounceLock)
{
if (m.ScanId != _currentScanId) ← 此时读到的 _currentScanId 已经是 "BBB" 了
return; ← "AAA" != "BBB",丢掉!
}
lock 只管"同一时刻",不管"先后顺序":
时间线:
主轴线程 副线程(XrayImageVM 的消息)
────── ─────────────────────────
lock { _currentScanId = "AAA" } ← 第一次扫描
unlock
用户又点了一次扫描
MainShellVM 发 ScanStartedMessage("BBB")
lock { _currentScanId = "BBB" } ← 第二次扫描覆盖
unlock
第一次扫描的轴到位了
AxisEcatVM 发 AxisPositionReady("AAA")
lock {
_currentScanId 是 "BBB"
"AAA" != "BBB" → 丢掉
}
unlock
你的理解其实是对的。_currentScanId 确实就是被两次 lock 块改来改去的,只不过:
第1次 lock 改成 “AAA”
第1次 unlock(此时 “AAA” 暴露在外面)
第2次 lock 改成 “BBB”(覆盖了 “AAA”)
第2次 unlock
第3次 lock 来检查时,读到的是 “BBB”
lock 解决的是"两个人同时改"的问题,不是"一个人改完、另一个人读"的问题。 两次 lock { set } 之间没有锁保护,所以值当然可以变。
对,你的理解完全正确。就是时序问题。
我帮你把整个链条画清楚:
code复制
第一次点"点位扫描"
├── MainShellVM 发 ScanStartedMessage("AAA")
├── MainShellVM 发 StartScanMessage("AAA")
│
├── XrayImageVM: _currentScanId = "AAA" ← 存下
└── AxisEcatVM: _activeScanId = "AAA" ← 存下
└── RunScanTask 启动(轴开始走……)
第二次点"点位扫描" ← 用户又点了
├── MainShellVM 发 ScanStartedMessage("BBB")
├── MainShellVM 发 StartScanMessage("BBB")
│
├── XrayImageVM: _currentScanId = "BBB" ← 被覆盖了
└── AxisEcatVM: _activeScanId = "BBB" ← 被覆盖了
└── RunScanTask 启动(取消旧的,起新的)
旧的 RunScanTask("AAA") 可能还在跑
新的 RunScanTask("BBB") 也开始跑
第一次扫描的轴到位了
└── 旧的 RunScanTask("AAA") 完成 MoveToAsync
└── 发 AxisPositionReadyMessage("AAA")
XrayImageVM 收到
→ "AAA" != _currentScanId("BBB")
→ ❌ 丢掉!
第二次扫描的轴到位了
└── 新的 RunScanTask("BBB") 完成 MoveToAsync
└── 发 AxisPositionReadyMessage("BBB")
XrayImageVM 收到
→ "BBB" == _currentScanId("BBB")
→ ✅ 显示图像!
你刚才的细节我纠正一下
你说"XrayImage 的构造方法再订阅准备显示出图"——不是构造方法。XrayImageVM 在 MainWindow 构造函数里就 new 好了:
public MainWindow()
{
// ...
ImageVM = new XrayImageVM(); // ← 这里就注册了所有消息监听
// ...
}
它的订阅(Register)在构造函数里只执行一次,不是每次扫描都重新注册。所以:
XrayImageVM 只有一份实例,活了整个程序生命周期
↓
构造函数里 Register 了三个消息:
ScanStartedMessage → 更新 _currentScanId
AxisPositionReadyMessage → 过滤后显示图像
StopScanMessage → 清空
↓
每次扫描发来的消息,同一个人处理,靠 _currentScanId 区分"这是哪一次的"
你可以理解为:XrayImageVM 里有个人,拿着个牌子(_currentScanId),牌子上的字每点一次扫描就换一次,他只认牌子上写的那个 ScanId 的消息。
。
问题5
我没有显式的绑定:StartScanCommand=new RelayCommand(()=>StartScan) 只是用了MVVMToolKIT的自动绑定?就是StartScan头上加了[RelayCommand] 为啥编译器知道我的StartScanCommand 对应的是StartScan方法
这个问题问到 MVVM Toolkit 的源生成器核心了。简洁解释:
关键:这不是运行时反射,是编译时代码生成
你写这个:
private void StartScan()
{
// ...
}
编译器在背后帮你自动生成了这个(你看不到的代码):
// 编译器自动生成的 partial 类(你编译后在 obj 文件夹里能看到)
public partial class MainShellVM
{
public RelayCommand StartScanCommand
{
get
{
if (_startScanCommand == null)
_startScanCommand = new RelayCommand(StartScan); // ← 这就是你说的那行
return _startScanCommand;
}
}
private RelayCommand _startScanCommand;
}
为什么能自动生成? 三个条件缺一不可:
编译器看到这三样,就知道要在另一个 partial 文件里帮你补一个 StartScanCommand 属性。
命名规则
方法名 → 生成的命令名 → XAML 绑定
StartScan → StartScanCommand → {Binding MainVM.StartScanCommand}
StopScan → StopScanCommand → {Binding MainVM.StopScanCommand}
EmergencyStop → EmergencyStopCommand → {Binding MainVM.EmergencyStopCommand}
规则就是:方法名去掉首字母小写 + “Command”。
这也是为什么之前的代码里注释掉了手写的:
csharp复制
//public RelayCommand StartScanCommand { get; } ← 写了会报错,因为源生成器也会生成同名的
//public RelayCommand StopScanCommand { get; }
//public RelayCommand EmergencyStopCommand { get; }
如果手动写和源生成器生成同一个属性名,编译报错。 所以之前才注释掉了。
对比:之前你同事的写法(传统方式)vs 现在的
// 传统方式(没有源生成器)
public class MainShellVM
{
public ICommand StartScanCommand { get; }
public MainShellVM()
{
StartScanCommand = new RelayCommand(StartScan);
}
private void StartScan() { ... }
}
// 现在用 [RelayCommand] 的方式
public partial class MainShellVM : ObservableObject
{
private void StartScan() { ... }
// → StartScanCommand 自动生成了
}
效果完全一样,只是少写了几行模板代码。
问题6
public sealed class UpdateTargetMessage
{
public double NewTargetX { get; }
public string ScanId { get; }
public UpdateTargetMessage(double newTargetX,string scanId = null)
{
NewTargetX = newTargetX;
ScanId = scanId;
}
}
这个属性里为啥要放个构造方法,不直接用set
好问题!这涉及到消息类为什么要设计成不可变。
对比两种写法
你问的写法(可变的):
public sealed class UpdateTargetMessage
{
public double NewTargetX { get; set; } // ← 有 set
public string ScanId { get; set; }
// 构造函数可选,不必须
}
发消息时:
csharp复制
var msg = new UpdateTargetMessage();
msg.NewTargetX = 80;
msg.ScanId = _currentScanId;
WeakReferenceMessenger.Default.Send(msg);
现在项目里的写法(不可变的):
csharp复制
public sealed class UpdateTargetMessage
{
public double NewTargetX { get; } // ← 只有 get,没有 set
public string ScanId { get; }
public UpdateTargetMessage(double newTargetX, string scanId = null) // ← 必须通过构造函数赋值
{
NewTargetX = newTargetX;
ScanId = scanId;
}
}
发消息时:
WeakReferenceMessenger.Default.Send(new UpdateTargetMessage(TargetX, _currentScanId));
// 创建完就不能改了
为什么消息要设计成不可变
1️⃣ 线程安全
WeakReferenceMessenger 会把消息同时发给多个订阅者,可能在不同线程执行:
code复制MainShellVM 发消息
↓
AxisEcatVM 收到(后台线程)
XrayImageVM 收到(UI线程)
如果消息是可变的,AxisEcatVM 正在读的时候,XrayImageVM 把它改了——竞态条件(Race Condition),数据就乱了。
不可变对象:创建后只读,多线程同时读,绝对安全。
2️⃣ 消息代表"已经发生的事实"
code复制UpdateTargetMessage(80, “AAA”)
= “扫描AAA,目标改成了80”
这个事实发出去之后,就不应该再变了。
如果有人中途改成 NewTargetX = 999,
后面收到的订阅者看到的就是错误的数据。
3️⃣ sealed + 不可变 = 完整保护
csharp复制
public sealed class UpdateTargetMessage // ← sealed:不能被继承篡改
{
public double NewTargetX { get; } // ← 只有 get:不能改
}
现代 C# 的更简洁写法
如果是 C# 9.0+,可以用 record(天生不可变):
public sealed record UpdateTargetMessage(
double NewTargetX,
string? ScanId = null
);
// 一行搞定,属性默认只读,构造函数自动生成
你这个项目用的是传统写法,效果一样。
一句话总结: 消息类用构造函数 + 只读属性,是为了线程安全 + 语义正确(消息是事实,事实不能改)。
问题7
lock (_scanLock)锁啥
翻译:
private void StartScanInternal(double? targetX = null, double? targetY = null, int? exposureMs = null,bool force=false,string scanId=null)
{
//throw new NotImplementedException();
lock (_scanLock)
{
if (_isDisposed) return;
if (_runningScanTask != null && !_runningScanTask.IsCompleted )
{
if(!force) {
WeakReferenceMessenger.Default.Send(new LogMessage(“已有扫描任务运行,忽略新的请求”));
return;
}
try { _cts?.Cancel(); } catch { }
}
_activeScanId= scanId;
_cts = new CancellationTokenSource();
var token=_cts.Token;
double tx = targetX ?? TargetX;
double ty = targetY ?? TargetY;
_runningScanTask = RunScanTask(tx, ty, exposureMs, _cts.Token);
_runningScanTask.ContinueWith(t => {
if (t.IsFaulted)
{
var ex=t.Exception?.GetBaseException();
try { WeakReferenceMessenger.Default.Send(new LogMessage($“扫描任务异常:{ex?.Message}”)); }catch { }
}
},TaskScheduler.Default );
}
} lock (_scanLock)锁啥
lock (_scanLock) 保护的是三个共享字段在这段代码里的读写安全:
private readonly object _scanLock = new object(); // 专门的锁对象
// 三个被保护的字段
private CancellationTokenSource _cts; // ① 取消令牌
private Task _runningScanTask; // ② 当前运行的扫描任务
private string _activeScanId; // ③ 当前扫描ID
为什么需要锁——多线程同时访问
没有锁的话会发生这种事:
时间线(无锁的灾难):
线程A(Stop) 线程B(StartScanInternal)
──────────────── ────────────────────────
if (_runningScanTask != null) if (_runningScanTask != null)
→ true(还没判断完) → true(刚判断完)
↓ ↓
_cts?.Cancel() _cts?.Cancel() ← 两次取消同一个CTS
↓ ↓
_cts = new CTS() _runningScanTask = 新任务
↓ ↓
_runningScanTask = 新任务 _activeScanId = "BBB"
↓
_activeScanId = "AAA"
线程A: _activeScanId 设成 "AAA"
线程B: _activeScanId 设成 "BBB"
最后两个线程互相覆盖,_activeScanId 到底是谁?不知道!
锁保护的完整临界区
lock (_scanLock) // ← 进门前抢锁
{
// ★ 在这里面读/写 _runningScanTask / _cts / _activeScanId 都是安全的
if (_isDisposed) return;
if (_runningScanTask != null && !_runningScanTask.IsCompleted)
{
if (!force) { return; } // ← 挡住重复请求
_cts?.Cancel(); // ← 取消旧任务(只有一个线程能执行到这里)
}
_activeScanId = scanId; // ← 赋值(不会被其他线程同时改)
_cts = new CancellationTokenSource();
_runningScanTask = RunScanTask(...); // ← 启动新任务
}
// ← 出门时自动释放锁,其他线程可以进来
_scanLock 的作用:保证 _runningScanTask、_cts、_activeScanId 这三个共享字段,在"启动任务→运行中→停止/结束→清理"整个生命周期里,每次只有一个线程在操作它们。
重复取消_cts.Cancel()
_cts = new CancellationTokenSource(); // 创建一个 CTS
_cts.Cancel(); // 第1次取消:发送取消信号
_cts.Cancel(); // 第2次取消:再次调用
第1次 Cancel() → 遍历所有已注册 Token.Register() 的回调,一一执行
第2次 Cancel() → InvalidOperationException: Cancel has already been called on this object
时刻 T1:Stop() 线程进来
_cts?.Cancel(); // ← 第1次取消,_cts.Token.Register() 注册的回调被触发
// 回调是 RunScanTask 里的 await Task.Delay(200, token)
// → 抛出 OperationCanceledException
// → RunScanTask 里的 catch 捕获
↓
还没来得及赋值新 CTS
时刻 T2:StartScanInternal() 线程进来
if (_runningScanTask != null && !_runningScanTask.IsCompleted)
→ true(任务还在取消中)
↓
_cts?.Cancel(); // ← _cts 还是同一个对象!第2次调用
→ InvalidOperationException: Cancel has already been called
或者:
_cts = new CTS(); // ← 直接换新对象
// 但如果还没来得及换,Stop() 线程继续执行:
_runningScanTask = 新任务;
回调
回调是把一段代码当作参数传给另一个方法,让它在合适的时机被调用。
生活中的类比
你点外卖时跟服务员说:
“菜做好了叫我”(留了你的电话号码)
你不用站在厨房门口等
厨房做完菜 → 自动打你电话 → 你收到通知
这就是回调:你不在原地等,而是留个"通知我"的钩子,等事件发生时会自动触发。
你项目里的 Timer 回调
_statusTimer = new System.Threading.Timer(
RefreshAxisStatus, // ← 这是一个方法
null,
0,
200
);
这里把 RefreshAxisStatus(一整段代码)传给了 Timer。
Timer 内部大概是这样:
class Timer
{
private Action<object?> _callback; // 存着你传进来的方法
internal void Start()
{
while (true)
{
Thread.Sleep(200);
_callback(null); // ← 每200ms,自动调用你写的 RefreshAxisStatus()
}
}
}
另一个回调
_cts = new CancellationTokenSource();
Task.Delay(200, _cts.Token); // Task.Delay 内部藏着一个回调
Task.Delay内部
public static Task Delay(int ms, CancellationToken token)
{
var tcs = new TaskCompletionSource<bool>();
// 这里藏着一个回调:取消时执行
token.Register(() => // ← 你传进来一个 Action
{
tcs.TrySetCanceled(); // ← 取消时自动触发这个回调
});
return tcs.Task;
}
当调用 _cts.Cancel() 时,自动触发你传进去的那个回调。
回调的三种写法对比
// 委托变量
WeakReferenceMessenger.Default.Register<StartScanMessage>(this, (r, m) => { ... });
// ↑ Lambda
// System.Threading.Timer 的构造函数第二个参数也是回调
_statusTimer = new System.Threading.Timer(RefreshAxisStatus, null, 0, 200);
// ↑ 委托变量
回调 = 把"你要做的事情"包装成数据传进去,等时机到了由对方替你调用。
其他
Admin(无密码)@Admin MINGW64 ~
$ cd "/D/Download/WpfApp6 (2)/"
Admin(无密码)@Admin MINGW64 /D/Download/WpfApp6 (2)
$ cd wpfapp6
Admin(无密码)@Admin MINGW64 /D/Download/WpfApp6 (2)/wpfapp6 (master)
$ git add .
Admin(无密码)@Admin MINGW64 /D/Download/WpfApp6 (2)/wpfapp6 (master)
$ git commit -m "部分Bug修复"
[master ab357b0] 部分Bug修复
7 files changed, 189 insertions(+), 17 deletions(-)
delete mode 100644 .vs/WpfApp6/FileContentIndex/8396a965-6956-4e6c-963a-b7ddad38be4d.vsidx
Admin(无密码)@Admin MINGW64 /D/Download/WpfApp6 (2)/wpfapp6 (master)
$ git push origin master
Enumerating objects: 26, done.
Counting objects: 100% (26/26), done.
Delta compression using up to 12 threads
Compressing objects: 100% (14/14), done.
Writing objects: 100% (14/14), 8.05 KiB | 2.68 MiB/s, done.
Total 14 (delta 11), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Powered by GITEE.COM [1.1.23]
remote: Set trace flag b749cb95
To https://gitee.com/CZX1140075663/wpfapp6-test
0c771ac..ab357b0 master -> master
Admin(无密码)@Admin MINGW64 /D/Download/WpfApp6 (2)/wpfapp6 (master)
$ git add .
Admin(无密码)@Admin MINGW64 /D/Download/WpfApp6 (2)/wpfapp6 (master)
$ git commit -m "部分Bug修复"
[master 27c90d6] 部分Bug修复
4 files changed, 261 insertions(+), 83 deletions(-)
Admin(无密码)@Admin MINGW64 /D/Download/WpfApp6 (2)/wpfapp6 (master)
$ git push origin master
Enumerating objects: 19, done.
Counting objects: 100% (19/19), done.
Delta compression using up to 12 threads
Compressing objects: 100% (10/10), done.
Writing objects: 100% (10/10), 5.17 KiB | 1.29 MiB/s, done.
Total 10 (delta 7), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Powered by GITEE.COM [1.1.23]
remote: Set trace flag 204698a5
To https://gitee.com/CZX1140075663/wpfapp6-test
ab357b0..27c90d6 master -> master
Admin(无密码)@Admin MINGW64 /D/Download/WpfApp6 (2)/wpfapp6 (master)
$
测试项目逻辑流程还在整理中:
更多推荐

所有评论(0)