项目中,下载游戏这块有一个需求,每一时刻正在下载的游戏个数不能超过5个,如果超过5个,则置为等待状态,这5个中的一个下载并安装完毕后,等待状态中的一个游戏开始下载。
由于这个项目是我中途入手,下载这块的代码是公司某大神写的,看了之后比较复杂,基本上“Task任务 + 多线程 + Lock锁 + 轮询机制”完成这个工作(WCF + UDP协议提供两个端之间的通信、广播支持),既然用到了这些个知识点,就先逐个击破,对于与Task对应的lock锁做如下浅显理解:
一、Lock是个啥?
lock
关键字将语句块标记为临界区,方法是获取给定对象的互斥锁,执行语句,然后释放该锁。(类似于我们在数据结构中学习过的PV操作和临界区,参考银行家算法)。
lock使用时的几点注意:
lock
关键字在块的开始处调用 Enter,而在块的结尾处调用 Exit。 ThreadInterruptedException 引发,如果 Interrupt 中断等待输入 lock
语句的线程。
通常,应避免锁定 public
类型,否则实例将超出代码的控制范围。 常见的结构 lock (this)
、lock (typeof (MyType))
和 lock ("myLock")
违反此准则:
(1) 如果实例可以被公共访问,将出现 lock (this)
问题。
(2)如果 MyType
可以被公共访问,将出现 lock (typeof (MyType))
问题。
(3)由于进程中使用同一字符串的任何其他代码都将共享同一个锁,所以出现 lock("myLock")
问题。
最佳做法是定义 private
对象来锁定, 或 private static
对象变量来保护所有实例所共有的数据,并且在lock正文中,不能使用thread.wait等等待关键字。
二、一个lock的demo
#region 使用了Lock锁
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("Negative Balance");
}
lock (thisLock)
{
if (balance >= amount)
{
Console.WriteLine("Balance before Withdrawal : " + balance);
Console.WriteLine("Amount to Withdraw : -" + amount);
balance = balance - amount;
Console.WriteLine("Balance after Withdrawal : " + balance);
return amount;
}
else
{
return 0;
}
}
}
public void DoTransactions()
{
for (int i = 0; i < 100; i++)
{
Withdraw(r.Next(1, 100));
}
}
}
class Test
{
static void Main()
{
Thread[] threads = new Thread[10];
Account acc = new Account(1000);
for (int i = 0; i < 10; i++)
{
Thread t = new Thread(new ThreadStart(acc.DoTransactions));
threads[i] = t;
}
for (int i = 0; i < 10; i++)
{
threads[i].Start();
}
Console.ReadKey();
}
}
#endregion
通过Task任务,启动10个线程,来调用Account的Withdraw方法,
一般情况下,10个线程几乎同时调用这个方法的时候,算钱这块,before draw , draw money,after draw就会出错,必须保证是一个线程完整的来调用这个方法之后,另一个线程再来调用,这样子Account的余额才不会算错,于是乎在这里,加上一个lock锁:
lock (thisLock)
{
if (balance >= amount)
{
Console.WriteLine("Balance before Withdrawal : " + balance);
Console.WriteLine("Amount to Withdraw : -" + amount);
balance = balance - amount;
Console.WriteLine("Balance after Withdrawal : " + balance);
return amount;
}
else
{
return 0; // transaction rejected
}
}
代码执行效果如图:
可以算一下,Account没有差错。
对于没有使用lock的情况:
可以看出,对于一套数据操作的顺序都是错的,更别说数据准确性了。
可以说lock这个操作,就像是高速路的收费站,如图:
各个线程就像是拥堵在告诉路出口的汽车,保证原子性、完整性的前提下,汽车一辆辆的从出口出来。这样子虽然会造成暂时的阻塞,但是对于数据的准确性则得到了保证,对于类似于秒杀、争夺有限的数据库资源的地方, 就可以使用lock方法来完成。
既然说到了这里,就不得不提一下Queue这个东东,队列也是在数据结构里学习过的,在.Net体系当中被称为Queue,最大的特征就是先进先出,码一段代码:
static void Main(string[] args)
{
Queue q = new Queue();
q.Enqueue('A');
q.Enqueue('B');
q.Enqueue('C');
q.Enqueue('D');
Console.WriteLine("当前队列: ");
foreach (var c in q)
Console.Write(c + " ");
Console.WriteLine();
q.Enqueue('E');
q.Enqueue('F');
Console.WriteLine("当前队列: ");
foreach (var c in q)
Console.Write(c + " ");
Console.WriteLine();
Console.WriteLine("移除队列中的几个值 ");
char ch = (char)q.Dequeue();
Console.WriteLine("移除的值为: {0}", ch);
ch = (char)q.Dequeue();
Console.WriteLine("移除的值为: {0}", ch);
Console.WriteLine();
foreach (char c in q)
Console.Write(c + " ");
Console.ReadKey();
}
最为常用的两个方法,
Queue.Enqueue()以及Queue.Dequeue(),分别是压入队列和弹出队列,如上,压入A、B、C、D、E、F之后,弹出了队列中的两个值,运行结果如图:
在项目中的应用,将日志写入到数据库日志表中时,为保证每条日志的先后顺序,操作核心代码为:
{
while (true)
{
MyLog log = null;
lock (SyncObj)
{
if (_logQueue.Count > 0)
log = _logQueue.Dequeue();
}
if (log != null)
{
try
{
Db_Log dbLog = new Db_Log()
{
Caption = log.Caption,
Msg = log.Msg,
Type = (int)log.Type,
Level = (int)log.Level,
Dt = ConvertDateTimeToInt(log.Dt)
};
DbSession.Insert(dbLog);
}
catch (Exception ex)
{
}
}
if (_logQueue.Count > 0)
continue;
autoEvent.WaitOne();
}
}
这里主要是通过
_
logQ
ueue的count值,不断循环进行弹出操作,直至_logQueue的count值为0.
对于Queue,如图:
至此,初步的lock以及queue完成,下一篇分析其在项目中下载游戏处的实际应用。