线程系列08,实现线程锁的各种方式,使用lock,Montor,Mutex,Semaphore以及线程死锁

当涉及到多线程共享数据,需要数据同步的时候,就可以考虑使用线程锁了。本篇体验线程锁的各种用法以及线程死锁。主要包括:

※ 使用lock处理数据同步
※ 使用Monitor.Enter和Monitor.Exit处理数据同步
※ 使用Mutex处理进程间数据同步
※ 使用Semaphore处理数据同步
※ 线程死锁

□ 使用lock处理数据同步

假设有一个类,主要用来计算该类2个字段的商,在计算商的方法之内让被除数自减,即被除数有可能为零。使用lock语句块保证每次只有一个线程进入该方法。

    class ThreadSafe
    {
        static readonly object o = new object();
        private static int _val1, _val2;

        public ThreadSafe(int val1, int val2)
        {
            _val1 = val1;
            _val2 = val2;
        }

        public void Calculate()
        {
            lock (o)
            {
                --_val2;
                if (_val2 != 0)
                {
                    Console.WriteLine(_val1/_val2);
                }
                else
                {
                    Console.WriteLine("_val2为零");
                }

            }
        }
    }


○ new object()创建的对象实例,也被称作同步对象
○ 同步对象必须是引用类型
○ 同步对象通常是私有的、静态的

客户端有一个静态字段val2被ThreadSafe的2个实例方法共用。

    class Program
    {
        private static int val2 = 2;
        static void Main(string[] args)
        {

            ThreadSafe ts1 = new ThreadSafe(2, val2);
            ThreadSafe ts2 = new ThreadSafe(2, val2);

            Thread[] threads = new Thread[2];
            threads[0] = new Thread(ts1.Calculate);
            threads[1] = new Thread(ts2.Calculate);

            threads[0].Start();
            threads[1].Start();
            Console.ReadKey();
        }
    }

○ 虽然ThreadSafe的2个实例方法共享了客户端静态字段val2,因为有了lock的存在,保证了val2的数据同步
○ 使用lock出现异常,需要手动处理

□ 使用Monitor.Enter和Monitor.Exit处理数据同步

把上面的Calculate方法修改为:

        public void Calculate()
        {
            Monitor.Enter(o);
            _val2--;
            try
            {
                if (_val2 != 0)
                {
                    Console.WriteLine(_val1 / _val2);
                }
                else
                {
                    Console.WriteLine("被除数为零");
                }
            }
            finally
            {
                Monitor.Exit(o);
            }

        }



○ 能得到相同的结果。      
○ lock其实是语法糖,其内部的实现逻辑就是Monitor.Enter和Monitor.Exit的实现逻辑

如果把Monitor.Exit注释掉,会发生什么呢?

        public void Calculate()
        {
            Monitor.Enter(o);
            _val2--;
            try
            {
                if (_val2 != 0)
                {
                    Console.WriteLine(_val1 / _val2);
                }
                else
                {
                    Console.WriteLine("被除数为零");
                }
            }
            finally
            {
                //Monitor.Exit(o);
            }
        }


可见,如果没有Monitor.Exit,会捕捉不到异常。

不过,以上代码还有一些不易察觉的、潜在的问题:如果在执行Monitor.Enter方法的时候出现异常,线程将拿不到锁;如果在Monitor.Enter与try之间出现异常,由于无法执行try...catch语句块,锁得不到释放。

为了解决以上问题, CLR 4.0给出了一个Monitor.Enter的重载方法。

public static void Enter (object obj, ref bool lockTaken);

现在,如果在执行Monitor.Enter方法的时候失败,即没有拿到锁,lockTaken就为false,finally语句块中无需释放锁;如果在Monitor.Enter之后出现异常,因为线程拿到了锁,lockTaken就为true,最后在finally语句块中释放锁。

所以,Calculate方法更健壮的写法为:

        public void Calculate()
        {
            bool lockTaken = false;
            _val2--;
            try
            {
                Monitor.Enter(o, ref lockTaken);
                if (_val2 != 0)
                {
                    Console.WriteLine(_val1 / _val2);
                }
                else
                {
                    Console.WriteLine("被除数为零");
                }
            }
            finally
            {
                if (lockTaken)
                {
                    Monitor.Exit(o);
                }
            }
        }

另外,Monitor还提供了多个静态方法TryEnter的重载,可以指定在某个时间段内获取锁。

□ 使用Mutex处理进程间数据同步

Mutex的作用和lock相似,不过与lock不同的是:Mutex可以跨进程实施线程锁。Mutex有2个重要的静态方法:

○ WaitOne:阻止当前线程,如果收到当前实例的信号,则为true,否则为false
○ ReleaseMutex:用来释放锁,只有获取锁的线程才可以使用该方法,与lock一样

Mutex一个经典应用就是:同一时间只能允许一个实例出现。

    class Program
    {
        static Mutex mutex = new Mutex(true,"darren.mutex");
        static void Main(string[] args)
        {
            if (!mutex.WaitOne(2000))//如果找到互拆体,即有另外一个相同的实例在运行着
            {
                Console.WriteLine("另外一个实例已经在运行着了~~");
                Console.ReadLine();
            }
            else//如果没有发现互拆体
            {
                try
                {
                    RunAnother();
                }
                finally
                {
                    mutex.ReleaseMutex();
                }
            }
        }

        static void RunAnother()
        {
            Console.WriteLine("我是模拟另外一个实例正在运行着~~不过可以按回车键退出");
            Console.ReadLine();
        }
    }


以上是分别2次双击应用程序后的结果。

□ 使用Semaphore处理数据同步

Semaphore可以被形象地看成是一个舞池,比如该舞池最多能容纳100人,超过100,都要在舞池外边排队等候进入。如果舞池中有一个人离开,在外面等候队列中排在最前面的那个人就可以进入舞池。

如果舞池的容量是1,这时候Semaphore就和Mutex与lock很像了。不过,与Mutex和lock不同的是,任何线程都可以释放Semaphore。

    class Program
    {
        static Semaphore _semaphore = new Semaphore(3,3);
        static void Main(string[] args)
        {
            Console.WriteLine("ladies and gentleman,舞会开始了~~");
            for (int i = 1; i <= 5; i++)
            {
                new Thread(IWannaDance).Start(i);
            }
        }

        static void IWannaDance(object id)
        {
            Console.WriteLine(id + "想跳舞");
            _semaphore.WaitOne();
            Console.WriteLine(id + "进了");
            Thread.Sleep(3000);
            Console.WriteLine(id + "准备离开舞池了");
            _semaphore.Release();
        }
    }



可见,舞池最多可容纳3人,超过3人都得排队。

□ 线程死锁

有2个线程:线程1和线程2。有2个资源,资源1和资源2。线程1已经拿到了资源1的锁,还想拿资源2的锁,线程2已经拿到了资源2的锁,同时还想拿资源1的锁。线程1和线程2都没有放弃自己的锁,还同时想要另外的锁,这就形成线程死锁。就像2个小孩,手上都有自己的玩具,却还想要对方的玩具,谁也不肯让谁。

举一个银行转账的例子来呈现线程死锁。

首先是银行账户,提供了存款和取款的方法。

    public class Account
    {
        private double _balance;
        private int _id;

        public Account(int id, double balance)
        {
            this._id = id;
            this._balance = balance;
        }

        public int ID
        {
            get { return _id; }
        }

        //取款
        public void Withdraw(double amount)
        {
            _balance -= amount;
        }

        //存款
        public void Deposit(double amount)
        {
            _balance += amount;
        }
    }


其次是用来转账的一个管理类。

    public class AccountManager
    {
        private Account _fromAccount;
        private Account _toAccount;
        private double _amountToTransfer;

        public AccountManager(Account fromAccount, Account toAccount, double amount)
        {
            this._fromAccount = fromAccount;
            this._toAccount = toAccount;
            this._amountToTransfer = _amountToTransfer;
        }

        //转账
        public void Transfer()
        {
            Console.WriteLine(Thread.CurrentThread.Name + "正在" + _fromAccount.ID.ToString() + "获取锁");
            lock (_fromAccount)
            {
                Console.WriteLine(Thread.CurrentThread.Name + "已经" + _fromAccount.ID.ToString() + "获取到锁");
                Console.WriteLine(Thread.CurrentThread.Name + "被阻塞1秒");
                //模拟处理时间
                Thread.Sleep(1000);
                Console.WriteLine(Thread.CurrentThread.Name + "醒了,想想获取" + _toAccount.ID.ToString() + "的锁");
                lock (_toAccount)
                {
                    Console.WriteLine("如果造成线程死锁,这里的代码就不执行了~~");
                    _fromAccount.Withdraw(_amountToTransfer);
                    _toAccount.Deposit(_amountToTransfer);
                }
            }
        }
    }


○ 使用了2个lock,称为"嵌套锁",当一个方法中调用另外的方法,通常使用"嵌套锁"
○ 第1个lock下的Thread.Sleep(1000)让线程阻塞1秒,好让另一个线程进来
○ 把"正在获取XX锁","已经获取到XX锁"......等状态,打印到控制台上

客户端开2个线程,一个线程账户A向账户B转账,另一个线程账户B向账户A转账。

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("准备转账了");
            Account accountA = new Account(1, 5000);
            Account accountB = new Account(2, 3000);

            AccountManager accountManagerA = new AccountManager(accountA, accountB, 1000);
            Thread threadA = new Thread(accountManagerA.Transfer);
            threadA.Name = "线程A";

            AccountManager accountManagerB = new AccountManager(accountB, accountA, 2000);
            Thread threadB = new Thread(accountManagerB.Transfer);
            threadB.Name = "线程B";

            threadA.Start();
            threadB.Start();

            threadA.Join();
            threadB.Join();
            Console.WriteLine("转账完成");
        }
    }


正如死锁的定义:线程A获取锁1,线程2获取锁2,线程A想获取锁2,同时线程B想获取锁1。结果:线程死锁。

○ 获取锁和释放锁的过程是相当快的,大概在几十纳秒的数量级
○ 线程锁能解决并发问题,但如果持有锁的时间过长,会增加线程死锁的可能

总结:
○ 同一进程内,在同一时间,只有一个线程获取锁,占用一个资源或一段代码,使用lock或Monitor.Enter/Monitor.Exit
○ 同一进程或不同进程内,在同一时间,只有一个线程获取锁,占用一个资源或一段代码,使用Mutex
○ 同一进程或不同进程内,在同一时间,规定有限的线程占有一个资源或一段代码,使用Semaphore
○ 使用线程锁的时候要注意造成线程死锁,当线程持有锁的时间过长,容易造成线程死锁

线程系列包括:

线程系列01,前台线程,后台线程,线程同步

线程系列02,多个线程同时处理一个耗时较长的任务以节省时间

线程系列03,多线程共享数据,多线程不共享数据

线程系列04,传递数据给线程,线程命名,线程异常处理,线程池

线程系列05,手动结束线程

线程系列06,通过CLR代码查看线程池及其线程

线程系列07,使用lock语句块或Interlocked类型方法保证自增变量的数据同步

线程系列08,实现线程锁的各种方式,使用lock,Montor,Mutex,Semaphore以及线程死锁

时间: 2024-10-11 01:18:05

线程系列08,实现线程锁的各种方式,使用lock,Montor,Mutex,Semaphore以及线程死锁的相关文章

线程系列09,线程的等待、通知,以及手动控制线程数量

当一个线程直到收到另一个线程的通知才执行相关的动作,这时候,就可以考虑使用"事件等待句柄(Event Wait Handles)".使用"事件等待句柄"主要用到3个类: AutoResetEvent, ManualResetEvent以及CountdownEvent(.NET 4.0以后才有).本篇包括: ※ 一个线程等待另一个线程的通知※ 2个线程互相通知等待※ 一个线程等待队列中的多个任务通知※ 手动控制线程的数量 □ 一个线程等待另一个线程的通知 最简单的情景

线程系列10,无需显式调用线程的情形

通常,我们会通过线程的构造函数先创建线程再使用线程.而实际上,.NET中有些类提供的方法,其内部就是使用多线程处理的.一些封装了多线程.异步处理方法的类都符合了"事件驱动异步模式(event-based asynchronous pattern)".以System.ComponentModel下的BackgroundWorker类来说,该类就符合这种模式. BackgroundWorker类属性:WorkerSupportsCancellation:设置为true表示允许取消Worke

线程同步机制之互斥锁

进程间通讯介绍 1.几种进程间的通信方式 # 管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用.进程的亲缘关系通常是指父子进程关系. # 有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信. # 信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问.它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源.因此,主要作为进程间以及同一进

Java并发编程系列-(4) 显式锁与AQS

4 显示锁和AQS 4.1 Lock接口 核心方法 Java在java.util.concurrent.locks包中提供了一系列的显示锁类,其中最基础的就是Lock接口,该接口提供了几个常见的锁相关的操作. public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit

线程系列02,多个线程同时处理一个耗时较长的任务以节省时间

当面对一个耗时较长的任务时,我们可以把这个任务切分成多个部分,然后同时交给多个线程处理. □ 统计字节数组一个比较耗时的方式 以下来统计一个字节数组的大小. class Program { static byte[] values = new byte[500000000]; static void Main(string[] args) { GenerateByteArray(); Console.WriteLine("正在统计字节数"); Stopwatch watch = new

线程同步机制之互斥锁通信机制

#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <string.h> void *thread_function(void *arg); pthread_mutex_t work_mutex; #define WORK_SIZE 1024 char work_area[WORK_SIZE]; int time_to_exit=0; int main(int argc,

第9章 线程编程(4)_线程同步1:互斥锁

5. 线程的互斥和同步 5.1 同步和互斥的概念 (1)线程同步:是一个宏观概念,在微观上包含线程的相互排斥和线程的先后执行的约束问题.解决同步方式一般采用条件变量和信号量. (2)线程互斥:线程执行的相互排斥(注意,它不关心线程间执行的先后顺序!).解决互斥一般使用互斥锁.读写锁和信号量. [编程实验]银行ATM(线程不安全的例子) //account.h #ifndef __ACCOUNT_H__ #define __ACCOUNT_H__ typedef struct { int code

【Java线程】锁机制:synchronized、Lock、Condition

http://www.infoq.com/cn/articles/java-memory-model-5  深入理解Java内存模型(五)——锁 http://www.ibm.com/developerworks/cn/java/j-jtp10264/  Java 理论与实践: JDK 5.0 中更灵活.更具可伸缩性的锁定机制 http://blog.csdn.net/ghsau/article/details/7481142 1.synchronized 把代码块声明为 synchronize

线程同步:何时互斥锁不够,还需要条件变量?

http://www.blogjava.net/fhtdy2004/archive/2009/07/05/285519.html 线程同步:何时互斥锁不够,还需要条件变量? 很显然,pthread中的条件变量与Java中的wait,notify类似 假设有共享的资源sum,与之相关联的mutex 是lock_s.假设每个线程对sum的操作很简单的,与sum的状态无关,比如只是sum++.那么只用mutex足够了.程序员只要确保每个线程操作前,取得lock,然后sum++,再unlock即可.每个