代码优化分多个方面,而性能是主要的优化目标。本文将从对象缓存、DOM操作等12个方面讲解如何优化jQuery代码的执行效率,从而总体提升代码性能。

1、总是从ID选择器开始继承

在jQuery中最快的选择器是ID选择器,因为它直接来自 于JavaScript的getElementById()方法。
例如有一段HTML代码:
<div id=“content”>
<form method=“post” action=“#”>
<h2>交通信号灯</h2>
<ul id=“traffic_light”>
<li><input type=“radio” class=“on” name=“light” value=“red” /> 红色</li>
<li><input type=“radio” class=“off” name=“light” value=“yellow” /> 黄色</li>
<li><input type=“radio” class=“off” name=“light” value=“green” /> 绿色</li>
</ul>
<input class=“button” id=“traffic_button” type=“submit” value=“Go” />
</form>
</div>

如果采用下面的选择器,那么效率是低效的。
var traffic_button = $(“#content .button”);

因为button已经有ID了,我们可以直接使用ID选择器。如下所示:
var traffic_button = $(“#traffic_button”);

当然 这只是对于单一的元素来讲。如果你需要选择多个元素,这必然会涉及到 DOM遍历和循环,为了提高性能,建议从最近的ID开始继承。

如下所示:
var traffic_lights = $(“#traffic_light input”);

2、在class前使用tag(标签名)

在jQuery中第二快的选择器是tag(标签)选择器比 如:
$(“head”)

跟ID选择器累时,因为它来自原生的getElementsByTagName() 方法。继续看刚才那段HTML代码:
<div id=“content”>
<form method=“post” action=“#”>
<h2>交通信号灯</h2>
<ul id=“traffic_light”>
<li><input type=“radio” class=“on” name=“light” value=“red” /> 红色</li>
<li><input type=“radio” class=“off” name=“light” value=“yellow” /> 黄色</li>
<li><input type=“radio” class=“off” name=“light” value=“green” /> 绿色</li>
</ul>
<input class=“button” id=“traffic_button” type=“submit” value=“Go” />
</form>
</div>

比如需要选择红绿单选框,那么可以使用一个tag name来限制(修饰)class ,如下所示:
var active_light = $(“input.on”);

当然也可以结合 就近的ID,如下所示:
var active_light = $(“#traffic_light input.on”);

在使用tag来修饰class的时候,我们需要注意以下几点:

(1) 不要使用tag来修饰ID,如下所示:
var content = $(“div#content”);

这样一来,选择器会先遍历所有的div元素,然后匹配#content。

(2)不要画蛇添足的使用ID来修饰ID,如下所示:
var traffic_light = $(“#content #traffic_light”);

3、将jQuery对 象缓存起来

把jQuery对象缓存起来 就是要告诉我们 要养成将jQuery对象缓存进变量的习惯。下面是一个jQuery新手写的一段代码:
$(“#traffic_light input.on”).bind(“click”, function(){ … });
$(“#traffic_light input.on”).css(“border”, “1px dashed yellow”);
$(“#traffic_light input.on”).css(“background-color”, “orange”);
$(“#traffic_light input.on”).fadeIn(“slow”);

但切记不要这么做。我们应该先将对象缓存进一个变量然后再操作,如下所示:
var $active_light = $(“#traffic_light input.on”);
$active_light.bind(“click”, function(){ … });
$active_light.css(“border”, “1px dashed yellow”);
$active_light.css(“background-color”, “orange”);
$active_light.fadeIn(“slow”);

记住,永远不要让相同的选择器在你的代码里出现多次.

注:

(1)为了区分普通的JavaScript对象和jQuery对 象,可以在变量首字母前加上$符号。

(2)上面代码可以使用jQuery的链式操作加以改善,如 下所示:
var $active_light = $(“#traffic_light input.on”);
$active_light.bind(“click”, function(){ … })
.css(“border”, “1px dashed yellow”)
.css(“background-color”, “orange”)
.fadeIn(“slow”);

如果你打算在其他函数中使用jQuery对象,那么你必须把 它们缓存到全局环境中。如下代码所示:
// 在全局范围定义一个对象(例如:window对象)
window.$my = {
head : $(“head”),
traffic_light : $(“#traffic_light”),
traffic_button : $(“#traffic_button”)
};
function do_something(){ // 现在你可以引用存储的结果并操作它们
var script = document.createElement(“script”);
$my.head.append(script);
// 当你在函数内部操作是,可以继续将查询存入全局对象中去.
$my.cool_results = $(“#some_ul li”);
$my.other_results = $(“#some_table td”);
// 将全局函数作为一个普通的jquery对象去使用.
$my.other_results.css(“border-color”, “red”);
$my.traffic_light.css(“border-color”, “green”);
}
//你也可以在其他函数中使用它

4、限制直接对DOM进行操作

这里的基本思想是在内存中建立你确实想要的东西,然后更新DOM,这并不是一个jQuery最佳实践,但必须进行有效的JavaScript操作,直接的DOM操作速度很慢。例如,你想动 态的创建一组列表元素,千万不要这样做,如下所示:
var top_100_list = [...], // 假设这里是100个独一无二的字符串
$mylist = $(“#mylist”); // jQuery 选择到 <ul> 元素
for (var i=0, l=top_100_list.length; i<l; i++) {
$mylist.append(“<li>” + top_100_list[i] + “</li>”);
}

我们应该将整套元素字符串在插入进DOM中之前先全部创建好,如下所示:
// 这个变量将用来存储我们的列表元素
var top_100_list = [...],$mylist = $(“#mylist”), top_100_li = “”;
for (var i=0, l=top_100_list.length; i<l; i++){
top_100_li += “<li>” + top_100_list[i] + “</li>”;
}
$mylist.html(top_100_li);

注:记得以前还看过一朋友写过这样的代码:
for (i = 0; i < 1000; i++) {
var $myList = $('#myList’);
$myList.append('This is list item ' + i);
}

你应该已经看出问题所在了,#mylist被循环获取了1000次!

5、冒泡

除非在特殊情况下,否则每一个js事件(例如:click,mouseover等。)都会冒泡到父级节点,当我们需要给多个元素调用同个函数时这点 会很有用。

代替这种效率很差的多元素事件监听的方法就是,你只需向它们的父节点绑定一次。比如,我们要为一个拥有很多输入框的表单绑定这样的行为:当输入框被 选中时为它添加一个class。传统的做法是,直接选中input,然后绑定focus等,如下所示:
$(“#entryform input”).bind(“focus”, function(){
$(this).addClass(“selected”);
}).bind(“blur”, function(){
$(this).removeClass(“selected”);
});

当然上面代码能帮我们完成相应的任务,但如果你要寻求更高效的方法,请使用如下代码:
$(“#entryform”).bind(“focus”, function(e){
var $cell = $(e.target); // e.target 捕捉到触发的目标元素
$cell.addClass(“selected”);
}).bind(“blur”, function(e){
var $cell = $(e.target);
$cell.removeClass(“selected”);
});

通过在父级监听获取焦点和失去焦点的事件,对目标元素进行操作。在上面代码中,父级元素扮演了一个调度员的角色, 它可以基于目标元素绑定事件,如果你发现你给很多元素绑定了同一个事件监听, 那么现在的你肯定知道哪里做错了。

同理,在Table操作时,我们也可以使用这种方式加以改进代码:

普通的方式:
$('#myTable td’).click(function(){
$(this).css('background’, 'red’);
});

改进方式:
$('#myTable’).click(function(e) {
var $clicked = $(e.target);
$clicked.css('background’, 'red’);
});

假设有100个td,在使用普通的方式的时候,你绑定了100个事件。在改进方式中,你只为一个元素绑定了1个事件。至于是100个事件的效率高, 还是1个事件的效率高,相信你也能自行分辨了。

6、推迟到 $(window).load

jQuery 对于开发者来说有一个很诱人的东西,可以把任何东西挂到$(document).ready下。尽管$(document).rady 确实很有用,它可以在页面渲染时,其它元素还没下载完成就执行。如果你发现你的页面一直是载入中的状态,很有可能就是$(document).ready 函数引起的。

你可以通过将jQuery函数绑定 到$(window).load 事件的方法来减少页面载入时的CPU使用率,它会在所有的html(包括<iframe>)被下载完成后执行。
$(window).load(function(){
// 页面完全载入后才初始化的jQuery函数.
});

一些特效的功能,例如拖放,视觉特效和动画,预载入隐藏图像等等,都是适合这种技术的场合。

7、压缩JavaScript

压缩和最小化你的JavaScript文件:

在线压缩地址:http://dean.edwards.name/packer/,压缩 之前,请保证你的代码的规范性,否则可能失败,导致JS错误。

8、尽量使用ID代替Class

前面性能优化已经说过,ID选择器的速度是最快的。所以在HTML代码中,能使用ID的尽量使用ID来代替class。

看下面的一个例子:
var $myList = $('#myList’); // 创建一个list
var myListItems = '<ul>’;
for (i = 0; i < 1000; i++) {
// 这里使用的是class
myListItems += '<li class=”listItem’ + i + '”>This is a list item</li>’;
}
myListItems += '</ul>’;
$myList.html(myListItems);
for (i = 0; i < 1000; i++) { // 选择每一个 li
var selectedItem = $('.listItem’ + i);
}

在代码最后,选择每个li的过程中,总共用了5066毫秒,超过5秒了。接着我们做一个对比,用ID代替class:
var $myList = $('#myList’); // 创建一个list
var myListItems = '<ul>’;
for (i = 0; i < 1000; i++) {
//这里使用的是id
myListItems += '<li id=”listItem’ + i + '”>This is a list item</li>’;
}

myListItems += '</ul>’;
$myList.html(myListItems);
for (i = 0; i < 1000; i++) { // 选择每一个 li
var selectedItem = $('#listItem’ + i);
}

在上段代码中,选择每个li总共只用了61毫秒,相比class的方式,将近快了100倍。

9、给选择器一个上下文

jQuery选择器中有一个这样的选择器,它能指定上下文。
jQuery( expression, context );

通过它,能缩小选择器在DOM中搜索的范围,达到节省时间,提高效率。

普通方式:
$('.myDiv’);

改进方式:
$('.myDiv’ , $(“#listItem”) );

10、慎用 .live() 方法(尽量不要使用)

这是jQuery1.3.1版本之后增加的方法,这个方法的功能就是为 新增的DOM元素 动态绑定事件。但对于效率来说,这个方法比较占用资源。所以请尽量不要使用它。

例如有这么一段代码:
<script type=“text/javascript” >
$(function(){
$(“p”).click(function(){
alert($(this).text());
});
$(“button”).click(function(){
$(“<p>this is second p</p>”).appendTo(“body”);
});
}) </script>
<body>
<p>this is first p</p> <button>add</button>
</body>

运行后,你会发现 新增 的 p元素,并没用被绑定click事件。你可以改成.live(”click”)方式解决此问题,代码如下:
$(function(){
$(“p”).live(“click”,function(){ //改成live方式
alert( $(this).text() );
});
$(“button”).click(function(){ $(“<p>this is second p</p>”).appendTo(“body”); });})

但我并不建议大家这么做,我想用另一种方式去解决这个问题,代码如下:
$(function(){
$(“p”).click(function(){
alert( $(this).text() );
});
$(“button”).click(function(){
$(“<p>this is second p</p>”).click(function(){ //为新增的元素重新绑定一次
alert( $(this).text() );
}).appendTo(“body”);
});
})

虽然我把绑定事件重新写了一次,代码多了点,但这种方式的效率明显高于live()方式,特别是在频繁的DOM操作中,这点非常明显。

11、子选择器和后代选择器

后代选择器经常用到,比如:
$(“#list p”);

后代选择器获取的是元素内部所有元素,而有时候实际只要获取 子元素,那么就不应该使用后代选择器。
应该使用子选择器,代码如下:
$(“#list > p”);

12、使用data()方法存储临时变量

下面是一段非常简单的代码:
$(function(){
var flag = false;
$(“button”).click(function(){
if(flag){
$(“p”).text(“true”);
flag=false;
}else{
$(“p”).text(“false”);
flag=true;
}
});
})

改用data()方式后,代码如下:
$(function(){
$(“button”).click(function(){
if( $(“p”).data(“flag”) ){
$(“p”).text(“true”);
$(“p”).data(“flag”,false);
}else{
$(“p”).text(“false”);
$(“p”).data(“flag”,true);
}
});

})

13,尽量使 用原生的JavaScript方法

 
看下面一段代码,它用来判断多选框是否被选中:

 
$(document).ready(function(){
var $cr = $("#cr");  //jQuery对象
$cr.click(function(){
if($cr.is(":checked")){ //jQuery方式判断
alert("感谢你的支持!你可以继续操作!");
}
})
});
上 面代码中,判断是否选中是用了 jquery的方法,但这里可以直接使用原生的JavaScript方法,看下面代码:
$(document).ready(function(){
var $cr = $("#cr");  //jQuery对象
var cr = $cr.get(0); //DOM对象,获取 $cr[0]
$cr.click(function(){
if(cr.checked){ //原生的JavaScript方式判断
alert("感谢你的支持!你可以继续操作!");
}
})
});

 

毋庸置疑,第二种方式效率高于第一种方式,因为他不需要 拐弯抹角的去调用许多函数。


更多:

$(this).css("color","red");   ......

改成:

this.style.color ="red"   ......


$("<p></p>")

改成:

$( document.createElement("p") )



有时候你 也许根本不需要jQuery。( 如果不涉及兼容性问题和自己的水平问题。)



 
14,使用for代替each()方法
    
    这个跟13有点类似,但我特意提取出来,让大家注 意下。
 

 
var array = new Array ();  
for (var i=0; i<10000; i++) {  
    array[i] = 0;  
}  
$.each (array, function (i) {  
    array[i] = i;  
});  
 15,使用join()来拼接字符串。
    
    也许你 之前一直使用 " +  " 来 拼接长字符串。现在你可以改改了。虽然它可能会有点奇怪,但它确实有助于优化性能,尤其是长字符串处理的时候。
 
    首 先创建一个数组,然后循环,最后使用join()把数组转化为字符串。
 
      
     var array = [];  
    for (var i=0; i<=10000; i++) {       
        array[i] = '<li>'+i+'</li>';   
    }      
    $('#list').html (array.join (''));  
 
 
 16,Ajax中文乱码。
    
  Ajax传递中文时会乱码,传统方式大家都是利用escape去编码,然后去后台解码。

$.ajax({
type: "POST",
url: "AjaxTest.jsp",
data: "txt="+$('#txt').val(),
success: function(msg){
$("#AjaxResponse").text(msg);
}
});
 
现在你可以换一换了,你可以非常简单的 解决这个问题,只需要添加一参数即可:
 $.ajax({
type: "POST",
url: "AjaxTest.jsp",
data: "txt="+$('#tbox1').val(),
success: function(msg){
$("#AjaxResponse").text(msg);
},
contentType:"application/x-www-form-urlencoded;charset=utf-8"
}); 

posted @ 2010-06-12 10:07 陈星 阅读(249) 评论(0) 编辑

正则表达式(Regular expressions)是一套语法匹配规则,各种语言,如Perl, .Net和Java都有其对应的共享的正则表达式类库。在.Net中,这个类库叫做Regex。
简单的说,Regex是从字符窗中查找匹配字符串的应用类。通过Regex,编程人员能够非常方便的从一段数据中提取自己所需要的数据信息。举一个简单的例子,让大家对Regex有个大概的了解:
Regex regex = new Regex(@"d+");
Match m = regex.Match("fox 9212gold");
Console.WriteLine(m.Value.ToString());
结果很明显,regex为我们找到了字符串”fox 9212gold”中的数字字符串,输出结果为”9212” .
对 Regex有了一个基本的概念之后,我需要告诉你一个非常好的消息,那就是Regex可以为我们做的远不止这么一点点,他是一套功能非常强大语法匹配规则。当然,这里还有一个坏消息等着我们,那就是强大功能的语法规则自然需要大量纷繁复杂的keyword支持,这也为Regex的学习带来了极大的难度。想要真正掌握正则表达式,可不是几个sample能够全部揭示与说明的。

创建一个Regex对象
Regex的构造函数有三种,在这里就不讨论默认构造函数了。另外两个构造函数中,一个构造函数接收正则表达式字符串作为入参,另一个以正则表达式字符串和RegexOptions作为入参。如:
Regex regex = new Regex("w+$");
Regex regex = new Regex("s+", RegexOptions.IgnoreCase | RegexOptions.Multiline);
RegexOptions可以为我们提供一些特殊的帮助,比如IgnoreCase能够在匹配是忽略大小写,Multiline能够调整^和$的意义,改为匹配一行的开头和结尾。
上面我们构造了一个正则表达式,只不过我们还没有用它来做任何事情,马上我们就可以通过使用下面的几个方法,实现对字符串对象的操作了。
匹配字符串
Regex有两个获取匹配的方法Match()和Matches(),分别代表匹配一个,匹配多个结果。这里就用Matches来展示一下怎么使用Regex获取匹配字符串,并将其显示出来。
public static void showMatches(string expression, RegexOptions option, string ms)
{
Regex regex = new Regex(expression, option);
MatchCollection matches = regex.Matches(ms);
//show matches
Console.WriteLine("////////////////----------------------------------////////////////");
Console.WriteLine(" string: "{0}"  expression: "{1}"  match result is:", ms, expression);
foreach(Match m in matches)
{ Console.WriteLine("match string is: "{0}", length: {1}",m.Value.ToString(), m.Value.Length);
}
Console.WriteLine("matched count: {0}", matches.Count);
}
方法Matched通过比较入参字符串和正则表达式,找到所有符合的结果,并将结果作为MatchCollection传出来。这样,只要简单的遍历这个collection,就可以很快的获得所有的结果。

组的概念

当你获得这样的一个字符串”最后比分是:19/24”,你肯定希望有一个正则表达式,他不单能够找到形如 data1/data2的字符串,还应该能够直接把data1,data2作为单独的结果传送出来。否则你需要再对形如”19/24”的字符串进行分析,才能够顺利得到双方的比分。显然,正则表达式不会忽略这个问题,所以他增加了组的概念。你可以把一次搜索的结果分别放入不同的组中,并通过组名或者组的所以分别取得这些组的结果。比如上例,我们就可以用@”(\d+)/(\d+)”作为表达式。来看看结果吧:
Regex regex = new Regex(@"(d+)/(d+)");
MatchCollection matches = regex.Matches(@"最后比分是:19/24");
//show matches
Console.WriteLine("////////////////----------------------------------////////////////");
foreach(Match m in matches)
{
//Console.WriteLine("match string is: "{0}", length: {1}", // m.Value.ToString(), m.Value.Length);
foreach(string name in regex.GetGroupNames())
{
Console.WriteLine("  capture group "{0}" value is:"{1}"" , name, m.Groups[name].Value);
}
}
Console.WriteLine("matched count: {0}", matches.Count);
输出:
////////////////----------------------------------////////////////
capture group "0" value is:"19/24"
capture group "1" value is:"19"
capture group "2" value is:"24"
matched count: 1

现在清楚了吧,Regex对象把匹配的结果放入组0中。同时,匹配的组信息也放入了对应的组中。组的名称在默认的情况下,是从1开始依次增加的整数。0作为保留名称,专门用于指示匹配的整个字符串。既然有”默认情况”这样的概念,自然也就意味着用户可以自定义组的名称。方法很简单,在组的前面加上:?<name>就可以了。好了,现在把前面的正则表达式修改一下,换成@” (?<score1>\d+)/(?<score1>\d+)”,现在再看看结果:
////////////////----------------------------------////////////////
capture group "0" value is:"19/24"
capture group "score1" value is:"19"
capture group "score2" value is:"24"
matched count: 1

换成自己定义的名字了吧,哈哈!为了在今后的测试中,能够更加方便的看到所有结果,我们对前面介绍过的showMatches()做一点小小的调整。这样,如果在表达式中包含了组的定义,我们也可以在不需要修改任何代码的情况下,直接看到所有的组信息了,调整后的方法 showMatchesPro()如下:

public static void showMatchesPro(string expression, RegexOptions option, string ms)
{
Regex regex = new Regex(expression, option);
MatchCollection matches = regex.Matches(ms);
//show matches
Console.WriteLine("////////////////----------------------------------////////////////");
Console.WriteLine(" string: "{0}"  expression: "{1}"  match result is:",ms, expression);
foreach(Match m in matches)
{
  foreach(string name in regex.GetGroupNames())
{
Console.WriteLine("  capture group "{0}" value is:"{1}"",name, m.Groups[name].Value);
}
}
Console.WriteLine("matched count: {0}", matches.Count);
// show group name
Console.WriteLine("group name count {0}", regex.GetGroupNames().Length);
foreach(string name in regex.GetGroupNames())
{
Console.WriteLine("group name :"{0}"", name);
}
}
替换字符串
Regex也提供了方便的匹配结果替换的功能。为了方便测试,我们也把他写成方法,代码如下:

public static string replaceMatch(string expression, RegexOptions option, string ms, string rep)
{
Regex regex = new Regex(expression, option);
string result = regex.Replace(ms, rep);
Console.WriteLine("////////////////----------------------------------////////////////");
Console.WriteLine("string: "{0}", expression:"{1}", replace by : "{2}"", ms, expression, rep);
Console.WriteLine("replace result string is: "{0}", length: {1}", result.ToString(), result.Length);
return result;
}

Regex.Replace通常接受两个string作为入参,第一个string为输入字符串。第二个字符串用来替代匹配字符串,它可以包含一些特殊字符串,用来代表特别转换。

特殊字符串 替换结果
$& 匹配的字符串,也可以用$0
$1, $2, . . . 匹配字符串中的对应组,用索引标示
${name} 匹配字符串中的对应组,用名称标示
$‘ 匹配位置之前的字符串
$’ 匹配位置之后的字符串
$$ 一个‘$’ 字符
$_ 输入字符串
$+ 匹配字符串的所有组中,最后一个组中的数据

是不是看了这么多怪型怪状的特殊字符串,有点头晕了?嗯,那就弄两个sample来看看结果吧!
Sample1:
replaceMatch(@"\d+", RegexOptions.None, "fef 12/21 df 33/14 727/1", "<<$&>>");
输出,所有数字型的数据都被替换成了<<data>>:
////////////////----------------------------------////////////////
string: "fef 12/21 df 33/14 727/1", expression:"\d+", replace by : "<<$&>>"
replace result string is: "fef <<12>>/<<21>> df <<33>>/<<14>> <<727>>/<<1>>",
length: 50

Sample2:
replaceMatch(@"(\d+)/(\d+)", RegexOptions.None, "fef 12/21 df 33/14 727/1", "$+");
输出,所有data1/data2匹配的数据,都被替换成了data2:
////////////////----------------------------------////////////////
string: "fef 12/21 df 33/14 727/1", expression:"(\d+)/(\d+)", replace by : "$+"
replace result string is: "fef 21 df 14 1", length: 16

怎么样,Regex的功能够丰富的吧!可是,也许你的需求不光这么简单,比如说,你要把”I have 200 dollars”中间的money加倍,怎么办?我晕倒,好像没有现成的东西可以用。没有关系,Regex还有更好的功能。它允许你自己定义转换公式。
using System.Text.RegularExpressions;
class RegularExpressions
{
static string CapText(Match m)
{
// Get the matched string.
string x = m.ToString();
// double this value
string result = (int.Parse(x) * 2).ToString();
return result;
}
static void Main()
{
string text = "i have 200 dollars";
string result = Regex.Replace(text, @"d+",new MatchEvaluator(RegularExpressions.CapText));
System.Console.WriteLine("result=[" + result + "]");
}
}
看看结果,太好了,我的钱真的变成了两倍!
但本文的目的是希望提供给大家一个方便好用的测试类,因此我们重载上面的repalceMatch方法,也允许自定义转换公式作为入参:

public static string replaceMatch(string expression, RegexOptions option, string ms,
MatchEvaluator evaluator)
{
Regex regex = new Regex(expression, option);
string result = regex.Replace(ms, evaluator);
Console.WriteLine("////////////////----------------------------------////////////////");
Console.WriteLine("string: "{0}", expression:"{1}", replace by a evaluator.", ms, expression);
Console.WriteLine("replace result string is: "{0}", length: {1}", result.ToString(), result.Length);
return result;
}
拆分字符串

Regex还提供了从匹配位置将字符串拆分的方法Split。这个方法同样具有多个重载,不过这些都不是重点,大家可以自己看文档。我们继续完成我们用于测试的方法:
public static void splitMatch(string expression, RegexOptions option, string ms)
{
Regex regex = new Regex(expression, option);
string[] result = regex.Split(ms);
Console.WriteLine("////////////////----------------------------------////////////////");
Console.WriteLine("string: "{0}", expression: "{1}", split result is:", ms, expression);
foreach(string m in result)
{
Console.WriteLine("splited string is: "{0}", length: {1}",m.ToString(), m.Length);
}
Console.WriteLine("splited count: {0}", result.Length);
}
代码简单,不多做解释。直接来一个smaple看看结果:
splitMatch(@"/",RegexOptions.None, "2004/4/25");
输出:
////////////////----------------------------------////////////////
string: "2004/4/25", expression: "/", split result is:
splited string is: "2004", length: 4
splited string is: "4", length: 1
splited string is: "25", length: 2
splited count: 3

这个文章的目的很简单:介绍Regex的几个主要功能(匹配、替换和拆分),并提供几个简单方便的测试函数。让你能够测试你对正则表达式的理解是否准确。

比如想要确认^$的作用,你可以放入这样的(input, expression)数据:
(“123”, “^\d+$”) (“123aaa456”, “^\d+”) (“123aaa456”, “123&”)

确认\d, \s, \w, \W的作用,可以这样测试:
(“123abc gc 456”, “\d+”)(“123abc gc 456”, “\s+”)
(“123abc gc 456”, “\w+”)(“123abc gc 456”, “\W+”)

比较? + *之间的区别可以用这样的数据:
(“a123 abcd”, “a\d?”) (“a123 abcd”, “a\d+”) (“a123 abcd”, “a\d*”)

posted @ 2009-12-18 11:36 陈星 阅读(17) 评论(0) 编辑
摘要: 自己一直都是学习使用asp.net mvc框架+linQ的,所以对于ado.net基本上一无所知,最近接了个小项目,所以打算学习+用ado.net来完成。   首先就想到了,写一个通用点SQLHelper类来辅助完成数据库的操作。1.定义一个类:public class SQLHelper<T> where T : class, new()   说明:SQLHelper有一个T的泛型类...阅读全文
posted @ 2009-12-10 12:28 陈星 阅读(93) 评论(0) 编辑

在前面节中,我们先了解了反射,然后利用反射查看了类型信息,并学习了如何创建自定义特性,并利用反射来遍历它。可以说,前面三节,我们学习的都是反射是什么,在接下来的章节中,我们将学习反射可以做什么。在进行更有趣的话题之前,我们先看下如何动态地创建一个对象。

我们新建一个Console控制台项目,叫做Reflection4(因为本文是Part4,你也可以起别的名字)。然后,添加一个示范类,本文中将通过对这个示范类的操作来进行说明:

public class Calculator {

    private int x;
    private int y;

    public Calculator(){
       x = 0;
       y = 0;
    }

    public Calculator(int x, int y) {
       this.x = x;
       this.y = y;
    }
}

1.使用无参数构造函数创建对象

上面这个类非常简单,它包含两个构造函数,一个是有参数的构造函数,一个是无参数的构造函数,我们先看看通过反射,使用无参数的构造函数创建对象。创建对象通常有两种方式,一种是使用Assembly的CreateInstance方法:

Assembly asm = Assembly.GetExecutingAssembly();          
Object obj = asm.CreateInstance("Reflection4.Calculator", true);
// 输出:Calculator() invoked

CreateInstance的第一个参数代表了要创建的类型实例的字符串名称,第二个参数说明是不是大小写无关(Ignore Case)。注意到CreateInstance返回的是一个Object对象,意味着如果想使用这个对象,需要进行一次类型转换。

创建对象的另一种方式是调用Activator类的静态方法CreateInstance:

ObjectHandle handler = Activator.CreateInstance(null, "Reflection4.Calculator");
Object obj = handler.Unwrap();

其中CreateInstance的第一个参数说明是程序集的名称,为null时表示当前程序集;第二个参数说明要创建的类型名称。 Activator.CreateInstance返回的是一个ObjectHandle对象,必须进行一次Unwrap()才能返回Object类型, 进而可以强制转换成我们需要的类型(本例中是Calculator)。ObjectHandle包含在System.Runtime.Remoting命 名空间中,可见它是Remoting相关的,对于Remoting我暂时没有做太多研究,我们现在只要知道可以通过这种方式创建对象就可以了。

2.使用有参数构造函数创建对象

如果我们想通过有参数的构造函数创建对象,我们可以使用Assembly的CreateInstanc()的重载方法:

// 有参数构造函数创建对象
Assembly asm = Assembly.GetExecutingAssembly();
Object[] parameters = new Object[2];    // 定义构造函数需要的参数
parameters[0] = 3;
parameters[1] = 5;

Object obj = asm.CreateInstance("Reflection4.Calculator", true, BindingFlags.Default, null, parameters, null, null);

// 输出:Calculator(int x, int y) invoked

我们看一下CreateInstance需要提供的参数:

  1. 前两个在前一小节已经说明过了;
  2. BindingFlags在前面我们也用到过,它用于限定对类型成员的搜索。在这里指定Default,意思是不使用 BingdingFlags的策略(你可以把它理解成null,但是BindingFlags是值类型,所以不可能为null,必须有一个默认值,而这个 Default就是它的默认值);
  3. 接下来的参数是Binder,它封装了CreateInstance绑定对象(Calculator)的规则,我们几乎永远都会传递null进去,实际上使用的是预定义的DefaultBinder;
  4. 接下来是一个Object[]数组类型,它包含我们传递进去的参数,有参数的构造函数将会使用这些参数;
  5. 接下来的参数是一个CultureInfo类型,它包含了关于语言和文化的信息(简单点理解就是什么时候ToString("c")应该显示“¥”,什么时候应该显示“$”)。

动态调用方法

接下来我们看一下如何动态地调用方法。注意,本文讨论的调用不是将上面动态创建好的对象由Object类型转换成Calculator类型再进行方 法调用,这和“常规调用”就没有区别了,让我们以.Net Reflection 的方式来进行方法的调用。继续进行之前,我们为Calculator添加两个方法,一个实例方法,一个静态方法:

public int Add(){
    int total= 0;
    total = x + y;
    Console.WriteLine("Invoke Instance Method: ");
    Console.WriteLine(String.Format("[Add]: {0} plus {1} equals to {2}", x, y, total));
    return total;
}

public static void Add(int x, int y){
    int total = x + y;
    Console.WriteLine("Invoke Static Method: ");
    Console.WriteLine(String.Format("[Add]: {0} plus {1} equals to {2}", x, y, total));
}

调用方法的方式一般有两种:

  1. 在类型的Type对象上调用InvokeMember()方法,传递想要在其上调用方法的对象(也就是刚才动态创建的Calculator类型实例),并指定BindingFlags为InvokeMethod。根据方法签名,可能还需要传递参数。
  2. 先通过Type对象的GetMethond()方法,获取想要调用的方法对象,也就是MethodInfo对象,然后在该对象上调用Invoke方法。根据方法签名,可能还需要传递参数。

需要说明的是,使用InvokeMember不限于调用对象的方法,也可以用于获取对象的字段、属性,方式都是类似的,本文只说明最常见的调用方法。

1.使用InvokeMember调用方法

我们先看第一种方法,代码很简单,只需要两行(注意obj在上节已经创建,是Calculator类型的实例):

Type t = typeof(Calculator);
int result = (int)t.InvokeMember("Add", BindingFlags.InvokeMethod, null, obj, null);
Console.WriteLine(String.Format("The result is {0}", result));

输出:
Invoke Instance Method:
[Add]: 3 plus 5 equals to 8
The result is 8

在InvokeMember方法中,第一个参数说明了想要调用的方法名称;第二个参数说明是调用方法(因为InvokeMember的功能非常强 大,不光是可以调用方法,还可以获取/设置 属性、字段等。此枚举的详情可参看Part.2或者MSDN);第三个参数是Binder,null说明使用默认的Binder;第四个参数说明是在这个 对象上(obj是Calculator类型的实例)进行调用;最后一个参数是数组类型,表示的是方法所接受的参数。

我们在看一下对于静态方法应该如何调用:

Object[] parameters2 = new Object[2];
parameters2[0] = 6;
parameters2[1] = 9;
t.InvokeMember("Add", BindingFlags.InvokeMethod, null, typeof(Calculator), parameters2);

输出:
Invoke Static Method:
[Add]: 6 plus 9 equals to 15

我们和上面对比一下:首先,第四个参数传递的是 typeof(Calculator),不再是一个Calculator实例类型,这很容易理解,因为我们调用的是一个静态方法,它不是基于某个具体的类 型实例的,而是基于类型本身;其次,因为我们的静态方法需要提供两个参数,所以我们以数组的形式将这两个参数进行了传递。

2.使用MethodInfo.Invoke调用方法

我们再看下第二种方式,先获得一个MethodInfo实例,然后调用这个实例的Invoke方法,我们看下具体如何做:

Type t = typeof(Calculator);
MethodInfo mi = t.GetMethod("Add", BindingFlags.Instance | BindingFlags.Public);
mi.Invoke(obj, null);

输出:
Invoke Instance Method:
[Add]: 3 plus 5 equals to 8
请按任意键继续. . .

在代码的第二行,我们先使用GetMethod方法获取了一个方法对象MethodInfo,指定BindingFlags为Instance和 Public,因为有两个方法都命名为“Add”,所以在这里指定搜索条件是必须的。接着我们使用Invoke()调用了Add方法,第一个参数obj是 前面创建的Calculator类型实例,表明在该实例上创建方法;第二个参数为null,说明方法不需要提供参数。

我们再看下如何使用这种方式调用静态方法:

Type t = typeof(Calculator);
Object[] parameters2 = new Object[2];
parameters2[0] = 6;
parameters2[1] = 9;
MethodInfo mi = t.GetMethod("Add", BindingFlags.Static | BindingFlags.Public);
mi.Invoke(null, parameters2);
// mi.Invoke(t, parameters2);   也可以这样

输出:
Invoke Static Method:
[Add]: 6 plus 9 equals to 15

可以看到与上面的大同小异,在GetMethod()方法中,我们指定为搜索BindingFlags.Static,而不是 BindingFlags.Public,因为我们要调用的是静态的Add方法。在Invoke()方法中,需要注意的是第一个参数,不能在传递 Calculator类型实例,而应该传递Calculator的Type类型或者直接传递null。因为静态方法不是属于某个实例的。

NOTE:通过上面的例子可以看出:使用反射可以达到最大程度上的多态,举个例子, 你可以在页面上放置一个DropDownList控件,然后指定它的Items的value为你某个类的方法的名称,然后在 SelectedIndexChanged事件中,利用value的值来调用类的方法。而在以前,你只能写一些if else 语句,先判断DropDownList返回的值,根据值再决定调用哪个方法。使用这种方式,编译器在代码运行之前(或者说用户选择了某个选项之前)完全不 知道哪个方法将被调用,这也就是常说的 迟绑定(Late Binding)

Coding4Fun:遍历System.Drawing.Color结构

我们已经讲述了太多的基本方法和理论,现在让我们来做一点有趣的事情:大家知道在Asp.Net中控件的颜色设置,比如说ForeColor, BackColor等,都是一个System.Draw.Color结构类型。在某些情况下我们需要使用自定义的颜色,那么我们会使用类似这样的方式 Color.FromA#7d190d创建一个颜色值。但有时候我们会觉得比较麻烦,因为这个数字太不直观了,我们甚至需要把这个值贴到 PhotoShop中看看是什么样的。

这时候,我们可能会想要使用Color结构提供的默认颜色,也就是它的141个静态属性,但是这些值依然是以名称,比如DarkGreen的形式给 出的,还是不够直观,如果能把它们以色块的形式输出到页面就好了,这样我们查看起来会方便的多,以后使用也会比较便利。我已经实现了它,可以点击下面的链 接查看:

效果预览:http://www.tracefact.net/demo/reflection/color.aspx

基本实现

现在我们来看一下实现过程:

先创建页面Color.aspx(或其他名字),然后在Head里添加些样式控制页面显示,再拖放一个Panel控件进去。样式表需要注意的 是#pnColors div部分,它定义了页面上将显示的色块的样式;Id为pnColors的Panel控件用于装载我们动态生成的div。

<head>
<style type="text/css">
body{font-size:14px;}
h1{font-size:26px;}
#pnColors div{
    float:left;width:140px;
    padding:7px 0;
    text-align:center;
    margin:3px;
    border:1px solid #aaa;
    font-size:11px;
    font-family:verdana, arial
}
</style>
</head>

<body>
    <h1>Coding4Fun:使用反射遍历System.Drawing.Color结构</h1>
    <form id="form1" runat="server">
       <asp:Panel ID="pnColors" runat="server"></asp:Panel>
    </form>
</body>

NOTE:如果将页面命名为了 Color.aspx,那么需要在代码后置文件中修改类名,比如改成:Reflection_Color,同时页面顶部也需要修改成Inherits="Reflection_Color",不然会出现命名冲突的问题。

下一步的思路是这样的:我们在phColors中添加一系列的div,这些div也就是页面上我们将要显示的色块。我们设置div的文本为 颜色的名称 和 RGB数值,它的背景色我们设为相应的颜色(色块的其他样式,比如宽、边框、宽度已经在head中定义)。我们知道在Asp.Net中,并没有一个Div 控件,只有HtmlGenericControl,此时,我们最好定义一个Div让它继承自HtmlGenericControl。

public class Div:HtmlGenericControl
{
    private string name;
   
    public Div(Color c)
       : base("div")     // 调用基类构造函数,创建一个 Div
    {
       this.name = c.Name;      // 颜色名称
      
       // 设置文本
       this.InnerHtml = String.Format("{0}<br />RGB({1},{2},{3})", name, c.R, c.G, c.B);

       int total = c.R + c.G + c.B;
       if (total <= 255)     // 如果底色太暗,前景色改为明色调
           this.Style.Add("color", "#eee");

       // 设置背景颜色
       this.Style.Add("background", String.Format("rgb({0},{1},{2})", c.R, c.G, c.B));
    }
}

如同我们前面所描述的,这个Div接受一个Color类型作为构造函数的参数,然后在构造函数中,先设置了它的文本为 颜色名称 和 颜色的各个数值(通过Color结构的R, G, B属性获得)。然后设置了div的背景色为相应的RGB颜色。

NOTE:在上面 if(total<=255)那里,可能有的颜色本身就很暗,如果这种情况再使用黑色的前景色那么文字会看不清楚,所以我添加了判断,如果背景太暗,就将前景色调的明亮一点。

OK,现在我们到后置代码中只要做一点点的工作就可以了:

protected void Page_Load(object sender, EventArgs e)
{
    List<Div> list = new List<Div>();

    Type t = typeof(Color);      // 页首已经包含了 using System.Drawing;
    // 获取属性
    PropertyInfo[] properties = t.GetProperties(BindingFlags.Static | BindingFlags.Public);
    Div div;

    // 遍历属性
    foreach (PropertyInfo p in properties)
    {
       // 动态获得属性
       Color c;         
       c = (Color)t.InvokeMember(p.Name, BindingFlags.GetProperty, null, typeof(Color), null);       
      
       div = new Div(c);
       list.Add(div);
    }

    foreach (Div item in list)   {
       pnColors.Controls.Add(item);
    }
}

上面的代码是很直白的:先创建一个Div列表,用于保存即将创建的色块。然后获取Color类型的Type实例。接着我们使用 GetProperties()方法,并指定BindingFlags获取所有的静态公共属性。然后遍历属性,并使用InvokeMember()方法获 取了属性值,因为返回的是一个Object类型,所以我们需要把它强制转换成一个Color类型。注意在这里InvokeMember的 BindingFlags指定为GetProperty,意为获取属性值。第四个参数为typeof(Color),因为颜色属性(比如 DarkGreen)是静态的,不是针对于某个实例的,如果是实例,则需要传递调用此属性的类型实例。最后,我们根据颜色创建div,并将它加入列表,遍 历列表并逐一加入到Id为pnColors的Panal控件中。

现在已经OK了,如果打开页面,应该可以看到类似这样的效果:

为列表排序

上面的页面看上去会比较乱,因为列表大致是按颜色名称排序的(Transparnet例外),我们最好可以让列表基于颜色进行排序。关于列表排序,我在 基于业务对象的排序 一文中已经非常详细地进行了讨论,所以这里我仅给出实现过程,而不再进行讲述。这一小节与反射无关,如果你对排序已经非常熟悉,可以跳过。

在页面上添加一个RadioButtonList控件,将AutoPostBack设为true,我们要求可以按名称和颜色值两种方式进行排序:

排序:
<asp:RadioButtonList ID="rblSort" runat="server" AutoPostBack="true" RepeatDirection="Horizontal" RepeatLayout="Flow">
    <asp:ListItem Selected="True">Name</asp:ListItem>
     <asp:ListItem>Color</asp:ListItem>       
</asp:RadioButtonList>

在后置代码中,添加一个枚举作为排序的依据:

public enum SortBy{
    Name,         // 按名称排序
    Color         // 暗颜色值排序
}

修改Div类,添加 ColorValue字段,这个字段代表颜色的值,并创建嵌套类型ColorComparer,以及方法GetComparer:

public class Div:HtmlGenericControl
{
    private int colorValue;
    private string name;
   
    public Div(Color c)
       : base("div")     // 调用基类构造函数,创建一个 Div
    {
       this.name = c.Name;      // 颜色名称

       this.colorValue =        // 颜色的色彩值
           c.R * 256 * 256 + c.G * 256 + c.B;
      
       // 设置文本
       this.InnerHtml = String.Format("{0}<br />RGB({1},{2},{3})", name, c.R, c.G, c.B);

       int total = c.R + c.G + c.B;
       if (total <= 255)     // 如果底色太暗,前景色改为明色调
           this.Style.Add("color", "#eee");

       // 设置背景颜色
       this.Style.Add("background", String.Format("rgb({0},{1},{2})", c.R, c.G, c.B));
    }
   
    // 返回一个Comparer()用于排序
    public static ColorComparer GetComparer(SortBy sort) {
       return new ColorComparer(sort);
    }

    // 默认以名称排序
    public static ColorComparer GetComparer() {
       return GetComparer(SortBy.Name);
    }
   
    // 嵌套类型,用于排序
    public class ColorComparer : IComparer<Div>
    {
       private SortBy sort;

       public ColorComparer(SortBy sort) {
           this.sort = sort;
       }

       // 实现IComparer<T>接口,根据sort判断以何为依据一进排序
       public int Compare(Div x, Div y)
       {
           if (sort == SortBy.Name)
              return String.Compare(x.name, y.name);
           else
              return x.colorValue.CompareTo(y.colorValue);
       }
    }
}

在Page_Load事件上面,我们添加语句,获取当前的排序依据(枚举):

SortBy sort;

if (!IsPostBack) {
    sort = SortBy.Name;
} else {
    sort = (SortBy)Enum.Parse(typeof(SortBy), rblSort.SelectedValue);
}

在将列表输出到页面之前,我们调用列表的Sort方法:

list.Sort(Div.GetComparer(sort));   // 对列表进行排序

foreach (Div item in list)   {
    pnColors.Controls.Add(item);
}

好了,所有工作都完成了,再次打开页面,可以看到类似如下画面,我们可以按照名称或者颜色值来对列表进行排序显示:

总结

本文分三个部分讲述了.Net中反射的一个应用:动态创建对象和调用对象方法(属性、字段)。我们先学习最常见的动态创建对象的两种方式,随后分别 讨论了使用Type.InvokeMember()和MethodInfo.Invoke()方法来调用类型的实例方法和静态方法。最后,我们使用反射遍 历了System.Drawing.Color结构,并输出了颜色值。

posted @ 2009-12-08 19:04 陈星 阅读(191) 评论(0) 编辑

注:本文中出现的代码均在.net Framework RC3环境中运行通过

.多线程的概念

Windows是一个多任务的系统,如果你使用的是windows 2000及其以上版本,你可以通过任务管理器查看当前系统运行的程序和进程。什么是进程呢?当一个程序开始运行时,它就是一个进程,进程所指包括运行中的程序和程序所使用到的内存和系统资源。而一个进程又是由多个线程所组成的,线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。浏览器就是一个很好的多线程的例子,在浏览器中你可以在下载JAVA小应用程序或图象的同时滚动页面,在访问新页面时,播放动画和声音,打印文件等。
多线程的好处在于可以提高CPU的利用率——任何一个程序员都不希望自己的程序很多时候没事可干,在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率。
然而我们也必须认识到线程本身可能影响系统性能的不利方面,以正确使用线程:

  • 线程也是程序,所以线程需要占用内存,线程越多占用内存也越多
  • 多线程需要协调和管理,所以需要CPU时间跟踪线程
  • 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题
  • 线程太多会导致控制太复杂,最终可能造成很多Bug

基于以上认识,我们可以一个比喻来加深理解。假设有一个公司,公司里有很多各司其职的职员,那么我们可以认为这个正常运作的公司就是一个进程,而公司里的职员就是线程。一个公司至少得有一个职员吧,同理,一个进程至少包含一个线程。在公司里,你可以一个职员干所有的事,但是效率很显然是高不起来的,一个人的公司也不可能做大;一个程序中也可以只用一个线程去做事,事实上,一些过时的语言如fortune,basic都是如此,但是象一个人的公司一样,效率很低,如果做大程序,效率更低——事实上现在几乎没有单线程的商业软件。公司的职员越多,老板就得发越多的薪水给他们,还得耗费大量精力去管理他们,协调他们之间的矛盾和利益;程序也是如此,线程越多耗费的资源也越多,需要CPU时间去跟踪线程,还得解决诸如死锁,同步等问题。总之,如果你不想你的公司被称为“皮包公司”,你就得多几个员工;如果你不想让你的程序显得稚气,就在你的程序里引入多线程吧!
本文将对C#编程中的多线程机制进行探讨,通过一些实例解决对线程的控制,多线程间通讯等问题。为了省去创建GUI那些繁琐的步骤,更清晰地逼近线程的本质,下面所有的程序都是控制台程序,程序最后的Console.ReadLine()是为了使程序中途停下来,以便看清楚执行过程中的输出。
好了,废话少说,让我们来体验一下多线程的C#吧!


.操纵一个线程


任何程序在执行时,至少有一个主线程,下面这段小程序可以给读者一个直观的印象:

//SystemThread.cs
using System;
using System.Threading;
namespace ThreadTest
{
  class RunIt
  {
    [STAThread]
    static void Main(string[] args)
    {
      Thread.CurrentThread.Name="System Thread";//给当前线程起名为"System Thread"
            Console.WriteLine(Thread.CurrentThread.Name+"'Status:"+Thread.CurrentThread.ThreadState);
      Console.ReadLine();
    }
  }
} 

编译执行后你看到了什么?是的,程序将产生如下输出:

System Thread's Status:Running


在这里,我们通过Thread类的静态属性CurrentThread获取了当前执行的线程,对其Name属性赋值“System Thread”,最后还输出了它的当前状态(ThreadState)。所谓静态属性,就是这个类所有对象所公有的属性,不管你创建了多少个这个类的实例,但是类的静态属性在内存中只有一个。很容易理解CurrentThread为什么是静态的——虽然有多个线程同时存在,但是在某一个时刻,CPU只能执行其中一个。
就像上面程序所演示的,我们通过Thread类来创建和控制线程。注意到程序的头部,我们使用了如下命名空间:

using System;
using System.Threading;

在.net framework class library中,所有与多线程机制应用相关的类都是放在System.Threading命名空间中的。其中提供Thread类用于创建线程,ThreadPool类用于管理线程池等等,此外还提供解决了线程执行安排,死锁,线程间通讯等实际问题的机制。如果你想在你的应用程序中使用多线程,就必须包含这个类。Thread类有几个至关重要的方法,描述如下:

  • Start():启动线程
  • Sleep(int):静态方法,暂停当前线程指定的毫秒数
  • Abort():通常使用该方法来终止一个线程
  • Suspend():该方法并不终止未完成的线程,它仅仅挂起线程,以后还可恢复。
  • Resume():恢复被Suspend()方法挂起的线程的执行

1

下面我们就动手来创建一个线程,使用Thread类创建线程时,只需提供线程入口即可。线程入口使程序知道该让这个线程干什么事,在C#中,线程入口是通过ThreadStart代理(delegate)来提供的,你可以把ThreadStart理解为一个函数指针,指向线程要执行的函数,当调用Thread.Start()方法后,线程就开始执行ThreadStart所代表或者说指向的函数。
打开你的VS.net,新建一个控制台应用程序(Console Application),下面这些代码将让你体味到完全控制一个线程的无穷乐趣!

    //ThreadTest.cs
  using System;
  using System.Threading;
  namespace ThreadTest
  {
  public class Alpha
    {
      public void Beta()
      {
        while (true)
        {
          Console.WriteLine("Alpha.Beta is running in its own thread.");
        }
      }
    };
    public class Simple
    {
      public static int Main()
      {
        Console.WriteLine("Thread Start/Stop/Join Sample");
        Alpha oAlpha = new Alpha();
        file://这里创建一个线程,使之执行Alpha类的Beta()方法
        Thread oThread = new Thread(new ThreadStart(oAlpha.Beta));
        oThread.Start();
        while (!oThread.IsAlive);
          Thread.Sleep(1);
        oThread.Abort();
        oThread.Join();
        Console.WriteLine();
        Console.WriteLine("Alpha.Beta has finished");
        try
        {
          Console.WriteLine("Try to restart the Alpha.Beta thread");
          oThread.Start();
        }
        catch (ThreadStateException)
        {
          Console.Write("ThreadStateException trying to restart Alpha.Beta. ");
          Console.WriteLine("Expected since aborted threads cannot be restarted.");
          Console.ReadLine();
        }
        return 0;
      }
    }
  } 

 

这段程序包含两个类Alpha和Simple,在创建线程oThread时我们用指向Alpha.Beta()方法的初始化了ThreadStart代理(delegate)对象,当我们创建的线程oThread调用oThread.Start()方法启动时,实际上程序运行的是Alpha.Beta()方法:

Alpha oAlpha = new Alpha();
Thread oThread = new Thread(new ThreadStart(oAlpha.Beta));
oThread.Start(); 
 

然后在Main()函数的while循环中,我们使用静态方法Thread.Sleep()让主线程停了1ms,这段时间CPU转向执行线程oThread。然后我们试图用Thread.Abort()方法终止线程oThread,注意后面的oThread.Join(),Thread.Join()方法使主线程等待,直到oThread线程结束。你可以给Thread.Join()方法指定一个int型的参数作为等待的最长时间。之后,我们试图用Thread.Start()方法重新启动线程oThread,但是显然Abort()方法带来的后果是不可恢复的终止线程,所以最后程序会抛出ThreadStateException异常。
程序最后得到的结果将如下图:

在这里我们要注意的是其它线程都是依附于Main()函数所在的线程的,Main()函数是C#程序的入口,起始线程可以称之为主线程,如果所有的前台线程都停止了,那么主线程可以终止,而所有的后台线程都将无条件终止。而所有的线程虽然在微观上是串行执行的,但是在宏观上你完全可以认为它们在并行执行。
读者一定注意到了Thread.ThreadState这个属性,这个属性代表了线程运行时状态,在不同的情况下有不同的值,于是我们有时候可以通过对该值的判断来设计程序流程。ThreadState在各种情况下的可能取值如下:

  • Aborted:线程已停止
  • AbortRequested:线程的Thread.Abort()方法已被调用,但是线程还未停止
  • Background:线程在后台执行,与属性Thread.IsBackground有关
  • Running:线程正在正常运行
  • Stopped:线程已经被停止
  • StopRequested:线程正在被要求停止
  • Suspended:线程已经被挂起(此状态下,可以通过调用Resume()方法重新运行)
  • SuspendRequested:线程正在要求被挂起,但是未来得及响应
  • Unstarted:未调用Thread.Start()开始线程的运行
  • WaitSleepJoin:线程因为调用了Wait(),Sleep()或Join()等方法处于封锁状态

上面提到了Background状态表示该线程在后台运行,那么后台运行的线程有什么特别的地方呢?其实后台线程跟前台线程只有一个区别,那就是后台线程不妨碍程序的终止。一旦一个进程所有的前台线程都终止后,CLR(通用语言运行环境)将通过调用任意一个存活中的后台进程的Abort()方法来彻底终止进程。
当线程之间争夺CPU时间时,CPU按照是线程的优先级给予服务的。在C#应用程序中,用户可以设定5个不同的优先级,由高到低分别是Highest,AboveNormal,Normal,BelowNormal,Lowest,在创建线程时如果不指定优先级,那么系统默认为ThreadPriority.Normal。给一个线程指定优先级
,我们可以使用如下代码:

  //设定优先级为最低
  myThread.Priority=ThreadPriority.Lowest; 

通过设定线程的优先级,我们可以安排一些相对重要的线程优先执行,例如对用户的响应等等。
现在我们对怎样创建和控制一个线程已经有了一个初步的了解,下面我们将深入研究线程实现中比较典型的的问题,并且探讨其解决方法。
  三.线程的同步和通讯——生产者和消费者
假设这样一种情况,两个线程同时维护一个队列,如果一个线程对队列中添加元素,而另外一个线程从队列中取用元素,那么我们称添加元素的线程为生产者,称取用元素的线程为消费者。生产者与消费者问题看起来很简单,但是却是多线程应用中一个必须解决的问题,它涉及到线程之间的同步和通讯问题。
前面说过,每个线程都有自己的资源,但是代码区是共享的,即每个线程都可以执行相同的函数。但是多线程环境下,可能带来的问题就是几个线程同时执行一个函数,导致数据的混乱,产生不可预料的结果,因此我们必须避免这种情况的发生。C#提供了一个关键字lock,它可以把一段代码定义为互斥段(critical section),互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待。在C#中,关键字lock定义如下:

  lock(expression) statement_block 

expression代表你希望跟踪的对象,通常是对象引用。一般地,如果你想保护一个类的实例,你可以使用this;如果你希望保护一个静态变量(如互斥代码段在一个静态方法内部),一般使用类名就可以了。而statement_block就是互斥段的代码,这段代码在一个时刻内只可能被一个线程执行。
  下面是一个使用lock关键字的典型例子,我将在注释里向大家说明lock关键字的用法和用途:

  //lock.cs
  using System;
  using System.Threading;
  internal class Account
  {
  int balance;
  Random r = new Random();
  internal Account(int initial)
  {
    balance = initial;
  }
  internal int Withdraw(int amount)
  {
    if (balance < 0)
    {
    file://如果balance小于0则抛出异常
    throw new Exception("Negative Balance");
    }
    //下面的代码保证在当前线程修改balance的值完成之前
    //不会有其他线程也执行这段代码来修改balance的值
    //因此,balance的值是不可能小于0的
    lock (this)
    {
    Console.WriteLine("Current Thread:"+Thread.CurrentThread.Name);
    file://如果没有lock关键字的保护,那么可能在执行完if的条件判断之后
    file://另外一个线程却执行了balance=balance-amount修改了balance的值
    file://而这个修改对这个线程是不可见的,所以可能导致这时if的条件已经不成立了
    file://但是,这个线程却继续执行balance=balance-amount,所以导致balance可能小于0
    if (balance >= amount)
    {
      Thread.Sleep(5);
      balance = balance - amount;
      return amount;
    }
    else
    {
      return 0; // transaction rejected
    }
    }
  }
  internal void DoTransactions()
  {
    for (int i = 0; i < 100; i++)
    Withdraw(r.Next(-50, 100));
  }
  }
  internal class Test
  {
  static internal Thread[] threads = new Thread[10];
  public static void Main()
  {
    Account acc = new Account (0);
    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].Name=i.ToString();
    for (int i = 0; i < 10; i++)
    threads[i].Start();
    Console.ReadLine();
  }
  }

 

而多线程公用一个对象时,也会出现和公用代码类似的问题,这种问题就不应该使用lock关键字了,这里需要用到System.Threading中的一个类Monitor,我们可以称之为监视器,Monitor提供了使线程共享资源的方案。
Monitor类可以锁定一个对象,一个线程只有得到这把锁才可以对该对象进行操作。对象锁机制保证了在可能引起混乱的情况下一个时刻只有一个线程可以访问这个对象。Monitor必须和一个具体的对象相关联,但是由于它是一个静态的类,所以不能使用它来定义对象,而且它的所有方法都是静态的,不能使用对象来引用。下面代码说明了使用Monitor锁定一个对象的情形:

    ......
  Queue oQueue=new Queue();
  ......
  Monitor.Enter(oQueue);
  ......//现在oQueue对象只能被当前线程操纵了
  Monitor.Exit(oQueue);//释放锁

如上所示,当一个线程调用Monitor.Enter()方法锁定一个对象时,这个对象就归它所有了,其它线程想要访问这个对象,只有等待它使用Monitor.Exit()方法释放锁。为了保证线程最终都能释放锁,你可以把Monitor.Exit()方法写在try-catch-finally结构中的finally代码块里。对于任何一个被Monitor锁定的对象,内存中都保存着与它相关的一些信息,其一是现在持有锁的线程的引用,其二是一个预备队列,队列中保存了已经准备好获取锁的线程,其三是一个等待队列,队列中保存着当前正在等待这个对象状态改变的队列的引用。当拥有对象锁的线程准备释放锁时,它使用Monitor.Pulse()方法通知等待队列中的第一个线程,于是该线程被转移到预备队列中,当对象锁被释放时,在预备队列中的线程可以立即获得对象锁。
下面是一个展示如何使用lock关键字和Monitor类来实现线程的同步和通讯的例子,也是一个典型的生产者与消费者问题。这个例程中,生产者线程和消费者线程是交替进行的,生产者写入一个数,消费者立即读取并且显示,我将在注释中介绍该程序的精要所在。用到的系统命名空间如下:

  using System;
  using System.Threading; 

首先,我们定义一个被操作的对象的类Cell,在这个类里,有两个方法:ReadFromCell()和WriteToCell。消费者线程将调用ReadFromCell()读取cellContents的内容并且显示出来,生产者进程将调用WriteToCell()方法向cellContents写入数据。

  public class Cell
  {
  int cellContents; // Cell对象里边的内容
  bool readerFlag = false; // 状态标志,为true时可以读取,为false则正在写入
  public int ReadFromCell( )
  {
    lock(this) // Lock关键字保证了什么,请大家看前面对lock的介绍
    {
    if (!readerFlag)//如果现在不可读取
    {
      try
      {
      file://等待WriteToCell方法中调用Monitor.Pulse()方法
      Monitor.Wait(this);
      }
      catch (SynchronizationLockException e)
      {
      Console.WriteLine(e);
      }
      catch (ThreadInterruptedException e)
      {
      Console.WriteLine(e);
      }
    }
    Console.WriteLine("Consume: {0}",cellContents);
    readerFlag = false; file://重置readerFlag标志,表示消费行为已经完成
    Monitor.Pulse(this); file://通知WriteToCell()方法(该方法在另外一个线程中执行,等待中)
    }
    return cellContents;
  }
  public void WriteToCell(int n)
  {
    lock(this)
    {
    if (readerFlag)
    {
      try
      {
      Monitor.Wait(this);
      }
      catch (SynchronizationLockException e)
      {
      file://当同步方法(指Monitor类除Enter之外的方法)在非同步的代码区被调用
      Console.WriteLine(e);
      }
      catch (ThreadInterruptedException e)
      {
      file://当线程在等待状态的时候中止
      Console.WriteLine(e);
      }
    }
    cellContents = n;
    Console.WriteLine("Produce: {0}",cellContents);
    readerFlag = true;
    Monitor.Pulse(this); file://通知另外一个线程中正在等待的ReadFromCell()方法
    }
  }
  }

下面定义生产者CellProd和消费者类CellCons,它们都只有一个方法ThreadRun(),以便在Main()函数中提供给线程的ThreadStart代理对象,作为线程的入口。

  public class CellProd
  {
  Cell cell; // 被操作的Cell对象
  int quantity = 1; // 生产者生产次数,初始化为1
  public CellProd(Cell box, int request)
  {
    //构造函数
    cell = box;
    quantity = request;
  }
  public void ThreadRun( )
  {
    for(int looper=1; looper<=quantity; looper++)
    cell.WriteToCell(looper); file://生产者向操作对象写入信息
  }
  }
  public class CellCons
  {
  Cell cell;
  int quantity = 1;
  public CellCons(Cell box, int request)
  {
    cell = box;
    quantity = request;
  }
  public void ThreadRun( )
  {
    int valReturned;
    for(int looper=1; looper<=quantity; looper++)
    valReturned=cell.ReadFromCell( );//消费者从操作对象中读取信息
  }
  } 

然后在下面这个类MonitorSample的Main()函数中我们要做的就是创建两个线程分别作为生产者和消费者,使用CellProd.ThreadRun()方法和CellCons.ThreadRun()方法对同一个Cell对象进行操作。

  public class MonitorSample
  {
  public static void Main(String[] args)
  {
    int result = 0; file://一个标志位,如果是0表示程序没有出错,如果是1表明有错误发生
    Cell cell = new Cell( );
    //下面使用cell初始化CellProd和CellCons两个类,生产和消费次数均为20次
    CellProd prod = new CellProd(cell, 20);
    CellCons cons = new CellCons(cell, 20);
    Thread producer = new Thread(new ThreadStart(prod.ThreadRun));
    Thread consumer = new Thread(new ThreadStart(cons.ThreadRun));
    //生产者线程和消费者线程都已经被创建,但是没有开始执行
    try
    {
    producer.Start( );
    consumer.Start( );
    producer.Join( );
    consumer.Join( );
    Console.ReadLine();
    }
    catch (ThreadStateException e)
    {
    file://当线程因为所处状态的原因而不能执行被请求的操作
    Console.WriteLine(e);
    result = 1;
    }
    catch (ThreadInterruptedException e)
    {
    file://当线程在等待状态的时候中止
    Console.WriteLine(e);
    result = 1;
    }
    //尽管Main()函数没有返回值,但下面这条语句可以向父进程返回执行结果
    Environment.ExitCode = result;
  }
  } 

大家可以看到,在上面的例程中,同步是通过等待Monitor.Pulse()来完成的。首先生产者生产了一个值,而同一时刻消费者处于等待状态,直到收到生产者的“脉冲(Pulse)”通知它生产已经完成,此后消费者进入消费状态,而生产者开始等待消费者完成操作后将调用Monitor.Pulese()发出的“脉冲”。它的执行结果很简单:

 

  Produce: 1
  Consume: 1
  Produce: 2
  Consume: 2
  Produce: 3
  Consume: 3
  ...
  ...
  Produce: 20
  Consume: 20 

 

事实上,这个简单的例子已经帮助我们解决了多线程应用程序中可能出现的大问题,只要领悟了解决线程间冲突的基本方法,很容易把它应用到比较复杂的程序中去。
四、线程池和定时器——多线程的自动管理
在多线程的程序中,经常会出现两种情况。一种情况下,应用程序中的线程把大部分的时间花费在等待状态,等待某个事件发生,然后才能给予响应;而另外一种情况则是线程平常都处于休眠状态,只是周期性地被唤醒。在.net framework里边,我们使用ThreadPool来对付第一种情况,使用Timer来对付第二种情况。
  ThreadPool类提供一个由系统维护的线程池——可以看作一个线程的容器,该容器需要Windows 2000以上版本的系统支持,因为其中某些方法调用了只有高版本的Windows才有的API函数。你可以使用ThreadPool.QueueUserWorkItem()方法将线程安放在线程池里,该方法的原型如下:
  //将一个线程放进线程池,该线程的Start()方法将调用WaitCallback代理对象代表的函数
  public static bool QueueUserWorkItem(WaitCallback);
  //重载的方法如下,参数object将传递给WaitCallback所代表的方法
  public static bool QueueUserWorkItem(WaitCallback, object);
要注意的是,ThreadPool类也是一个静态类,你不能也不必要生成它的对象,而且一旦使用该方法在线程池中添加了一个项目,那么该项目将是没有办法取消的。在这里你无需自己建立线程,只需把你要做的工作写成函数,然后作为参数传递给ThreadPool.QueueUserWorkItem()方法就行了,传递的方法就是依靠WaitCallback代理对象,而线程的建立、管理、运行等等工作都是由系统自动完成的,你无须考虑那些复杂的细节问题,线程池的优点也就在这里体现出来了,就好像你是公司老板——只需要安排工作,而不必亲自动手。

下面的例程演示了ThreadPool的用法。首先程序创建了一个ManualResetEvent对象,该对象就像一个信号灯,可以利用它的信号来通知其它线程,本例中当线程池中所有线程工作都完成以后,ManualResetEvent的对象将被设置为有信号,从而通知主线程继续运行。它有几个重要的方法:Reset(),Set(),WaitOne()。初始化该对象时,用户可以指定其默认的状态(有信号/无信号),在初始化以后,该对象将保持原来的状态不变直到它的Reset()或者Set()方法被调用,Reset()方法将其设置为无信号状态,Set()方法将其设置为有信号状态。WaitOne()方法使当前线程挂起直到ManualResetEvent对象处于有信号状态,此时该线程将被激活。然后,程序将向线程池中添加工作项,这些以函数形式提供的工作项被系统用来初始化自动建立的线程。当所有的线程都运行完了以后,ManualResetEvent.Set()方法被调用,因为调用了ManualResetEvent.WaitOne()方法而处在等待状态的主线程将接收到这个信号,于是它接着往下执行,完成后边的工作。

  using System;
  using System.Collections;
  using System.Threading;
  //这是用来保存信息的数据结构,将作为参数被传递
  public class SomeState
  {
  public int Cookie;
  public SomeState(int iCookie)
  {
    Cookie = iCookie;
  }
  }
  public class Alpha
  {
  public Hashtable HashCount;
  public ManualResetEvent eventX;
  public static int iCount = 0;
  public static int iMaxCount = 0;
  public Alpha(int MaxCount)
  {
    HashCount = new Hashtable(MaxCount);
    iMaxCount = MaxCount;
  }
  file://线程池里的线程将调用Beta()方法
  public void Beta(Object state)
  {
    //输出当前线程的hash编码值和Cookie的值
    Console.WriteLine(" {0} {1} :", Thread.CurrentThread.GetHashCode(),
    ((SomeState)state).Cookie);
    Console.WriteLine("HashCount.Count=={0}, Thread.CurrentThread.GetHashCode()=={1}", HashCount.Count, Thread.CurrentThread.GetHashCode());
    lock (HashCount)
    {
    file://如果当前的Hash表中没有当前线程的Hash值,则添加之
    if (!HashCount.ContainsKey(Thread.CurrentThread.GetHashCode()))
      HashCount.Add (Thread.CurrentThread.GetHashCode(), 0);
    HashCount[Thread.CurrentThread.GetHashCode()] =
((int)HashCount[Thread.CurrentThread.GetHashCode()])+1;
    }
    int iX = 2000;
    Thread.Sleep(iX);
    //Interlocked.Increment()操作是一个原子操作,具体请看下面说明
    Interlocked.Increment(ref iCount);
    if (iCount == iMaxCount)
    {
    Console.WriteLine();
    Console.WriteLine("Setting eventX ");
    eventX.Set();
    }
  }
  }
  public class SimplePool
  {
  public static int Main(string[] args)
  {
    Console.WriteLine("Thread Pool Sample:");
    bool W2K = false;
    int MaxCount = 10;//允许线程池中运行最多10个线程
    //新建ManualResetEvent对象并且初始化为无信号状态
    ManualResetEvent eventX = new ManualResetEvent(false);
    Console.WriteLine("Queuing {0} items to Thread Pool", MaxCount);
    Alpha oAlpha = new Alpha(MaxCount); file://创建工作项
    //注意初始化oAlpha对象的eventX属性
    oAlpha.eventX = eventX;
    Console.WriteLine("Queue to Thread Pool 0");
    try
    {
    file://将工作项装入线程池
    file://这里要用到Windows 2000以上版本才有的API,所以可能出现NotSupportException异常
    ThreadPool.QueueUserWorkItem(new WaitCallback(oAlpha.Beta),
    new SomeState(0));
    W2K = true;
    }
    catch (NotSupportedException)
    {
    Console.WriteLine("These API's may fail when called on a non-Windows 2000 system.");
    W2K = false;
    }
    if (W2K)//如果当前系统支持ThreadPool的方法.
    {
    for (int iItem=1;iItem < MaxCount;iItem++)
    {
      //插入队列元素
      Console.WriteLine("Queue to Thread Pool {0}", iItem);
      ThreadPool.QueueUserWorkItem(new WaitCallback(oAlpha.Beta),new SomeState(iItem));
    }
    Console.WriteLine("Waiting for Thread Pool to drain");
    file://等待事件的完成,即线程调用ManualResetEvent.Set()方法
    eventX.WaitOne(Timeout.Infinite,true);
    file://WaitOne()方法使调用它的线程等待直到eventX.Set()方法被调用
    Console.WriteLine("Thread Pool has been drained (Event fired)");
    Console.WriteLine();
    Console.WriteLine("Load across threads");
    foreach(object o in oAlpha.HashCount.Keys)
    Console.WriteLine("{0} {1}", o, oAlpha.HashCount[o]);
    }
    Console.ReadLine();
    return 0;
  }
  }

程序中有些小地方应该引起我们的注意。SomeState类是一个保存信息的数据结构,在上面的程序中,它作为参数被传递给每一个线程,你很容易就能理解这个,因为你需要把一些有用的信息封装起来提供给线程,而这种方式是非常有效的。程序出现的InterLocked类也是专为多线程程序而存在的,它提供了一些有用的原子操作,所谓原子操作就是在多线程程序中,如果这个线程调用这个操作修改一个变量,那么其他线程就不能修改这个变量了,这跟lock关键字在本质上是一样的。
我们应该彻底地分析上面的程序,把握住线程池的本质,理解它存在的意义是什么,这样我们才能得心应手地使用它。下面是该程序的输出结果:

 

  Thread Pool Sample:
  Queuing 10 items to Thread Pool
  Queue to Thread Pool 0
  Queue to Thread Pool 1
  ...
  ...
  Queue to Thread Pool 9
  Waiting for Thread Pool to drain
  98 0 :
  HashCount.Count==0, Thread.CurrentThread.GetHashCode()==98
  100 1 :
  HashCount.Count==1, Thread.CurrentThread.GetHashCode()==100
  98 2 :
  ...
  ...
  Setting eventX
  Thread Pool has been drained (Event fired)
  Load across threads
  101 2
  100 3
  98 4
  102 1

 

与ThreadPool类不同,Timer类的作用是设置一个定时器,定时执行用户指定的函数,而这个函数的传递是靠另外一个代理对象TimerCallback,它必须在创建Timer对象时就指定,并且不能更改。定时器启动后,系统将自动建立一个新的线程,并且在这个线程里执行用户指定的函数。下面的语句初始化了一个Timer对象:

  Timer timer = new Timer(timerDelegate, s,1000, 1000); 

第一个参数指定了TimerCallback代理对象;第二个参数的意义跟上面提到的WaitCallback代理对象的一样,作为一个传递数据的对象传递给要调用的方法;第三个参数是延迟时间——计时开始的时刻距现在的时间,单位是毫秒;第四个参数是定时器的时间间隔——计时开始以后,每隔这么长的一段时间,TimerCallback所代表的方法将被调用一次,单位也是毫秒。这句话的意思就是将定时器的延迟时间和时间间隔都设为1秒钟。

定时器的设置是可以改变的,只要调用Timer.Change()方法,这是一个参数类型重载的方法,一般使用的原型如下:

  public bool Change(long, long);

 

下面这段代码将前边设置的定时器修改了一下:

   timer.Change(10000,2000); 
 

很显然,定时器timer的时间间隔被重新设置为2秒,停止计时10秒后生效。
下面这段程序演示了Timer类的用法。

   using System;
  using System.Threading;
  class TimerExampleState
  {
  public int counter = 0;
  public Timer tmr;
  }

  class App
  {
  public static void Main()
  {
    TimerExampleState s = new TimerExampleState();

    //创建代理对象TimerCallback,该代理将被定时调用
    TimerCallback timerDelegate = new TimerCallback(CheckStatus);

    //创建一个时间间隔为1s的定时器
    Timer timer = new Timer(timerDelegate, s,1000, 1000);
    s.tmr = timer;

    //主线程停下来等待Timer对象的终止
    while(s.tmr != null)
    Thread.Sleep(0);
    Console.WriteLine("Timer example done.");
    Console.ReadLine();
  }
  file://下面是被定时调用的方法

  static void CheckStatus(Object state)
  {
    TimerExampleState s =(TimerExampleState)state;
    s.counter++;
    Console.WriteLine("{0} Checking Status {1}.",DateTime.Now.TimeOfDay, s.counter);
    if(s.counter == 5)
    {
    file://使用Change方法改变了时间间隔
    (s.tmr).Change(10000,2000);
    Console.WriteLine("changed...");
    }
    if(s.counter == 10)
    {
    Console.WriteLine("disposing of timer...");
    s.tmr.Dispose();
    s.tmr = null;
    }
  }
  }


程序首先创建了一个定时器,它将在创建1秒之后开始每隔1秒调用一次CheckStatus()方法,当调用5次以后,在CheckStatus()方法中修改了时间间隔为2秒,并且指定在10秒后重新开始。当计数达到10次,调用Timer.Dispose()方法删除了timer对象,主线程于是跳出循环,终止程序。程序执行的结果如下:

上面就是对ThreadPool和Timer两个类的简单介绍,充分利用系统提供的功能,可以为我们省去很多时间和精力——特别是对很容易出错的多线程程序。同时我们也可以看到.net Framework强大的内置对象,这些将对我们的编程带来莫大的方便。

、互斥对象——更加灵活的同步方式
有时候你会觉得上面介绍的方法好像不够用,对,我们解决了代码和资源的同步问题,解决了多线程自动化管理和定时触发的问题,但是如何控制多个线程相互之间的联系呢?例如我要到餐厅吃饭,在吃饭之前我先得等待厨师把饭菜做好,之后我开始吃饭,吃完我还得付款,付款方式可以是现金,也可以是信用卡,付款之后我才能离开。分析一下这个过程,我吃饭可以看作是主线程,厨师做饭又是一个线程,服务员用信用卡收款和收现金可以看作另外两个线程,大家可以很清楚地看到其中的关系——我吃饭必须等待厨师做饭,然后等待两个收款线程之中任意一个的完成,然后我吃饭这个线程可以执行离开这个步骤,于是我吃饭才算结束了。事实上,现实中有着比这更复杂的联系,我们怎样才能很好地控制它们而不产生冲突和重复呢?
这种情况下,我们需要用到互斥对象,即System.Threading命名空间中的Mutex类。大家一定坐过出租车吧,事实上我们可以把Mutex看作一个出租车,那么乘客就是线程了,乘客首先得等车,然后上车,最后下车,当一个乘客在车上时,其他乘客就只有等他下车以后才可以上车。而线程与Mutex对象的关系也正是如此,线程使用Mutex.WaitOne()方法等待Mutex对象被释放,如果它等待的Mutex对象被释放了,它就自动拥有这个对象,直到它调用Mutex.ReleaseMutex()方法释放这个对象,而在此期间,其他想要获取这个Mutex对象的线程都只有等待。
下面这个例子使用了Mutex对象来同步四个线程,主线程等待四个线程的结束,而这四个线程的运行又是与两个Mutex对象相关联的。其中还用到AutoResetEvent类的对象,如同上面提到的ManualResetEvent对象一样,大家可以把它简单地理解为一个信号灯,使用AutoResetEvent.Set()方法可以设置它为有信号状态,而使用AutoResetEvent.Reset()方法把它设置为无信号状态。这里用它的有信号状态来表示一个线程的结束。

 

  // Mutex.cs
  using System;
  using System.Threading;
  public class MutexSample
  {
  static Mutex gM1;
  static Mutex gM2;
  const int ITERS = 100;
  static AutoResetEvent Event1 = new AutoResetEvent(false);
  static AutoResetEvent Event2 = new AutoResetEvent(false);
  static AutoResetEvent Event3 = new AutoResetEvent(false);
  static AutoResetEvent Event4 = new AutoResetEvent(false);
  public static void Main(String[] args)
  {
    Console.WriteLine("Mutex Sample ...");
    //创建一个Mutex对象,并且命名为MyMutex
    gM1 = new Mutex(true,"MyMutex");
    //创建一个未命名的Mutex 对象.
    gM2 = new Mutex(true);
    Console.WriteLine(" - Main Owns gM1 and gM2");
    AutoResetEvent[] evs = new AutoResetEvent[4];
    evs[0] = Event1; file://为后面的线程t1,t2,t3,t4定义AutoResetEvent对象
    evs[1] = Event2;
    evs[2] = Event3;
    evs[3] = Event4;
    MutexSample tm = new MutexSample( );
    Thread t1 = new Thread(new ThreadStart(tm.t1Start));
    Thread t2 = new Thread(new ThreadStart(tm.t2Start));
    Thread t3 = new Thread(new ThreadStart(tm.t3Start));
    Thread t4 = new Thread(new ThreadStart(tm.t4Start));
    t1.Start( );// 使用Mutex.WaitAll()方法等待一个Mutex数组中的对象全部被释放
    t2.Start( );// 使用Mutex.WaitOne()方法等待gM1的释放
    t3.Start( );// 使用Mutex.WaitAny()方法等待一个Mutex数组中任意一个对象被释放
    t4.Start( );// 使用Mutex.WaitOne()方法等待gM2的释放
    Thread.Sleep(2000);
    Console.WriteLine(" - Main releases gM1");
    gM1.ReleaseMutex( ); file://线程t2,t3结束条件满足
    Thread.Sleep(1000);
    Console.WriteLine(" - Main releases gM2");
    gM2.ReleaseMutex( ); file://线程t1,t4结束条件满足
    //等待所有四个线程结束
    WaitHandle.WaitAll(evs);
    Console.WriteLine("... Mutex Sample");
    Console.ReadLine();
  }
  public void t1Start( )
  {
    Console.WriteLine("t1Start started, Mutex.WaitAll(Mutex[])");
    Mutex[] gMs = new Mutex[2];
    gMs[0] = gM1;//创建一个Mutex数组作为Mutex.WaitAll()方法的参数
    gMs[1] = gM2;
    Mutex.WaitAll(gMs);//等待gM1和gM2都被释放
    Thread.Sleep(2000);
    Console.WriteLine("t1Start finished, Mutex.WaitAll(Mutex[]) satisfied");
    Event1.Set( ); file://线程结束,将Event1设置为有信号状态
  }
  public void t2Start( )
  {
    Console.WriteLine("t2Start started, gM1.WaitOne( )");
    gM1.WaitOne( );//等待gM1的释放
    Console.WriteLine("t2Start finished, gM1.WaitOne( ) satisfied");
    Event2.Set( );//线程结束,将Event2设置为有信号状态
  }
  public void t3Start( )
  {
    Console.WriteLine("t3Start started, Mutex.WaitAny(Mutex[])");
    Mutex[] gMs = new Mutex[2];
    gMs[0] = gM1;//创建一个Mutex数组作为Mutex.WaitAny()方法的参数
    gMs[1] = gM2;
    Mutex.WaitAny(gMs);//等待数组中任意一个Mutex对象被释放
    Console.WriteLine("t3Start finished, Mutex.WaitAny(Mutex[])");
    Event3.Set( );//线程结束,将Event3设置为有信号状态
  }
  public void t4Start( )
  {
    Console.WriteLine("t4Start started, gM2.WaitOne( )");
    gM2.WaitOne( );//等待gM2被释放
    Console.WriteLine("t4Start finished, gM2.WaitOne( )");
    Event4.Set( );//线程结束,将Event4设置为有信号状态
  }
  } 

下面是该程序的执行结果:

从执行结果可以很清楚地看到,线程t2,t3的运行是以gM1的释放为条件的,而t4在gM2释放后开始执行,t1则在gM1和gM2都被释放了之后才执行。Main()函数最后,使用WaitHandle等待所有的AutoResetEvent对象的信号,这些对象的信号代表相应线程的结束。
六、小结
多线程程序设计是一个庞大的主题,而本文试图在.net Framework环境下,使用最新的C#语言来描述多线程程序的概貌。希望本文能有助于大家理解线程这种概念,理解多线程的用途,理解它的C#实现方法,理解线程将为我们带来的好处和麻烦。C#是一种新的语言,因此它的线程机制也有许多独特的地方,希望大家能通过本文清楚地看到这些,从而可以对线程进行更深入的理解和探索。

posted @ 2009-12-04 18:19 陈星 阅读(10) 评论(0) 编辑
摘要: 原题:用递归求第10个数,它等于前2数之和,如{1,1,2,3,5} 得到递归式为f(n)=f(n-1)+f(n-2),终止条件为f(0)=1, f(1)=1。求的数为f(9)。 1 #include <iostream>2 using namespace std;34 int f(int n)5 {6 if (n==1 || n==0)7 {8 return 1;9 }10 ...阅读全文
posted @ 2009-11-27 16:36 陈星 阅读(923) 评论(0) 编辑
摘要: 虽然有很多种方式可以解决这个问题,但 是我们可以用T-SQL代码来处理这个文件删除过程。我用xp_cmdshell命令和FORFILES命令来处理类似于你所列出的过程。可以从 Microsoft TechNet中获得FORFILES的可靠信息,但是在这篇文章中,我将涉及FORFILES大量的结构和使用方法来达到你的目的。  该FORFILES命令将选取文件的一个子集并且针对这个子集执行一个命令。这...阅读全文
posted @ 2009-11-04 11:23 陈星 阅读(38) 评论(0) 编辑
摘要: C#日期格式化 from: http://51xingfu.blog.51cto.com/219185/46222 日期转化一 为了达到不同的显示效果有时,我们需要对时间进行转化,默认格式为:2007-01-03 14:33:34 ,要转化为其他格式,要用到DateTime.ToString的方法(String, IFormatProvider),如下所示: using System; u...阅读全文
posted @ 2009-11-04 10:39 陈星 阅读(10) 评论(0) 编辑
摘要: 主要为了学习一下相关的网络蜘蛛,为自己获取信息使用背景 随着Internet的普及,网络信息正以极高的速度增长,在这么多数据中找到自己需要的信息是一件很繁琐的事情,找到需要的信息后如何获取也是件麻烦的事。这就需要Internet信息抓取程序来代替人工的操作。 所谓Internet信息抓取程序,就是程序会按照用户的关键词或关键网站来收集相应的信息,并提供给用户想要的信息格式。 信息量的增加会带来...阅读全文
posted @ 2009-10-29 17:27 陈星 阅读(48) 评论(0) 编辑
摘要: 原文转自:http://www.csharp.net.cn/post/三种模拟自动登录和提交POST信息的实现方法.html 网页自动登录(提交Post内容)的用途很多,如验证身份、程序升级、网络投票等,以下是用C#实现的方法。 网页自动登录和提交POST信息的核心就是分析网页的源代码(HTML),在C#中,可以用来提取网页HTML的组件比较多,常用的用WebBrowser、WebClient、H...阅读全文
posted @ 2009-10-29 17:24 陈星 阅读(81) 评论(0) 编辑