代码改变世界

C# 线程手册 第三章 使用线程 小心死锁

2012-02-11 22:14  DanielWise  阅读(6462)  评论(3编辑  收藏  举报

尽管使用线程同步对线程安全来说是必须的,但是如果没有用好的话就可能导致死锁。因此,理解什么是死锁并知道如何避免死锁是非常重要的。当两个或两个以上的线程等待两个或多于两个锁被释放然后程序中的逻辑导致锁永远都不会被释放时死锁就发生了。图3描述了一个典型的死锁场景。

Dl

图3

 

在上图中,线程1获得通过进入一个对象的关键区域获得这个对象的锁L1。在关键部分中线程1想要获取锁L2。线程2获得锁L2同时还想获得锁L1。所以,现在线程1无法获得锁L2而线程2无法获得锁L1,因为这两个线程彼此拥有对方需要的锁而又不会释放它们。结果是两个线程都进入无限等待或者死锁。

阻止潜在的死锁发生的最好的方式是避免在同一时间获取多个锁,这种情况不是经常发生。然而,如果必须这样做的话,你需要一种策略来保证你可以在一个稳定的、定义好的顺序获得多个锁。这取决于每个程序是如何设计的,不同的同步策略在避免死锁时可能很不同。没有一种方法可以用来避免所有类型的死锁。大多数时候,死锁不会被检测到除非程序被部署到一个大规模运行环境中。如果我们能够在我们程序的测试阶段就能发现死锁的话那么我们可以说是非常幸运的。当然这取于很多因素,开发人员水平,测试人员水平,开发/测试工具,经验等等。

一个很关键的但是经常被忽略的关于锁定策略部分的是文档。不幸的是,即使花费很多时间来设计避免死锁的同步策略,在记录这个过程的文档方面也仅仅做了很少的工作。至少,每个方法都应该有一个关于它是如何确定它要获得的锁以及描述方法内的关键部分的文档。

让我们来看一个例子,Deadlock.cs:

/*************************************
/* copyright (c) 2012 daniel dong
 * 
 * author:daniel dong
 * blog:  www.cnblogs.com/danielwise
 * email: guofoo@163.com
 * 
 */

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace Deadlock
{
    class DL
    {
        int field1 = 0;
        int field2 = 0;
        private object lock1 = new int[1];
        private object lock2 = new int[1];

        public void First(int val)
        {
            lock (lock1)
            {
                Console.WriteLine("First: Acquired lock 1: "
                    + Thread.CurrentThread.GetHashCode() + " Now Sleeping.");
                //Try commenting Thread.Sleep()
                Thread.Sleep(1000);
                Console.WriteLine("First: Acquired lock 1: "
                    + Thread.CurrentThread.GetHashCode() + " Now wants lock2.");

                lock (lock2)
                {
                    Console.WriteLine("First: Acquired lock 2: "
                        + Thread.CurrentThread.GetHashCode());
                    field1 = val;
                    field2 = val;
                }
            }
        }

        public void Second(int val)
        {
            lock (lock2)
            {
                Console.WriteLine("Second: Acquired lock 2: "
                    + Thread.CurrentThread.GetHashCode());

                lock (lock1)
                {
                    Console.WriteLine("Second: Acquired lock 1: "
                        + Thread.CurrentThread.GetHashCode());
                    field1 = val;
                    field2 = val;
                }
            }
        }
    }

    public class MainApp
    {
        DL d = new DL();

        public static void Main()
        {
            MainApp m = new MainApp();
            Thread t1 = new Thread(new ThreadStart(m.Run1));
            t1.Start();
            Thread t2 = new Thread(new ThreadStart(m.Run2));
            t2.Start();
            Console.ReadLine();
        }

        public void Run1()
        {
            this.d.First(10);
        }

        public void Run2()
        {
            this.d.Second(10);
        }
    }
}

Deadlock.cs 的输出结果如下:

Deadlock

在Deadlock.cs 中,线程t1调用First()方法获得lock1, 然后睡眠1秒钟。同时线程t2调用Second()方法并获得lock2. 然后在尝试在同一方法内获得lock1.但是线程1已经拥有lock1, 所以线程t2不得不等待线程1释放lock1. 当线程t1醒来后,它尝试获得lock2。而lock2已经被线程t2获得所以线程t1只能等待线程t2释放它。结果是发生了死锁而程序卡死。把方法First()中的Thread.Sleep()方法注释掉不会导致死锁,至少是个临时解决方案。因为线程t1会在线程t2之前获得lock2. 但是在真实场景中,Thread.Sleep()时发生的可能是一个连接数据库操作,导致线程t2在线程t1之前获得lock2, 最终结果也是死锁。这个例子告诉我们在任何多线程应用程序中设计出一个好的锁定方案是多么重要!一个好的锁定方案可能通过让所有线程运行在一个定义好的行为中以合作方式获得锁。在上面的例子中,线程t1不应该请求锁除非它被线程t2释放, 反之亦然。这些决定取决于特定应用场景而不具有一般性。测试各种锁定方案是同等重要的,因为死锁通常发生在那些缺少压力和功能性测试的系统中。

 

下一篇介绍两个关于线程安全的例子…