使用 Lock 语句同步数据访问
问题
如果多线程应用程序中的数据访问未同步,则可能会出现竞争条件。同步数据访问通常意味着一次只能有一个线程访问此类数据。但是,正如本系列第一篇指南中所探讨的那样,这可能比听起来更棘手。当涉及多个线程时,真正的同步涉及执行顺序。C # 中的lock语句是保证代码在多线程上下文中执行方式的最简单方法之一。
Lock 语句的实践
乍一看,lock语句的用法如下:
lock (foo)
{
...
}
让我们考虑一个例子,在上下文中查看lock语句。考虑一个多线程网络爬虫控制台应用程序的情况,您正在扫描 HTML 中的 URL 并将这些 URL 添加到文件中。有充分的理由,您希望确保在任何给定时间只有一个线程正在写入文件。我们之前发现以下方法不能提供我们所需的保证,因为我们仍然会遇到偶尔的IOException。
static bool fileIsInUse;
static void WriteToFile()
{
while (fileIsInUse)
{
System.Threading.Thread.Sleep(50);
}
try
{
fileIsInUse = true;
using (var fileStream = new FileStream("links.txt", ..., FileShare.None))
{
// write to the file stream
}
}
finally
{
fileIsInUse = false;
}
}
既然上面的方法不起作用,我们如何将lock语句应用于上面的示例,以实现必要的同步?实际上,使用lock语句相当容易,而且比我们上面尝试的更简单。对于这个例子,我们可以修改我们的代码如下:
static object linksLock = new object();
static void WriteToFile()
{
lock (linksLock)
{
using (var fileStream = new FileStream("links.txt", ..., FileShare.None))
{
// write to the file stream
}
}
}
就这些了!现在可以保证每次只有一个线程尝试创建此处指定的FileStream。如您所见,使用lock语句仅包含lock关键字、后面是括号中的变量以及与之配合的{ }块。您放入块中的任何代码都保证每次只由一个线程运行。这正是我们所需要的,这样网络爬虫就可以正常运行,而不会出现抛出IOException的危险。
因此,lock语句使用起来非常简单,但是它是如何工作的,以及传递给lock语句的括号中的变量有什么意义?
使用 Lock 语句的含义
了解代码中存在lock语句时会发生什么情况非常重要。您可能已经猜到了, lock语句代表某个线程获取锁(赋予该线程某些权限),然后在该线程退出块时释放锁(放弃所述权限)。设法进入lock语句块的线程可以独占访问块中的所有代码。这意味着,当另一个线程正在访问其块时,遇到lock语句的任何其他线程都必须等到另一个线程完成才能继续。
那么网络爬虫示例中的变量(比如 linksLock )怎么样呢? lock语句需要用括号括起来一个变量,因为它需要获取对象上的互斥锁(即 mutex)才能运行。指定对象的另一个好处是能够对多个块使用同一个锁。例如,假设在您的网络爬虫应用程序中,您有两个修改links.txt文件的方法:一个方法添加了一行文本,另一个方法删除了一行文本。这两个方法不仅需要独占访问数据,而且还需要访问相同的数据。因此,您不能允许AddLine和RemoveLine同时执行。对于这些情况,锁定同一个变量(如下所示)不仅是可以接受的,而且是必要的。
static object linksLock = new object();
static void AddLine()
{
lock (linksLock)
{
using (var fileStream = new FileStream("links.txt"...))
{
// add a line
}
}
}
static void RemoveLine()
{
lock (linksLock)
{
using (var fileStream = new FileStream("links.txt"...))
{
// remove a line
}
}
}
相反,您可能拥有不相关的数据,每个数据都需要同步访问,但彼此之间不需要同步。在这种情况下,您将需要使用单独的对象,以避免不必要的锁定。例如,假设您想在网络爬虫应用程序中维护两个文件,一个用于<a href="...(即“链接”)URL,一个用于<img src="...图像 URL。在写入图像文件时,无需等待对链接文件的访问,反之亦然。在这种情况下,您将使用两个对象进行锁定,一个用于链接,一个用于图像。例如:
static object linksLock = new object();
static void WriteToLinksFile()
{
lock (linksLock)
{
using (var fileStream = new FileStream("links.txt"...))
{
...
}
}
}
static object imagesLock = new object();
static void WriteToImagesFile()
{
lock (imagesLock)
{
using (var fileStream = new FileStream("images.txt"...))
{
...
}
}
}
Lock 语句的底层原理
虽然对于基本用法来说,了解lock语句的内部原理并不是必需的,但在高级场景中,更准确地了解lock语句的作用会很有帮助。例如,您可能会惊讶地发现,有一种方法(尽管很少使用)可以在lock语句的块内释放锁。理解这一点的关键是了解以下内容:
lock (myLockObject)
{
// your code
}
被 C# 编译器翻译为
bool lockAcquired = false;
try
{
Monitor.Enter(myLockObject, ref lockAcquired);
// your code
}
finally
{
if (lockAcquired)
{
Monitor.Exit(myLockObject);
}
}
Monitor类位于System.Threading命名空间中,您也可以使用它。事实上,如果您愿意,您可以直接使用Monitor.Enter和Monitor.Exit方法,而根本不使用lock。lock语句只是为了方便,就像 C# 的using语句一样。
那么,从Lock语句在后台使用Monitor类中我们学到了什么呢?首先,我们可以看到使用了try...finally,这意味着即使在您放入Lock语句块的代码中抛出了未处理的异常,锁也会被释放。其次,Monitor类还有其他方法可以与Enter和Exit结合使用,以实现线程之间更复杂的协调。例如,Monitor.Wait释放锁并立即阻塞,直到重新获得锁,而Monitor.Pulse本质上会唤醒这样的等待线程。例如,这些方法对于实现生产者/消费者队列很有用。
综上所述
lock语句是 C# 开发人员编写多线程应用程序时最简单、最常用的工具之一。它可用于同步对代码块的访问,通过一次只允许一个线程执行该块中的代码来实现线程安全。这在处理多个线程之间共享的变量、文件和其他数据时至关重要,因为不同步的数据访问会导致竞争条件。最后,由于lock语句是Monitor类中Enter/Exit方法的语法糖,因此lock还可以与其他Monitor方法结合使用,以实现更高级的线程协调形式。
您已经了解了开始使用lock语句所需的一切,但在使用它时可能会出现一些问题。本系列的下一篇指南将介绍使用lock语句时的最佳实践和避免常见错误。
免责声明:本内容来源于第三方作者授权、网友推荐或互联网整理,旨在为广大用户提供学习与参考之用。所有文本和图片版权归原创网站或作者本人所有,其观点并不代表本站立场。如有任何版权侵犯或转载不当之情况,请与我们取得联系,我们将尽快进行相关处理与修改。感谢您的理解与支持!
请先 登录后发表评论 ~