争用条件和死锁

Visual Basic .NET 或 Visual Basic 首次提供在 Visual Basic 应用程序中使用线程的功能。 线程引入了调试问题,如争用条件和死锁。 本文探讨这两个问题。

原始产品版本: Visual Basic、Visual Basic .NET
原始 KB 数: 317723

发生争用情况时

当两个线程同时访问共享变量时,会出现争用条件。 第一个线程读取变量,第二个线程从变量中读取相同的值。 然后,第一个线程和第二个线程对值执行操作,并争先查看哪个线程可以最后将值写入共享变量。 最后写入其值的线程值将保留,因为线程正在写入上一个线程所写入的值。

争用条件的详细信息和示例

每个线程分配一个预定义的时间段,用于在处理器上执行。 当为线程分配的时间过期时,线程的上下文将一直保存,直到其下一次打开处理器,处理器开始执行下一个线程。

单行命令如何导致争用条件

检查以下示例,了解争用条件的发生方式。 有两个线程,两个线程都在更新一个名为 total 的共享变量(这在程序集代码中表示 dword ptr ds:[031B49DCh] )。

  • 线程 1

    Total = Total + val1
    
  • 线程 2

    Total = Total - val2
    

前面 Visual Basic 代码的编译中的程序集代码(带有行号):

  • 线程 1

    1. mov eax,dword ptr ds:[031B49DCh]
    2. add eax,edi
    3. jno 00000033
    4. xor ecx,ecx
    5. call 7611097F
    6. mov dword ptr ds:[031B49DCh],eax
    
  • 线程 2

    1. mov eax,dword ptr ds:[031B49DCh]
    2. sub eax,edi
    3. jno 00000033
    4. xor ecx,ecx
    5. call 76110BE7
    6. mov dword ptr ds:[031B49DCh],eax
    

通过查看程序集代码,可以看到处理器在较低级别执行多少个操作来执行简单的加法计算。 线程可以在处理器上执行其所有或部分程序集代码。 现在看看此代码中出现争用条件的方式。

Total 为 100, val1 为 50,为 val2 15。 线程 1 有机会执行,但仅完成步骤 1 到 3。 这意味着 线程 1 读取变量并完成加法。 线程 1 现在正等待写出其新值 150。 停止线程 1线程 2 将完全执行。 这意味着它已将计算的值(85)写入变量 Total。 最后, 线程 1 重新获得控制权并完成执行。 它写出其值 (150)。 因此,当线程 1 完成时,值Total现在为 150 而不是 85。

你可以看到这可能是一个主要问题。 如果这是银行计划,客户将在其帐户中拥有不应存在的资金。

此错误是随机的,因为线程 1 可以在处理器过期之前完成其执行,然后 Thread 2 可以开始执行。 如果发生这些事件,则不会出现问题。 线程执行是不确定的,因此无法控制执行的时间或顺序。 另请注意,线程可能会在运行时与调试模式下以不同的方式执行。 此外,可以看到,如果在系列中执行每个线程,则不会发生错误。 这种随机性使得这些错误更难跟踪和调试。

为了防止争用条件发生,可以锁定共享变量,以便一次只有一个线程有权访问共享变量。 请谨慎执行此操作,因为如果线程 1Thread 2锁定了变量,线程 2 的执行也会停止,Thread 2 等待 Thread 1 释放变量。 (有关详细信息,请参阅 SyncLock 本文的 “参考” 部分。

争用条件的症状

争用条件的最常见症状是多个线程之间共享的变量的不可预知值。 这源于线程执行顺序的不可预测性。 有时一个线程会赢,另一个线程会赢。 在其他情况下,执行工作正常。 此外,如果单独执行每个线程,则变量值的行为正确。

发生死锁时

当两个线程同时锁定不同的变量,然后尝试锁定另一个线程已锁定的变量时,会发生死锁。 因此,每个线程停止执行,并等待其他线程释放变量。 由于每个线程都持有另一个线程想要的变量,因此不会发生任何操作,并且线程保持死锁。

死锁的详细信息和示例

以下代码具有两个对象, LeftVal 以及 RightVal

  • 线程 1

    SyncLock LeftVal
        SyncLock RightVal
            'Perform operations on LeftVal and RightVal that require read and write.
        End SyncLock
    End SyncLock
    
  • 线程 2

    SyncLock RightVal
        SyncLock LeftVal
            'Perform operations on RightVal and LeftVal that require read and write.
        End SyncLock
    End SyncLock
    

允许线程 1 锁定LeftVal发生死锁。 处理器停止 Thread 1 的执行,并开始执行 Thread 2线程 2RightVal ,然后尝试锁定 LeftVal。 由于 LeftVal 已锁定, 线程 2 将停止并等待 LeftVal 释放。 由于 线程 2 已停止, 因此允许线程 1 继续执行。 线程 1 尝试锁定但无法锁定 RightVal ,因为 Thread 2 已锁定它。 因此, Thread 1 开始等待,直到 RightVal 变为可用。 每个线程都等待另一个线程,因为每个线程都锁定了另一个线程正在等待的变量,并且两个线程都没有解锁它持有的变量。

死锁并不总是发生。 如果 Thread 1 在处理器停止前执行这两个锁, Thread 1 可以执行其操作,然后解锁共享变量。 线程 1 解锁变量后Thread 2 可以按预期继续执行。

当这些代码片段并排放置时,此错误似乎很明显,但在实践中,代码可能会出现在代码的单独模块或区域中。 这是一个很难跟踪的错误,因为,从同一代码中,可能会出现正确的执行和不正确的执行。

死锁的症状

死锁的常见症状是程序或线程组停止响应。 这也称为挂起。 至少有两个线程正在等待另一个线程锁定的变量。 线程不会继续,因为两个线程都不会释放其变量,直到它获取另一个变量。 如果程序正在等待其中一个或两个线程完成执行,则整个程序可能会挂起。

什么是线程

进程用于分隔在一台计算机上指定时间执行的不同应用程序。 操作系统不执行进程,但线程执行。 线程是一个执行单元。 操作系统将处理器时间分配给线程来执行线程的任务。 单个进程可以包含多个执行线程。 如果线程在分配给处理器期间无法完成其执行,则每个线程都维护自己的异常处理程序、计划优先级和一组操作系统用来保存线程上下文的结构。 上下文将一直保留到线程接收处理器时间的下一次。 上下文包括线程无缝继续执行所需的全部信息。 此信息包括线程的处理器寄存器集和主机进程的地址空间内的调用堆栈。

参考

有关详细信息,请在 Visual Studio 帮助中搜索以下关键字:

  • SyncLock。 允许锁定对象。 如果另一个线程尝试锁定同一对象,则会阻止该对象,直到第一个线程释放。 请谨慎使用 SyncLock ,因为问题可能会导致 SyncLock 滥用。 例如,此命令可以阻止争用条件,但会导致死锁。

  • InterLocked。 允许对基本数值变量执行一组线程安全操作。

有关详细信息,请参阅 线程和线程处理