欢迎到我的博客中阅读独立版本:http://www.dozer.cc/2012/03/async-and-await-in-asp-net-beta/

 

发现问题

在我的上一篇文章《async 与 await 在 Web 下的应用》中,我提到了 asp.net 4.5 在 Web.Config 中的一个奇怪配置:

<appSettings>
  <add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" />
</appSettings>

在 Stack Overflow 上提问后,终于有人回答我了。

看了别人的回复后,才发现了我上篇文章中的问题。

下面代码中的这种用法是错误的:

protected async void Page_Load(object sender, EventArgs e)
{
    WebClient client = new WebClient();
    var result1 = await client.DownloadStringTaskAsync("http://www.website.com");
    WebClient client2 = new WebClient();
    var result2 = await client.DownloadStringTaskAsync(result1);
    //do more
}

 

在事件上直接使用 async 引发的错误

代码段一:

public partial class WebForm1 : System.Web.UI.Page
{
    protected string Msg { get; set; }
    protected async void Page_Load(object sender, EventArgs e)
    {
        using (WebService service = new WebService())
        {
            Msg = await service.Method1TaskSync();
        }
    }

    protected async void Button_Test_Click(object sender, EventArgs e)
    {
        using (WebService service = new WebService())
        {
            Msg = await service.Method2TaskSync();
        }
    }
}

试问,最后的 Msg 的值是什么?应该是哪个方法的返回值?

如果去掉异步,那答案肯定是 Method2。那加上异步后呢?

这里用的是 async 和 await 来实现了异步,所以逻辑上的先后次序应该和代码上的先后次序一样。

但是上述代码两个事件会一起执行!导致了一定的问题!

总结一下上面代码的问题:当页面中的 Page_Load 事件和别的事件都用了 async 和 await 后会出现执行次序错误、死锁等问题。它们并不会按次序执行。

 

代码段二:

<appSettings>
  <add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" />
</appSettings>

 

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm1.aspx.cs" Inherits="AsyncAwait.WebForm1"
    Async="true" %>

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <%:Msg %>
    </div>
    </form>
</body>
</html>

后端代码和上面一样的代码,只不过把 UseTaskFriendlySynchronizationContext 的配置改成了 true,并且把数据显示到了页面上。

执行后发现:根本无法显示内容,页面在异步执行结束前就已经输出完毕了。

 

UseTaskFriendlySynchronizationContext 的作用和错误引发的原因

其实在老外的回答中已经说明了全部,我这里主要是翻译+精简一下。

 

UseTaskFriendlySynchronizationContext 的作用:

之前版本的 asp.net 所使用的异步不符合 CLR 的规范,而只有 RegisterAsyncTask 这个方法是符合 CLR 规范的。

所以 asp.net 4.5 中,加入这个新的配置是为了禁用掉之前不符合约定的功能,只要把这个配置设置为了 true,别的异步方案全部会失效。(代码段二主要就是演示了这个现象)

 

引发错误的原因:

async 和 await 关键字在底层主要是利用 SynchronizationContext 来实现了异步。(具体原理我也没研究过)

而这个方案首先不符合 CLR 规范,另外也会引起很多问题。(代码段一主要就是演示了其中一个问题)

 

目前正确的写法

首先,建议把 UseTaskFriendlySynchronizationContext 设置为 true。

另外,正确的写法如下:

public partial class WebForm1 : System.Web.UI.Page
{
    protected string Msg { get; set; }
    protected void Page_Load(object sender, EventArgs e)
    {
        RegisterAsyncTask(new PageAsyncTask(Method1));

    }
    private async Task Method1()
    {
        using (WebService service = new WebService())
        {
            Msg = await service.HelloWorldTaskSync();
        }
    }

    protected void Button_Test_Click(object sender, EventArgs e)
    {
        RegisterAsyncTask(new PageAsyncTask(Method2));
    }

    private async Task Method2()
    {
        using (WebService service = new WebService())
        {
            Msg = await service.HelloWorldTaskSync();
        }
    }
}

如果需要写异步,一定要用 RegisterAsyncTask 方法,实测证明,支持多次调用,而且会按次序执行。

 

老外说了,他们也想直接在事件上加 async 来写,但是由于技术原因并没有实现,希望在正式版或者未来的版本中可以实现吧!

 

参考资料:

http://stackoverflow.com/questions/9562836/whats-the-meaning-of-usetaskfriendlysynchronizationcontext

http://social.msdn.microsoft.com/Forums/en-NZ/async/thread/b2e8c51e-2808-46d0-92e9-b825321d0af8

posted @ 2012-03-14 20:10 Dozer 阅读(815) 评论(3) 编辑
摘要: 关于 .net 的异步,一篇文章是讲不完的,我这里就贴两篇文章让大家看一下: 《正确使用异步操作》、《C#客户端的异步操作》、《细说ASP.NET的各种异步操作》 另外,在 .net 4.0 中还推出了新的任务并行库(TPL),也是一种新异步模式: 《任务并行库》 最后,.net 4.5 又推出了全新的 async 和 await 关键字: 《C#与Visual Basic的未来(上)》 《C#与Visual Basic的未来(中)》 《C#与Visual Basic的未来(下)》 最后,在这几篇文章的基础上,想和大家谈谈 async 和 await 在 Web 下的应用,包括 WebForm 和 MVC。阅读全文
posted @ 2012-03-06 19:39 Dozer 阅读(1460) 评论(3) 编辑
摘要: 大家刚学编程的时候,一定还记得为什么要用函数。那就是把重复的代码归纳到一个函数中多次利用。这点毋庸置疑,大家也用的很熟了,但是除了这个还有什么改进空间吗?答案肯定是有的!阅读全文
posted @ 2012-01-22 22:26 Dozer 阅读(1147) 评论(2) 编辑

如果排版不好,可以到我个人博客上看,我是现在个人博客上写的,欢迎捧场^^:http://www.dozer.cc

 

ref 和 out 的区别

网上有很多这方面的文章,但是大部分人总是纠结于他们在原理上的那一点点细微的区别,所以导致了难以区分它们,也不知道什么时候改用哪一个了。

但是如果从试用场景的角度对它们进行区别的话,以后你一定不会再纠结了。

当你明白它们的适用场景后,再去扣其中的原理,使用中的一些问题也就迎刃而解了~

 

简单的来说,它们的区别在于:

ref 关键字 是作用是把一个变量的引用传入函数,和 C/C++ 中的指针几乎一样,就是传入了这个变量的栈指针。

out 关键字 的作用是当你需要返回多个变量的时候,可以把一个变量加上 out 关键字,并在函数内对它赋值,以实现返回多个变量。

 

 

几个简单的演示

上面说了 ref 和 out 的作用,非常简单,但是在具体使用的时候却遇到了很多麻烦,因为 C# 中本身就区分了引用类型和值类型。

我先举几个例子,来看看会出现哪些诡异的情况

 

代码段一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void Main(string[] args)
{
    int a;
    Test1(out a);//编译通过
 
    int b;
    Test2(ref b);//编译失败
}
 
static void Test1(out int a)
{
    a = 1;
}
static void Test2(ref int b)
{
    b = 1;
}

这两个关键字看起来用法一样,为什么会有合格现象?

网上的答案很简单:out 关键字在传入前可以不赋值,ref 关键字在传入前一定要赋值。

这是什么解释?受之于鱼但并没有授之予渔!这到底是为什么呢?

想知道背后真正原理的呢,就继续看下去吧,后面我讲会讲到这里的区别。

 

代码二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static void Main(string[] args)
{
    object a = new object(), b = new object(), c = new object();
 
    Test1(out a);
    Test2(ref b);
    Test3(c);
    //最终 a,b,c 分别是什么?
    //a,b = null
    //c 还是 object
}
 
static void Test1(out object a)
{
    a = null;
}
static void Test2(ref object b)
{
    b = null;
}
static void Test3(object c)
{
    c = null;
}

新建三个 object,object是引用类型;三个函数,分别是 out,ref和普通调用;执行了一样的语句;最后的结果为什么是这样呢?

如果你只是从浅层次理解了 out 和 ref 的区别,这个问题你一定回答不上了。(我以前也不知道)

所以,这是为什么呢?继续往下看。

 

^_^ 相信很多人晕了,我的目的达到了。(邪恶的笑~~)

那么,下面,我为大家从两个角度来分析一下。

对于值类型来说,加 out、加 ref 和什么都不加有什么共同点和区别?

对于引用类型来说,加 out、加 ref 和什么都不加有什么共同点和区别?

 

 

问题一:关于值类型

普通的传递值类型很简单了,传的只是一个值,没难度,平时都是这么用的,很好区分,所以这里就不惨和进去了。

接下来是 ref 和 out 的区别,为什么要了解区别呢?当然是为了了解怎么用它们,简单的来说就是需要了解:什么时候该用哪个。

 

个人总结有几个原则:

如果你是为了能多返回一个变量,那么就应该用 out:

用 out 关键字有几个好处:可以不关心函数外是否被赋值,并且如果在函数内没有赋值的话就会编译不通过。(提醒你一定要返回)

你可以把它当成是另一种形式的 return 来用,我们来做一个类比:

return 语句的特点:接收 return 的变量事先不需要赋值(当然如果赋值了也没关系),在函数内必须 return。

可以看到 out 关键字的作用、行为、特点 和 return 是完全一样的。因为它当初设计的目的就是为了解决这个问题的。

 

如果你想要像引用类型那样调用值类型,那你就可以 ref:

传入值类型的引用后,你可以用它,也可以不用它,你也可以重新修改它的各个属性,而函数外也可以随之改变。

我们来把 “传值类型的引用” 和 “传引用类型” 来做一个类比:

1
2
3
4
5
6
7
8
9
10
11
static void Main(string[] args)
{
    int a;
    Test1(ref a);//错误   1   使用了未赋值的局部变量“a”
 
    object b;
    Test2(b);//错误   2   使用了未赋值的局部变量“b”
}
static void Test1(ref int a) { }
 
static void Test2(object b) { }

传入加了 ref 的值类型 和 传入一个引用类型 的作用、行为、特点都是类似的。

同样,他们同时要遵守一个原则:传入前必须赋值,这个是为什么呢?

如果赋值后,传入两个函数的分别是 int a 的指针 和 object b 的指针。

而不赋值的话,a 和 b 根本还不存在,那它们又怎么会有地址呢?

 

注意:如果你只写了 object a ,而在后面的代码中没有赋值,它并没有真正地分配内存。

我们可以看一下三个操作的 IL 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
private static void Main(string[] args)
{
    //IL_0000: nop
    object a;//没做任何事
 
    //IL_0002: ldnull
    //IL_0003: stloc.1
    object b = null;//在栈中增加了一个指针,指向 null
 
    //IL_0004: newobj instance void [mscorlib]System.Object::.ctor()
    //IL_0009: stloc.2
    object c = new object();//在栈中增加了一个指针,指向新建的 object 对象
}

传入引用类型的目的是把一个已经存在的对象的地址传过去,而如果你只是进行了 object a 声明,并没做复制,这行代码跟没做任何事!

所以,除非你使用了 out 关键字,在不用关键字和用 ref 关键字的情况下,你都必须事先复制。 out 只是一种特殊的 return

 

总结:

现在你是否明白,当变量什么情况下该用什么关键字了吗?其实有时候 ref 和 out 都可以达到目的,你需要根据你的初衷,和它们的特点,来衡量一下到底使用哪个了!

另外,我们来看看两个同样的函数,用 out 和 ref 时候的 IL 代码

原函数:

1
2
3
4
5
6
7
8
private static void Test1(out int a)
{
    a = 1;
}
private static void Test2(ref int a)
{
    a = 1;
}

IL代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
.method private hidebysig static
    void Test1 (
        [out] int32& a
    ) cil managed
{
    // Method begins at RVA 0x2053
    // Code size 5 (0x5)
    .maxstack 8
 
    IL_0000: nop
    IL_0001: ldarg.0
    IL_0002: ldc.i4.1
    IL_0003: stind.i4
    IL_0004: ret
} // end of method Program::Test1
 
.method private hidebysig static
    void Test2 (
        int32& a
    ) cil managed
{
    // Method begins at RVA 0x2059
    // Code size 5 (0x5)
    .maxstack 8
 
    IL_0000: nop
    IL_0001: ldarg.0
    IL_0002: ldc.i4.1
    IL_0003: stind.i4
    IL_0004: ret
} // end of method Program::Test2

发现了吗? 它们在函数内部完全是一样的!因为他们的原理都是传入了这个变量的引用。只是 out 关键字前面出现了一个标记 [out]

它们在原理上的区别主要在于编译器对它们进行了一定的限制。

最上面“代码段一”中的问题你现在明白了吗?

 

 

问题二:关于引用类型

对于值类型来说,最难区别的是 ref 和 out,而对于引用类型来说就不同了。

首先,引用类型传的是引用,加了 ref 以后也是引用,所以它们是一样的?暂时我们就这么认为吧~ 我们暂时认为它们是一样的,并统称为:传引用。

所以,对于引用类型来说,out 和 传引用 的区别跟对于值类型传 ref 和 out 的区别类似,具体适用场景也和值类型类似,所以就不多加阐述了。

 

虽然我们说直接传和加 ref 都可以统称为传引用,但是它们还是有区别的!而且是一个很隐蔽的区别。

我们再来看一下最上面的代码段二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static void Main(string[] args)
{
    object a = new object(), b = new object(), c = new object();
 
    Test1(out a);
    Test2(ref b);
    Test3(c);
    //最终 a,b,c 分别是什么?
    //a,b = null
    //c 还是 object
}
 
static void Test1(out object a)
{
    a = null;
}
static void Test2(ref object b)
{
    b = null;
}
static void Test3(object c)
{
    c = null;
}

out 关键字就相当于 return ,所以内部赋值为 null ,就相当于 return 了 null

可是,为什么引用类型还要加 ref 呢?它本身部已经是引用了吗?为什么加了以后就会有天大的区别呢?!

 

用一句话概括就是:不加 ref 的引用是堆引用,而加了 ref 后就是栈引用! @_@ 好搞啊。。什么跟什么?让我们一步步说清楚吧!

 

正常的传递引用类型:

加了 ref 的传递引用类型:

这两张图对于上面那句话的解释很清楚了吧?

如果直接传,只是分配了一个新的栈空间,存放着同一个地址,指向同一个对象。

内外指向的都是同一个对象,所以对 对象内部的操作 都是同步的。

但是,如果把函数内部的 obj2 赋值了 null,只是修改了 obj2 的引用,而 obj1 依然是引用了原来的对象。

所以上面的例子中,外部的变量并没有收到影响。

同样,如果内部的对象作了  obj2 = new object() 操作以后,也不会对外部的对象产生任何影响!

 

而加了 ref  后,传入的不是 object 地址,传入的是 object 地址的地址!

所以,当你对 obj2 赋 null 值的时候,其实是修改了 obj1 的地址,而自身的地址没变,还是引用到了 obj1

 

虽然在函数内部的语句是一样的,其实内部机制完全不同。我们可以看一下IL代码,一看就知道了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
.method private hidebysig static
    void Test1 (
        object a
    ) cil managed
{
    // Method begins at RVA 0x2053
    // Code size 5 (0x5)
    .maxstack 8
 
    IL_0000: nop
    IL_0001: ldnull
    IL_0002: starg.s a
    IL_0004: ret
} // end of method Program::Test1
 
.method private hidebysig static
    void Test2 (
        object& a
    ) cil managed
{
    // Method begins at RVA 0x2059
    // Code size 5 (0x5)
    .maxstack 8
 
    IL_0000: nop
    IL_0001: ldarg.0//多了这行代码
    IL_0002: ldnull
    IL_0003: stind.ref
    IL_0004: ret
} // end of method Program::Test2

上面是直接传入,并赋 null 值的

下面是加 ref 的

我们可以发现仅仅是多了一行代码:IL_0001: ldarg.0

其实,这样代码的作用就是讲参数0加载到堆栈上,也就是先根据引用,找到了外部的变量,然后再根据外部的变量,找到了最终的对象!

 

那现在你知道什么时候该加 ref,什么时候不用加 ref 了吗?

再看了一个例子:

1
2
3
4
5
6
7
8
private static void Test1(List<int> list)
{
    list.Clear();
}
private static void Test2(ref List<int> list)
{
    list = new List<int>();
}

同样是清空一个 List,如果没加 ref ,只能用 clear。

而加了 ref 后可以直接 new 一个新的~

如果你没加 ref 就直接 new 一个新的了,抱歉,外部根本不知道有这个东西,你们操作的将不是同一个 List

 

所以,你一定要了解这点,并注意一下几件事:

1、一般情况下不要用 ref

2、如果你没加 ref,千万别直接给它赋值,因为外面会接收不到…

 

 

现在你全部明白了吗?^_^

 

原文地址:把 ref 和 out 关键字说透

posted @ 2011-10-28 17:03 Dozer 阅读(1821) 评论(12) 编辑
摘要: 最近想研究如何自定义 LINQ Provider ,但是一直无法入手,先写点收获吧~MSDN 上的这篇文章(《启用数据源以进行 LINQ 查询》)中写到:如果想对自己的数据源进行 LINQ 查询,那必须使用一下四种方法的其中一种。实现 IEnumerable<T> 接口实现标准的查询方法实现 IQueryable<T> 接口扩展已经实现的 LINQ 查询看到其中第二条,让人心生疑惑,那下面就来探讨一下吧~什么类型可以进行 LINQ 查询?123var queryLondonCustomers = from cust in customerswhere cust.City阅读全文
posted @ 2011-08-07 00:21 Dozer 阅读(1254) 评论(7) 编辑
摘要: 前言导航高亮一直是一个让大家头疼的问题。纯 Javascript 的话可以判断当前页面的地址和链接地址是否有关系。这样的弊端就是自由度太低,MVC 下会出一定的问题 (MVC 下有默认的 Controller 和 Action)另一种方案是前端后端结合,为每一张页面设置一个属性,然后在页面中判断。满足条件便高亮。这样的弊端就是,需要为你所有的页面设置属性,非常麻烦!那么有没有什么完美的解决方案?一劳永逸的?神奇的 eval 函数Javascript 有精粹也有糟粕,其中的 eval 是大多数动态语言都拥有的精髓。我们是否可以利用这个函数呢?基本思路:给每一个 li(对应一个链接)设置一个 cl阅读全文
posted @ 2011-03-21 14:07 Dozer 阅读(2435) 评论(24) 编辑
摘要: 目录:开篇通讯原理:UCenter API与子站之间的通讯原理和单点登陆原理加密与解密:AuthCode详解 & AuthCode函数翻译过程中的注意点网站搭建: 康盛旗下网站 & Asp.net 网站搭建阅读全文
posted @ 2011-01-26 23:47 Dozer 阅读(1631) 评论(7) 编辑
摘要: 开篇无论是 Asp.net 还是 MVC 中,想要设置网站的 Title 或者 Sitemap (不用控件)总是很麻烦。Title 和 Sitemap 都是有关联的,所以有什么办法可以 Write once, run anywhere 呢?先看一下效果和用法吧~[效果][用法:Controller中][用法:View中][用法总结]只要在 Controller 和 Action 上加上 Attri...阅读全文
posted @ 2010-12-05 20:26 Dozer 阅读(1374) 评论(11) 编辑
摘要: Unobtrusive JavaScript 是什么?以上的代码分别是 MVC 3 在“关闭”和“开启” Unobtrusive JavaScript 后生成的 Ajax.ActionLink。那 Unobtrusive JavaScript到 底是什么呢?简单地来说,就是一种代码分离的思想,把行为层和表现层分离开。具体的可以查看维基百科下对Unob...阅读全文
posted @ 2010-11-11 12:14 Dozer 阅读(3461) 评论(18) 编辑
摘要: 先声明一下,这个只是我的一个设想,目前还没实现JS压缩?CSS压缩?这个大家肯定都听说过,一个成熟的网站,JS压缩和CSS压缩是必须的,也是一个很好的提高网站性能的途径。但是,压缩好的JS和CSS怎么编辑呢?好吧,可以用支持JS或者CSS自动格式化的编辑器,让它还原…然后,编辑好后再压缩…不觉得很麻烦吗?Visual Studio 下的解决方案先看一张图:这是什么?一个...阅读全文
posted @ 2010-10-31 06:41 Dozer 阅读(2335) 评论(13) 编辑