在并发的环境里,“线程同步锁”可以保护共享数据,但是也会存在一些问题:
1) 实现比较繁琐,而且容易错漏。你必须标识出可能由多个线程访问的所有共享数据。然后,必须为其获取和释放一个线程同步琐,并且保证已经正确为所有共享资源添加了锁定代码。
2) 由于临界区无法并发运行,进入临界区就需要等待,加锁带来效率的降低。
3) 在复杂的情况下,很容易造成死锁,并发实体之间无止境的互相等待。
4) 优先级倒置造成实时系统不能正常工作。优先级低的进程拿到高优先级进程需要的锁,结果是高/低优先级的进程都无法运行,中等优先级的进程可能在狂跑。
5) 当线程池中一个线程被阻塞时,可能造成线程池根据CPU使用情况误判创建更多的线程以便执行其他任务,然而新创建的线程也可能因请求的共享资源而被阻塞,恶性循环,徒增线程上下文切换的次数,并且降低了程序的伸缩性。(这一点很重要)
1.临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。(临界区可以认为是操作共享资源的一段代码)
2.互斥量:为协调共同对一个共享资源的单独访问而设计的。
3.信号量:为控制一个具有有限数量用户资源而设计。
4.事 件:用来通知线程有一些事件已发生,从而启动后继任务的开始
system.threading.monitor.enter(x); try { ... } finally
{ system.threading.monitor.exit(x); }
(c)自定义类推荐用私有的只读静态对象,比如:private static readonly object obj = new object();为什么要设置成只读的呢?这时因为如果在lock代码段中改变obj的值,其它线程就畅通无阻了,因为互斥锁的对象变了,object.referenceequals必然返回false。(推荐的方式)
(a)为什么不能lock值类型,比如lock(1)呢?lock本质上monitor.enter,monitor.enter会使值类型装箱,每次lock的是装箱后的对象。lock其实是类似编译器的语法糖,因此编译器直接限制住不能lock值类型。
(b)退一万步说,就算能编译器允许你lock(1),但是object.referenceequals(1,1)始终返回false(因为每次装箱后都是不同对象),也就是说每次都会判断成未申请互斥锁,这样在同一时间,别的线程照样能够访问里面的代码,达不到同步的效果。同理lock((object)1)也不行。
(c)那么lock("xxx")字符串呢?msdn上的原话是:锁定字符串尤其危险,因为字符串被公共语言运行库 (clr)“暂留”。 这意味着整个程序中任何给定字符串都只有一个实例,就是这同一个对象表示了所有运行的应用程序域的所有线程中的该文本。因此,只要在应用程序进程中的任何位置处具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。
(d)通常,最好避免锁定 public 类型或锁定不受应用程序控制的对象实例。例如,如果该实例可以被公开访问,则 lock(this) 可能会有问题,因为不受控制的代码也可能会锁定该对象。这可能导致死锁,即两个或更多个线程等待释放同一对象。出于同样的原因,锁定公共数据类型(相比于对象)也可能导致问题。而且lock(this)只对当前对象有效,如果多个对象之间就达不到同步的效果。
(e)lock(typeof(class))与锁定字符串一样,范围太广了。
(1.4)示例
/* 该实例是一个线程中lock用法的经典实例,使得到的balance不会为负数 同时初始化十个线程,启动十个,但由于加锁,能够启动调用WithDraw方法的可能只能是其中几个 */ using System; namespace ThreadTest29 { class Account { private Object thisLock = new object();//设置锁对象 int balance; Random r = new Random(); public Account(int initial) { balance = initial; } int WithDraw(int amount) { if (balance < 0) { throw new Exception("负的Balance."); } //确保只有一个线程使用资源,一个进入临界状态,使用对象互斥锁,10个启动了的线程不能全部执行该方法 lock (thisLock) { if (balance >= amount) { Console.WriteLine("----------------------------:" + System.Threading.Thread.CurrentThread.Name + "---------------"); Console.WriteLine("调用Withdrawal之前的Balance:" + balance); Console.WriteLine("把Amount输入 Withdrawal :-" + amount); //如果没有加对象互斥锁,则可能10个线程都执行下面的减法,加减法所耗时间片段非常小,可能多个线程同时执行,出现负数。 balance = balance - amount; Console.WriteLine("调用Withdrawal之后的Balance :" + balance); return amount; } else { //最终结果 return 0; } } } public void DoTransactions() { for (int i = 0; i < 100; i++) { //生成balance的被减数amount的随机数 WithDraw(r.Next(1, 100)); } } } class Test { static void Main(string[] args) { //初始化10个线程 System.Threading.Thread[] threads = new System.Threading.Thread[10]; //把balance初始化设定为1000 Account acc = new Account(1000); for (int i = 0; i < 10; i++) { System.Threading.Thread t = new System.Threading.Thread(new System.Threading.ThreadStart(acc.DoTransactions)); threads[i] = t; threads[i].Name = "Thread" + i.ToString(); } for (int i = 0; i < 10; i++) { threads[i].Start(); } Console.ReadKey(); } } }
(2)Monitor类
这个算是实现锁机制的纯正类,在锁定的临界区中只允许让一个线程访问,其他线程排队等待。主要整理为2组方法。
(2.1)Monitor.Enter和Monitor.Exit
微软很照护我们,给了我们语法糖Lock,对的,语言糖确实减少了我们不必要的劳动并且让代码更可观,但是如果我们要精细的 控制,则必须使用原生类,这里要注意一个问题就是“锁住什么”的问题,一般情况下我们锁住的都是静态对象,我们知道静态对象属于类级别,当有很多线程共同访问的时候,那个静态对象对多个线程来说是一个,不像实例字段会被认为是多个。Monitor 锁定对象是引用类型,而非值类型,该对象用来定义锁的范围,与lock一样,毕竟lock是monitor的语法糖。
class Program { static void Main(string[] args) { for (int i = 0; i < 10; i++) { Thread t = new Thread(Run); t.Start(); } } //资源 static object obj = new object(); static int count = 0; static void Run() { Thread.Sleep(10); //进入临界区 Monitor.Enter(obj); Console.WriteLine("当前数字:{0}", ++count); //退出临界区 Monitor.Exit(obj); } }
(2.2)Monitor.Wait和Monitor.Pulse
首先这两个方法是成对出现,通常使用在Enter,Exit之间。 Wait: 暂时的释放资源锁,然后该线程进入”等待队列“中,那么自然别的线程就能获取到资源锁。 Pulse: 唤醒“等待队列”中的线程,那么当时被Wait的线程就重新获取到了锁。
这里我们是否注意到了两点:① 可能A线程进入到临界区后,需要B线程做一些初始化操作,然后A线程继续干剩下的事情。② 用上面的两个方法,我们可以实现线程间的彼此通信。
using System; using System.Collections.Generic; using System.Text; using System.Threading; namespace Test { public class Program { public static void Main(string[] args) { LockObj obj = new LockObj(); //注意,这里使用的是同一个资源对象obj Jack jack = new Jack(obj); John john = new John(obj); Thread t1 = new Thread(new ThreadStart(jack.Run)); Thread t2 = new Thread(new ThreadStart(john.Run)); t1.Start(); t1.Name = "Jack"; t2.Start(); t2.Name = "John"; Console.ReadLine(); } } //锁定对象 public class LockObj { } public class Jack { private LockObj obj; public Jack(LockObj obj) { this.obj = obj; } public void Run() { Monitor.Enter(this.obj); Console.WriteLine("{0}:我已进入茅厕。", Thread.CurrentThread.Name); Console.WriteLine("{0}:擦,太臭了,我还是撤!", Thread.CurrentThread.Name); //暂时的释放锁资源 Monitor.Wait(this.obj); Console.WriteLine("{0}:兄弟说的对,我还是进去吧。", Thread.CurrentThread.Name); //唤醒等待队列中的线程 Monitor.Pulse(this.obj); Console.WriteLine("{0}:拉完了,真舒服。", Thread.CurrentThread.Name); Monitor.Exit(this.obj); } } public class John { private LockObj obj; public John(LockObj obj) { this.obj = obj; } public void Run() { Monitor.Enter(this.obj); Console.WriteLine("{0}:直奔茅厕,兄弟,你还是进来吧,小心憋坏了!", Thread.CurrentThread.Name); //唤醒等待队列中的线程 Monitor.Pulse(this.obj); Console.WriteLine("{0}:哗啦啦....", Thread.CurrentThread.Name); //暂时的释放锁资源 Monitor.Wait(this.obj); Console.WriteLine("{0}:拉完了,真舒服。", Thread.CurrentThread.Name); Monitor.Exit(this.obj); } } }
(3)ReaderWriteLock类
先前也知道,Monitor实现的是在读写两种情况的临界区中只可以让一个线程访问,那么如果业务中存在”读取密集型“操作,就好比数据库一样,读取的操作永远比写入的操作多。针对这种情况,我们使用Monitor的话很吃亏,不过没关系,ReadWriterLock就很牛X,因为实现了”写入串行“,”读取并行“。
ReaderWriteLock中主要用3组方法:
<1> AcquireWriterLock: 获取写入锁。
ReleaseWriterLock:释放写入锁。
<2> AcquireReaderLock: 获取读锁。
ReleaseReaderLock:释放读锁。
<3> UpgradeToWriterLock:将读锁转为写锁。
DowngradeFromWriterLock:将写锁还原为读锁。
下面就实现一个写操作,三个读操作,要知道这三个读操作是并发的。
namespace Test { class Program { static List<int> list = new List<int>(); static ReaderWriterLock rw = new System.Threading.ReaderWriterLock(); static void Main(string[] args) { Thread t1 = new Thread(AutoAddFunc); Thread t2 = new Thread(AutoReadFunc); t1.Start(); t2.Start(); Console.Read(); } /// <summary> /// 模拟3s插入一次 /// </summary> /// <param name="num"></param> public static void AutoAddFunc() { //3000ms插入一次 Timer timer1 = new Timer(new TimerCallback(Add), null, 0, 3000); } public static void AutoReadFunc() { //1000ms自动读取一次 Timer timer1 = new Timer(new TimerCallback(Read), null, 0, 1000); Timer timer2 = new Timer(new TimerCallback(Read), null, 0, 1000); Timer timer3 = new Timer(new TimerCallback(Read), null, 0, 1000); } public static void Add(object obj) { var num = new Random().Next(0, 1000); //写锁 rw.AcquireWriterLock(TimeSpan.FromSeconds(30)); list.Add(num); Console.WriteLine("我是线程{0},我插入的数据是{1}。", Thread.CurrentThread.ManagedThreadId, num); //释放锁 rw.ReleaseWriterLock(); } public static void Read(object obj) { //读锁 rw.AcquireReaderLock(TimeSpan.FromSeconds(30)); Console.WriteLine("我是线程{0},我读取的集合为:{1}", Thread.CurrentThread.ManagedThreadId, string.Join(",", list)); //释放锁 rw.ReleaseReaderLock(); } } }
(4)标志量: 顾名思义,标志量就是声明一个布尔型变量,用来标示某些方法的执行状态。
下一篇博文中来介绍互斥量实现线程同步。。。。