问题一:这里加lock是啥意思?它的原理是, 为什么可以锁住?
private readonly Timer _timer;/// <summary>/// 构造函数中初始化定时器/// </summary>public FtpTransferService(){// 初始化定时器_timer = new Timer(_intervalMinutes * 60 * 1000);_timer.Elapsed += OnTimerElapsed;//绑定定时触发的事件_timer.AutoReset = true;//定时器的属性,控制是否在每次间隔后重复触发事件}/// <summary>/// 定时器触发事件 - 增加锁防止并发/// </summary>private void OnTimerElapsed(object sender, ElapsedEventArgs e){lock (_lock){try{WriteLog($"开始执行文件转移任务({DateTime.Now:yyyy-MM-dd HH:mm:ss})");// 遍历所有目录(包括非最内层)并复制结构TransferAllDirectories(_sourceRootPath);WriteLog($"文件转移任务执行完成({DateTime.Now:yyyy-MM-dd HH:mm:ss})");}catch (Exception ex){WriteLog($"任务执行异常:{ex.Message}\r\n堆栈:{ex.StackTrace}", isError: true);}}}
在这段代码中,lock (_lock)
的作用是防止定时器事件被并发执行,确保 OnTimerElapsed
方法中的逻辑在同一时间只能被一个线程执行。
为什么需要加 lock
?
System.Timers.Timer
的 Elapsed
事件默认由线程池线程触发。如果定时任务(如代码中的 TransferAllDirectories
)的执行时间超过了定时器的间隔时间,就会出现以下问题:
- 第一个任务还没执行完,第二个定时器事件就被触发,导致两个线程同时执行
OnTimerElapsed
中的逻辑。 - 这可能引发资源竞争(如同时操作同一文件/目录),导致数据错乱或异常。
lock
的作用就是保证同一时间只有一个线程能进入被锁定的代码块,避免这种并发冲突。
lock
的工作原理
lock
本质上是基于 .NET 的** Monitor 同步机制**实现的,其核心逻辑可以简化为:
- 当线程进入
lock (_lock)
代码块时,会尝试获取_lock
对象的「独占锁」。 - 如果锁未被占用,当前线程会获取锁并执行代码块。
- 如果锁已被其他线程占用,当前线程会进入阻塞状态,等待锁被释放。
- 当线程执行完代码块后,会自动释放锁,让其他等待的线程可以获取锁并执行。
为什么用 _lock
这个对象?
lock
需要一个「同步对象」(这里是 _lock
),它的作用是:
- 作为锁的「标识」,不同的同步对象对应不同的锁。
- 必须是引用类型(如
object
),因为值类型会被装箱,每次lock
都会创建新对象,导致锁失效。 - 通常定义为
private readonly
,确保:- 外部代码无法访问该对象(避免外部误操作导致锁失控)。
- 对象引用不会被修改(保证始终用同一个对象作为锁标识)。
这段代码中 lock
的实际效果
假设定时器间隔为 5 分钟,但 TransferAllDirectories
某次执行花了 7 分钟:
- 第一个线程在第 0 分钟进入代码块,获取
_lock
锁。 - 第 5 分钟时,定时器再次触发
Elapsed
事件,第二个线程尝试进入代码块,但此时锁被第一个线程占用,只能等待。 - 第 7 分钟,第一个线程执行完并释放锁,第二个线程立即获取锁并开始执行。
这样就避免了两个线程同时执行文件转移逻辑,保证了操作的安全性。
注意点
lock
会导致等待的线程阻塞,因此被锁定的代码块应尽量简短,避免长时间占用锁影响性能。- 不要用
string
或this
作为同步对象:string
有字符串驻留机制,可能导致不同地方的锁冲突;this
可能被外部代码访问,存在安全风险。 - 此代码中
_timer.AutoReset = true
表示定时器会重复触发,结合lock
可以完美解决「任务执行时间超过间隔」的并发问题。