c# thread4——lock,死锁,以及monitor关键字

多线程的存在是提高系统效率,挖掘cpu性能的一种手段,那么控制它,能够协同多个线程不发生bug是关键。

首先我们来看一段不安全的多线程代码。

public abstract class CalculateBase
{
        public int count = 0;
        public object _lock = new object();
        public abstract void Operation();
}

public class NoSafeCalculate : CalculateBase
{
        public override void Operation()
        {
                count++;
                count--;
        }
}
static void Main(string[] args)
{
            CalculateBase calculate = new NoSafeCalculate();
            Thread thread1 = new Thread(() =>
            {
                for (int i = 0; i < 1000000; i++)
                {
                    calculate.Operation();
                }
            });
            Thread thread2 = new Thread(() =>
            {
                for (int i = 0; i < 1000000; i++)
                {
                    calculate.Operation();
                }
            });
            thread1.Start();
            thread2.Start();
            thread1.Join();
            thread2.Join();
            Console.WriteLine(calculate.count);
            Console.ReadLine();

}

很简单的一段代码,大致意思就是启动两个线程去加减一个变量,由于是一个实例化对象,所以变量也是同一个,按道理来说,变量自增一次自减一次,应该是不变的,在单线程中确实如此,但是在多线程中结果就会发生变化。

我们来执行一下。

发现结果并不是0,而是226.并且每次执行都是不一样的结果。

为什么会出现这种情况?

相信很多人都知道

1.++,--不是原子操作,举个例子a++其实是多步操作。1)计算a+1的值。2)将值赋值给a,那么多线程在执行这种操作的时候就很容易引起并发问题,结果不一致。(c#自带原子性操作函数)

2.值的不一致。比如我这边的count在执行++操作,但是还没有执行完,另外一个线程也执行到了++操作,结果两个++执行完之后,最终只是+了一次。

要解决这种多线程并发问题,如果不考虑性能,那么lock关键字将是很好的选择

我们来看看安全的代码

public abstract class CalculateBase
    {
        public int count = 0;
        public object _lock = new object();
        public abstract void Operation();
    }

    public class NoSafeCalculate : CalculateBase
    {
        public override void Operation()
        {
                count++;
                count--;
        }
    }
    public class SafeCalculate : CalculateBase
    {
        public override void Operation()
        {
            lock (_lock)
            {
                count++;
                count--;
            }
        }
    }
static void Main(string[] args)
        {
            CalculateBase calculate = new SafeCalculate();
            Thread thread1 = new Thread(() =>
            {
                for (int i = 0; i < 1000000; i++)
                {
                    calculate.Operation();
                }
            });
            Thread thread2 = new Thread(() =>
            {
                for (int i = 0; i < 1000000; i++)
                {
                    calculate.Operation();
                }
            });
            thread1.Start();
            thread2.Start();
            thread1.Join();
            thread2.Join();
            Console.WriteLine(calculate.count);
            Console.ReadLine();

        }

很显然,相较于不安全的代码我们只是多了一个lock的作用域,我们在Operation方法中添加了一个lock作用域将我们的自增和自减包裹起来,并且传入了一个object类型的_lock参数。

结果:0

 多次测试结果都是0,那么可以说线城是安全了,那么lock关键字的作用是什么。

c#中lock关键字等同于java中的synchionzed,意思为"同步",也就是说被它包裹的代码段都将以同步的方式进行,也就是同时以单线程执行。

那么它是如何做到的?其实就是传入的参数的作用了,我们看到传入了一个_lock参数,其实它就是一个"锁的钥匙",谁能拿到他谁就能打开锁,执行代码。所以每次有一个线程拿到锁后,其他的线程只能等待这个线程执行完然后将锁释放,其他线程才能够继续执行。

(附加:为什么用object类型作为锁的钥匙,string或者基本类型可以么?有什么区别)

何为死锁

线程死锁是指由于两个或者多个线程互相持有对方所需要的资源会互相等待对方释放资源,导致这些线程处于等待状态,无法前往执行。如果线程都不主动释放所占有的资源,将产生死锁。

我们来完成一个死锁的demo

public class DeadLock
    {
        public object _lock = new object();
        public object _lock1 = new object();
        public void Into()
        {
            lock (_lock)
            {
                Thread.Sleep(2000);
                lock (_lock1)
                {
                    Console.WriteLine("I am success come in");
                }
            }
        }
        public void Out()
        {
            lock (_lock1)
            {
                Thread.Sleep(1000);
                lock (_lock)
                {
                    Console.WriteLine("I am success go out");
                }
            }
        }
    }
static void Main(string[] args)
{
            DeadLock dead = new DeadLock();
            Thread thread1 = new Thread(() => { dead.Into(); });
            Thread thread2 = new Thread(() => { dead.Out(); });
            thread1.Start();
            thread2.Start();
            thread1.Join();
            thread2.Join();
            Console.WriteLine("执行结束");
}

运行代码:

 那基本上是这辈子看不到打印"执行结束"这四个字了。

其实上面的例子就是一个死锁的典型场景,一个线程拿住了A钥匙,打开了A锁,执行完后需要B钥匙,才能打开B锁,可是B钥匙永远也拿不到,因为另外一个线程正占用这B钥匙,在等待A锁的释放

那么怎么解决这种问题呢。

1.尽量用不同的对象作为锁的"钥匙"

2.使用c#提供的monitor关键字

我们将上面的代码进行修改。

public class DeadLock
{
        public object _lock = new object();
        public object _lock1 = new object();
        public void Into()
        {
            lock (_lock)
            {
                Thread.Sleep(2000);
                lock (_lock1)
                {
                    Console.WriteLine("I am success come in");
                }
            }
        }
        public void Out()
        {
            lock (_lock1)
            {
                Thread.Sleep(1000);
                if (Monitor.TryEnter(_lock, 5))
                {
                    Console.WriteLine("go out success");
                }
                else
                {
                    Console.WriteLine("timeout");
                }

            }
        }
}

此时我们再来看看运行结果:

 可以看到我们的程序死锁走出来了。

我们的monitor的作用就是会接受一个超时的参数,如果在时间内拿到锁,则返回true否则返回false,避免了死锁

最后补充下,其实lock也是Monitor的一个语法糖,分解lock的代码我们就能够了解。

public void demoLock()
{
            bool getlock = false;
            try
            {
                Monitor.Enter(_lock, ref getlock);
            }
            finally
            {
                if (getlock)//如果拿到锁则释放锁
                {
                    //业务
                    //...
                    Monitor.Exit(_lock);
                }
            }
}

转载:https://www.cnblogs.com/qwqwQAQ/p/11980498.html

原文地址:https://www.cnblogs.com/BluceLee/p/13847129.html