使用 Lock 语句的最佳实践
Lock 语句很容易被误用
在多线程 C# 应用程序中同步数据访问时,通常使用lock语句 — 部分原因是它的语法简单。但是,它的简单性和普遍性有一个缺点:人们很容易使用lock语句,而没有真正考虑它的作用以及它需要什么才能达到预期的效果。如果不深入研究本系列上一篇指南中提到的更复杂的线程协调形式,即使是lock语句的基本用法也有一些陷阱,每个 C# 开发人员都希望了解这些陷阱。让我们来看看有关lock语句的一些建议。了解这些最佳实践将使您在利用这个有用的 C# 关键字时更有信心。
锁定引用类型,而不是值类型
一开始你可能会犯的一个错误是尝试锁定引用值类型而不是对象类型的变量。例如:
static int myLocker;
static void WriteToFile()
{
lock (myLocker)
{
...
}
}
幸运的是,C# 编译器会以编译器错误的形式提醒您这一点,告诉您“'int' 不是 lock 语句所要求的引用类型”。不过您可能还记得,lock语句是带有Monitor.Enter和Monitor.Exit的try / finally语句的语法糖。因此,您可能还希望以下内容产生编译器错误:
static int myLocker;
static void WriteToFile()
{
Monitor.Enter(myLocker);
try
{
...
}
finally
{
Monitor.Exit(myLocker);
}
}
但事实并非如此,上面的代码编译得很好。然而在运行时,对Monitor.Exit的调用会抛出SynchronizationLockException。为什么?
虽然lock是一个特殊的 C# 关键字,允许编译器为您执行额外的检查,但Monitor.Enter和Monitor.Exit是普通的 .NET 方法,可以接受任何object类型的变量。C# 语言允许将值类型(例如整数、布尔值或结构)自动“装箱”(即包装)为 object 类型的引用类型,这使我们能够将值类型传递到许多 .NET 方法中。但是,每次需要装箱时,自动装箱都会创建一个新对象,因此每次调用Monitor方法时,对象都是不同的。因此,当Monitor.Exit尝试为包含myLocker的 box 对象查找锁时,它找不到任何锁。
如果您不明白上一段话的意思,请不要担心。通过始终锁定引用类型而不是值类型,可以避免所有相关问题。引用类型基本上是任何非值类型的东西;示例包括类和委托。为了让事情更简单,您可以简单地使用object myLocker = new object();实例化一个对象。事实上,这样做是一种常见的做法,我们将在下一节中看到。
避免锁定任何可公开访问的内容
您选择与lock语句一起使用的对象没有太多事情要做,因此锁定一些可用的预先存在的对象可能很有诱惑力。例如,假设您正在编写一个多线程网络爬虫控制台应用程序,并且您的应用程序有某种单例缓冲区,它代表您要写入文件的数据的缓冲区。
public class CustomBuffer { ... }
public static class Singletons
{
public static CustomBuffer LinksBuffer { get; private set; } = ...
}
将缓冲区写入文件时,需要同步对该文件的访问;因此,您决定使用lock。在这种情况下,锁定单例缓冲区对象可能很诱人,例如:
static void WriteLinksBufferToFile()
{
lock (Singletons.LinksBuffer)
{
...
}
}
现在,这可能最初有效,并且它的优点是您不必仅为 lock 创建单独的变量。但是,假设后来又有另一位开发人员在处理应用程序的完全不同的方面。这位开发人员可能不如您那么认真,因此当他们需要锁定时,他们会使用他们找到的第一件事。假设他们也选择锁定同一个LinksBuffer单例,即使他们同步的内容与链接无关。您能看出为什么这会导致潜在的问题吗?
由于第二位开发人员决定锁定同一个对象,我们现在有不相关的代码在等待对方。无意中引入了低效率(也许是难以排除故障的错误)。如果每个开发人员都创建自己的锁定对象,则可以轻松避免此类问题。
让我们考虑另一个更加阴险的例子。假设您完全摆脱了Singletons类,并在正在向文件写入链接的网络爬虫代码中实例化CustomBuffer 。您将CustomBuffer声明为private,因此您认为锁定CustomBuffer实例是安全的,因为没有其他代码可以访问它。我们还假设CustomBuffer类现在位于单独的程序集中,并且您无权访问源代码。而且,您不知道的是,在CustomBuffer中的某个地方有以下代码:
lock (this)
{
...
}
我们现在无意中遇到了与之前完全相同的问题:应用程序中不相关的区域正在使用同一对象进行锁定!这是因为 this实例是公开可访问的,至少对于声明者(在本例中为您的网络爬虫代码)而言。因此,您应该避免锁定this,尽管它具有几乎不可抗拒的便利性。此外,避免锁定任何执行锁定以外操作的对象。这样做是保证不会引入上述问题的唯一方法。有充分理由认为,锁定名为myLocker或类似名称的对象类型的专用私有变量是一种最佳实践。这样,对象的用法就明确了,您和其他开发人员将来不太可能意外滥用它。保持简单!强烈推荐并常用以下方法。仅将此类变量用于锁定。
private static object myLocker = new object();
检查锁块开头的状态
在处理多线程代码时,作为一名开发人员,很容易忘记某些事情发生的时间并不总是在我们的控制范围内。因此,在使用lock语句时,请记住,我们不知道特定线程在进入代码块之前会在lock语句处停留多长时间。如果线程 A 遇到lock语句时线程 B 拥有锁,则可能需要几秒甚至几分钟才能在线程 B 释放锁并允许线程 A 获取锁。
因此,开发人员经常需要在进入lock语句块后检查应用程序的状态。事实上,您可能需要重新评估在 lock 语句之前刚刚评估的内容。例如,考虑这样一种情况:您有一些初始化代码,无论哪个线程先到达那里,这些代码只需要发生一次。以下方法是不完整的:
static bool isInitialized;
static object initLock = new object();
static void InitializeIfNeeded()
{
if (!isInitialized)
{
lock (initLock)
{
// init code here
isInitialized = true;
}
}
}
虽然部分正确,但上述方法可能允许多次初始化,特别是如果初始化很长时。当第二个线程遇到锁时,一个线程可能正在主动进行初始化。正确的方法应该是这样的:
static bool isInitialized;
static object initLock = new object();
static void InitializeIfNeeded()
{
if (!isInitialized)
{
lock (initLock)
{
if (!isInitialized)
{
// init code here
isInitialized = true;
}
}
}
}
isInitialized的第二次检查看起来是重复的,几乎就像是打字错误。但这绝对是必要的,因为线程不知道在遇到lock语句和最终获得锁之间发生了什么。因此, isInitialized的第一次外部检查是一种优化;权威检查发生在锁内部。因此,再次强调,在进入lock语句的块后,始终要考虑是否需要检查应用程序的状态。通常,答案是肯定的。
避免过度锁定
使用lock语句时要注意的最后一个陷阱是,在不需要时使用它!从“CPU 周期数”的角度来看,锁定本身并不昂贵,但是,当您考虑到等待锁定的线程在等待时无法执行任何操作时,任何不必要的暂停都会显著影响应用程序工作的整体执行时间。请考虑以下示例:
static object myLocker = new object();
static ConcurrentDictionary<string, string> keyValueData = new ...
static void RemoveAllData()
{
lock (myLocker)
{
keyValueData.Clear();
}
}
在上面的例子中,我们的锁是多余的,因为ConcurrentDictionary有自己的代码来同步对其数据的访问。事实上,System.Collections.Concurrent命名空间中的任何集合都有确保对其数据的访问同步的机制。这样的集合被认为是所谓的“线程安全的”,因此您可以在多线程上下文中使用它们而不必担心竞争条件。因此,在访问线程安全类时,您不需要进行任何额外的锁定。当您第一次使用 .NET Framework 类时,最好查看文档以获取有关其线程安全性(或缺乏线程安全性)的信息,以了解是否需要同步对该类型对象的访问。这样做将有助于您的应用程序达到其最大性能。
工具箱中的一个工具
对于编写多线程应用程序的 C# 开发人员来说, lock语句是一种非常有用的工具。任何数量的异步编程都具有挑战性,因此最好使用lock语句的简单语法。但即使是最简单的工具也并非没有警告。通过遵循上述最佳实践,您将避免许多常见问题,同时让您的应用程序完全按照您的预期运行。
对于异步编程来说,lock语句绝不是 C# 开发人员可用的唯一工具。查看我们与异步编程相关的其他 C# 指南,了解其他一些信息!
免责声明:本内容来源于第三方作者授权、网友推荐或互联网整理,旨在为广大用户提供学习与参考之用。所有文本和图片版权归原创网站或作者本人所有,其观点并不代表本站立场。如有任何版权侵犯或转载不当之情况,请与我们取得联系,我们将尽快进行相关处理与修改。感谢您的理解与支持!
请先 登录后发表评论 ~