多线程拨号C#版(探讨lock和Monitor线程同步)

0.需求

需求是这样的:实现多线程拨号,从数据中的批量读取需要拨打的号码,然后多线程拨号。拨号就是打电话给某一个号码,然后播放一段录音,自动化并非人工拨号。为什么要多线程拨号,如果一个一个拨号的话,一个电话3分钟,一个小时就打20个电话,太慢了,如果这里用10个线程拨号的话,那么20个电话6分钟就能搞定了。本文主要是探讨如何实现多线程拨号。

首先来思考一下实现流程:

首先从数据库中拿到需要拨打的号码,放在一个List中,每个号码加上一个标识,来标记这个号码是否已拨打。多线程每个线程都遍历这个List然后拿到标记是未拨打的号码来拨号,拨打完成后更改标记为已拨打。

1 第一个版本

好,开始写代码:

(号码列表应该从数据库中读取,拨号线程的数量应该写在配置文件中方便用户根据不同配置的计算机来设置线程数,如何读取数据库和配置文件不是本文重点,这里就直接写在main函数里了)

//号码对象,加上一个状态标记

classNumber

{

//电话号码

publicString PhoneNumber {set;get; }

//状态

public NumberState State { set; get; }

}

//枚举类型用来标识号码的状态

enumNumberState

{

notDial, //未拨号

finish //已完成

}

class Phone

{

//要拨打的号码列表

privateList<Number>numbers;

//线程数量

privateint threadCount = 0;

publicPhone(List<Number>numbers,int threadCount)

{

this.numbers= numbers;

this.threadCount= threadCount;

}

publicvoid run()

{

Console.WriteLine("拨号程序启动...");

//启动threadCount个线程来拨号

for(int i = 0; i < threadCount; i++)

{

//new一个线程,参数dial是函数名,也就是线程启动执行dial函数

Thread thread =newThread(dial);

//线程启动

thread.Start();

}

}

///<summary>

///这是一个拨号的函数

///</summary>

publicvoid dial()

{

//遍历列表

foreach(Number numberinnumbers)

{

//状态未拨号

if (number.State ==NumberState.notDial)

{

Console.WriteLine("[多线程]号码" + number.PhoneNumber +"开始拨号...");

Thread.Sleep(3000);//通话过程,这里用sleep来模拟

Console.WriteLine("[多线程]号码" + number.PhoneNumber +"拨号完成");

//设置状态已完成

number.State = NumberState.finish;

}

}

}

}

class Program

{

staticvoid Main(string[] args)

{

List<Number> numbers =newList<Number>();

//列表中添加号码

Numbernumber1 = new Number(){ PhoneNumber = "11111", State = NumberState.notDial };

numbers.Add(number1);

Numbernumber2 = new Number(){ PhoneNumber = "22222", State = NumberState.notDial };

numbers.Add(number2);

Numbernumber3 = new Number(){ PhoneNumber = "33333", State = NumberState.notDial };

numbers.Add(number3);

Numbernumber4 = new Number(){ PhoneNumber = "44444", State = NumberState.notDial };

numbers.Add(number4);

Numbernumber5 = new Number(){ PhoneNumber = "55555", State = NumberState.notDial };

numbers.Add(number5);

Numbernumber6 = new Number(){ PhoneNumber = "66666", State = NumberState.notDial };

numbers.Add(number6);

Numbernumber7 = new Number(){ PhoneNumber = "77777", State = NumberState.notDial };

numbers.Add(number7);

Numbernumber8 = new Number(){ PhoneNumber = "88888", State = NumberState.notDial };

numbers.Add(number8);

Numbernumber9 = new Number(){ PhoneNumber = "99999", State = NumberState.notDial };

numbers.Add(number9);

//启动拨号程序

//这里要传入两个参数,一个是号码列表,一个是线程数量(这里的线程数量应该是写在配置文件中可配置的,用户可以根据不同配置的计算机来配置线程数)

newPhone(numbers, 3).run();

Console.ReadKey();

}

}


到这里,也许你就会看出来一个问题了。一个线程正在拨未完成的号码尚未拨完,状态该没有来得及改成已完成,另一个线程遍历列表,发现这个未完成的号码,也开始拨号…这样就会出现多个线程同时拨打一个号码的情况。

多线程拨号C#版(探讨lock和Monitor线程同步)

此时就需要用到线程同步了。我们要控制每个号码只能有一个线程在拨打,也就是在拨打之前先给这个号码加锁防止别的线程再次拨打。


2 lock线程同步

说到线程同步,第一反应是关键字lock关键字。简单解释一下lock,其语法是这样的:

lock (加锁的对象)
{
 一段代码……
}

当一个线程对一个对象加锁以后,别的线程是不能再次加锁了,要想再次加锁,必须等待直到该线程解锁。大括号中的代码执行完成以后,会自动对其解锁。

本人在网上读过很多线程同步方面的文章,很多都有这么一种说法:这段代码在一个时刻内只可能被一个线程执行。实际这种说法是错误的。实际上lock并没有锁定这段代码,而是括号内的对象。也就是,这段代码可以被多个线程执行,但是这个对象不能被多个线程加锁,如果lock中换成不同的对象,这段代码是可以同时执行的。

针对本例,需要在拨打某个号码之前把一个Number对象加锁,结束修改状态之后,再解锁对象。这样就不会出现多个线程同时拨打一个号码的情况了。

修改上面的代码,在dial函数的foreach中加锁Number对象:

foreach(Number numberinnumbers)

{

//加锁号码对象

lock (number)

{

//状态未拨号

if (number.State ==NumberState.notDial)

{

Console.WriteLine("[多线程]号码" + number.PhoneNumber +"开始拨号...");

Thread.Sleep(3000);//通话过程,这里用sleep来模拟

Console.WriteLine("[多线程]号码" + number.PhoneNumber +"拨号完成");

//设置状态完成

number.State= NumberState.finish;

}

}

}

OK,再次运行…

多线程拨号C#版(探讨lock和Monitor线程同步)


什么,居然变成一个一个拨打了,这和单线程没什么区别了。

先停下来想想为什么会出现这种现象。

在这里,每个线程都是从上往下遍历一个list,拿到号码,然后加锁,然后再拨号。如果一个线程对某个号码已经加锁并在拨号,另一个线程遍历到这里的这里,还会试图加锁,于是就等啊等,终于等到前一个线程解锁了,它才加上了锁,但是人家都拨号完成了…

到这里我们就想,一个线程能不能在对号码加锁之前,先判断一下这个号码有没有被别的线程加锁。如果被其他线程加锁了,说明这个号码正在拨号,自动向下遍历…

3 Monitor

Monitor类可以实现类似lock的功能对对象进行加锁和解锁。Monitor相对lock更牛之处是它能判断一个对象是否已经被加锁了,返回true或false,即Monitor的静态函数TryEnter(obj)。

使用Monitor要注意一点,就是在使用完成后一定要释放锁。这一点不像lock那么方便(lock在程序块的大括号结束后自动释放锁)。释放锁的函数是Monitor.Exit(obj)。锁的释放代码要写在finally代码块中保证执行释放操作。

以下是比较规范的做法:

if (Monitor.TryEnter(number))

{

try

{

……

}

catch(..)

{

……

}

finally

{

Monitor.Exit(number);

}

回到多线程拨号了问题上来,再次改一下上面的代码:

foreach(Number numberinnumbers)

{

//首先,要查看这个对象是否被lock(正在拨号),如果没有则给它加锁,防止其他线程再对它进行拨号操作

if (Monitor.TryEnter(number))

{

try

{

//状态未拨号

if(number.State ==NumberState.notDial)

{

Console.WriteLine("[多线程]号码" + number.PhoneNumber +"开始拨号...");

Thread.Sleep(3000);//通话过程,这里用sleep来模拟

Console.WriteLine("[多线程]号码" + number.PhoneNumber +"拨号完成");

//设置状态完成

number.State = NumberState.finish;

}

}

finally

{

Monitor.Exit(number);

}

}

}

运行结果:

多线程拨号C#版(探讨lock和Monitor线程同步)


至此,我们想要的多线程拨号功能已经实现。


作者:叉叉哥 转载请注明出处:http://blog.****.net/xiao__gui/article/details/8018054