内容纲要

lock关键字

使用lock关键字来确保当一个线程使用某些资源时,同时其他线程无法使用该资源.

class Counter
{
    private readonly object _syncRoot = new object();
    public int Count { get; private set; }

    public void Add()
    {
        lock (_syncRoot)
        {
            Count++;
        }
    }

    public void Sub()
    {
        lock (_syncRoot)
        {
            Count--;
        }
    }
}

var c = new Counter();
//创建多个线程来访问c
Thread t1 = new Thread(() => TestCounter(c));
Thread t2 = new Thread(() => TestCounter(c));
Thread t3 = new Thread(() => TestCounter(c));
Thread t4 = new Thread(() => TestCounter(c));
Thread t5 = new Thread(() => TestCounter(c));
t1.Start();
t2.Start();
t3.Start();
t4.Start();
t5.Start();
t1.Join();
t2.Join();
t3.Join();
t4.Join();
t5.Join();
Console.WriteLine("count:"+c.Count);

最后输出count:0,有人可能会说这不是很正常吗,那请自己动手把Counter类中lock部分都移除再试一遍,最后输出就会不一样了!~

变成这样:

public void Add()
{
    Count++;
}

public void Sub()
{
    Count--;
}

这是因为如果去掉lock部分,Counter类就不是线程安全的.
当多个线程同时访问counter对象时,

第一个线程得到的counter值是10并增加为11,
第二个线程得到的值是11并增加为12,
第一个线程得到counter值12,但递减操作发生前,
第二个线程得到counter值也是12,
第一个线程将12递减为11并存回counter中,
同时第二个线程进行了同样的操作.

结果我们进行了两次两次递增操作但只有第一递减操作,
这显然是不对的.
这种情形被称为竞争条件,竞争条件是多线程环境中导致错误的常见原因.

为了确保不会发生以上情形,必须保证当有线程操作counter对象时,所有其他线程必须等待直到当前线程完成操作.我们可以使用lock关键字来实现这种行为.如果锁定了一个对象,需要访问该对象的所有其他线程则会处于阻塞状态,并等待知道该对象解除锁定,但这可能会导致严重的性能问题.

使用Monitor类锁定资源

死锁也是多线程编程中常见的错误.由于死锁将导致程序停止工作,可以使用Monitor类来避免死锁.

class Program
{
    static void Main(string[] args)
    {
        object l1 = new object();
        object l2 = new object();

        new Thread(()=> LockObject(l1,l2)).Start();
        lock (l2)
        {
            Console.WriteLine("Monitor.TryEnter 可以避免死锁,在超时后返回false");
            Thread.Sleep(1000);
            if (Monitor.TryEnter(l1,TimeSpan.FromSeconds(5)))
            {
                Console.WriteLine("成功获取到一个受保护的资源!");
            }
            else
            {
                Console.WriteLine("获取资源超时!");
            }
        }

        Console.WriteLine("------------------------------------");

        new Thread(() => LockObject(l1, l2)).Start();
        lock (l2)
        {
            Console.WriteLine("这将是一个死锁!");
            Thread.Sleep(1000);
            lock (l1)
            {
                Console.WriteLine("成功获取到一个受保护的资源!");
            }
        }

        Console.WriteLine("End.");
        Console.ReadKey();
    }

    static void LockObject(object l1,object l2)
    {
        lock (l1)
        {
            Thread.Sleep(1000);
            lock (l2);
        }
    }
}

输出:(最后因为进入死锁,所以End.未打印)
输出(最后因为进入死锁,所以End.未打印)

处理异常

在线程中始终使用try/catch代码块是非常重要的,因为不可能在线程代码之外来捕获异常.

class Program
{
    static void Main(string[] args)
    {
        var t = new Thread(ThreadOne);
        t.Start();
        t.Join();

        try
        {
            t = new Thread(ThreadTwo);
            t.Start();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Main:{ex.Message}");
        }

    }

    static void ThreadOne()
    {
        try
        {
            Console.WriteLine("Starting a faulty thread...");
            Thread.Sleep(1000);
            throw new Exception("Boom!!!");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"ThreadOne:{ex.Message}");
        }
    }

    static void ThreadTwo()
    {
        Console.WriteLine("Starting a faulty thread...");
        Thread.Sleep(1000);
        throw new Exception("Boom!!!");
    }

}

输出:
输出

由上图可见ThreadOne的异常被捕获,但是ThreadTwo的异常未被捕获