Hello World
Spiga

PDC 2010:C#与Visual Basic的未来(上)

2010-10-31 02:43 by 老赵, 8375 visits

PDC不愧为微软最高级的技术人员专业会议,看得我直呼过瘾。前几天在PDC 2010会议上Anders Hejlsberg发表了一场名为“The Future of C# and Visual Basic”的演说,谈论了未来C#和VB中最为重要的两个特性:“异步(Async)”及“编译器即服务(Compiler as a Service)”。我现在对这场演讲进行总结,但不会像上次《编程语言的发展趋势及未来方向》那样逐句翻译,而是以Anders的角度使用一种简捷合适的方式表述其完整内容。

在2000年的PDC上,我们给大家带来了一个全新的平台“.NET”,以及一个语言“C#”。.NET与C#每次发布时都有一个“主题”,一开始是“托管代码”,接着是“泛型”,然后是“LINQ”,直到最近的“动态性”,这就是C#和VB的演变过程。这两种语言面向的用户比较相近,微软也承诺会同时发展两种语言。因此这个演讲虽然以C#作为主题,但其实也会在VB中得以体现。

作为语言的设计者,要设法将工业界所重视的内容,使用语言表现出来,因此也有了这样的分类。“声明式”代表了一种编程的趋势,尽可能表现出“做什么”而不是“怎么做”,于是有了函数式编程与DSL等等。然后,目前研究的热门之一则是动态语言,如Python,Ruby,JavaScript等等,以及它们是如何影响静态语言的。还有便是“并发”,这里所指的广义的“并发”,包括单机上多核以及云或是数据中心上分布式系统等等,也就是各种“同时处理”的方式。

我们可以清楚地看到,C# 3.0和VB 9中的函数式编程,LINQ等特性体现了“声明式”,而C# 4.0和VB 10则出现了动态性,但都没有太多关于“并发”的成分在里面──它都体现在框架中了,例如.NET 4包含了任务并行库(Task Parallel Library),但对于语言来说,除了lock似乎就没有什么这方面的支持了。

如今对“并发”的需求已经是毋庸质疑的,很少有一个应用程序或是服务不需要连接外部系统。这种与外部系统,例如互联网进行交互的行为则增加了应用程序的延迟,这可能导致UI在和外部服务交互时长时间失去响应。而对于一个数据中心的服务,您可能就会发现CPU的利用率不高,因为系统都在等待其他服务的回复了。

为了解决这个问题,我们往往会使用“异步”的编程方式,它逐渐已经成为“高响应度”,“高伸缩性”的代名词了。此外还有一些API只提供了异步的版本,例如在JavaScript中发起HTTP请求,或是Silverlight的网络交互方面。这种情况以后只会越来越普遍。

于是下一版本的C#和VB就会在这里有所行动,目前会展示一下我们的早期工作,希望可以得到一些反馈。

说到异步化,您可以简单认为“一起运行”。一个同步方法,好比DownloadString,应用程序会执行这个方法,并等待结果返回,但是你不能把工作的执行过程与结果的送达区分开来。而对于异步编程来说,DownloadStringAsync在调用之后便会立即返回,过了一段时间,结果就会传递过来,于是执行过程和结果的送达便完全是可分离的了。而对于如今典型的异步模型来说,结果通过一个回调函数传递过来。

异步化可以的得到高度的响应能力,因为在等待任务的结果时我们可以做其他一些事情。而对于服务器来说,异步可以带来很好的伸缩性,因为线程得到释放了,而不需要等待请求返回结果。

通过图示可以更清楚地了解这点。例如有段代码叫做DownloadData,调用以后可以得到一些数据。在执行时,线程会有长时间的终止,它被阻塞了,要等到结果返回之后才能继续处理数据。与此相对的是其异步的版本,我们调用DownloadDataAsync方法之后,它立即将控制权交还给我们,过了一段时间,它会把结果传递给回调函数,让我们继续处理下去。但是在DownloadData和ProcessData之间,我们可以处理其他一些工作。如果这是UI线程,那么就可以用于响应其他用户操作。如果这是个服务器线程,那么在等待结果时这个线程可以用来处理其他请求。

那么,如果我们要执行多个请求,例如要调用两遍,对于同步的版本就会获得双倍的阻塞,即便两个请求是完全独立的。而在异步的情况下,我们可以快速地发出两个请求,这样便形成的并发,即便这里并没有使用额外的线程。于是便可以更快地得到结果,也能保证响应能力。

有人可能会说,我们可以利用后台线程来得到响应。没错,不过就引入了多线程模型,于是就要处理同步等线程安全问题。而且,在开发带有UI的应用程序时,我们不能在后台线程里操作UI,这样又出现了其他的复杂情况。而在服务器应用中,我们又不希望创建更多的线程,因为这会给线程池带来压力,线程之间会有竞争,就会降低请求的处理能力。

以上便是对异步编程的概述,您可能会问,既然异步有那么多好处,那么为什么不把所有的应用程序都写作异步的呢?那么现在我们就来看一下异步编程大概是什么样子的。

这里有个简单的应用程序,输入年份,可以下载到那一年的电影。现在这个程序是同步的写法。在搜索的时候UI会失去响应,这样的结果显然无法令人接受,我们要做的更好。我们可以将其改写为异步的形式。

同步的写法是这样的:

private void searchButton_Click(object sender, RoutedEventArgs e)
{
    LoadMovies(Int32.Parse(textBox.Text));
}

void LoadMovies(int year)
{
    resultsPanel.Children.Clear();
    statusText.Text = "";
    var pageSize = 10;
    var imageCount = 0;

    while (true)
    {
        var movies = QueryMovies(year, imageCount, pageSize);
        if (movies.Length == 0) break;
        DisplayMovies(movies);
        imageCount += movies.Length;
    }

    statusText.Text = String.Format("{0} Titles", imageCount);
}

Movie[] QueryMovies(int year, int first, int count)
{
    var client = new WebClient();
    var url = String.Format(query, year, first, count);
    var data = client.DownloadString(new Uri(url));

    var movies =
        from entry in XDocument.Parse(data).Desendanies(xs + "entry")
        let properties = entry.Element(xm + "properties")
        select new Movie
        {
            /* ... */
        };

    return movies.ToArray();
}

在点击按钮以后会调用LoadMovies方法,它会在一个循环中不断使用QueryMovies方法进行查询,在QueryMovies方法中我们使用WebClient下载一个XML,解析,构造Movie对象并返回,最终呈现在界面上。

下载时我们使用DownloadString方法,这是个同步方法,我们要把它修改成异步的方式。事实上还真有个异步的方法,叫做DownloadStringAsync,不过这就需要我们修改代码,例如要把QueryMovies中的大部分放入DownloadStringCompleted事件的处理函数中。同时,异步编程的痛苦慢慢体现出现了,我们无法返回数据,而必须传递到某个地方,于是QueryMovies方法则要返回void,并接受一个回调函数。

void QueryMovies(int year, int first, int count, Action<Movie[]> action)
{
    var client = new WebClient();
    var url = String.Format(query, year, first, count);

    client.DownloadStringCompleted += (sender, e) =>
    {
        var data = e.Result;
        var movies =
            from entry in XDocument.Parse(data).Descendants(xs + "entry")
            let properties = entry.Element(xm + "properties")
            select new Movie
            {
                /* ... */
            };

        action(movies.ToArray());
    };

    client.DownloadStringAsync(new Uri(url));
}

然后我们还需要处理QueryMovies的调用者,这里实在麻烦到家了,因为我们使用了一个while循环来查询电影,那么我们又该如何反复调用一个异步方法?

void LoadMovies(int year)
{
    resultsPanel.Children.Clear();
    statusText.Text = "";
    var pageSize = 10;
    var imageCount = 0;

    Action<Movie[]> action = null;
    action = movies =>
    {
        if (movie.Length > 0)
        {
            DisplayMovie(movies);
            imageCount += movies.Length;
            QueryMovies(year, imageCount, pageSize, action);
        }
        else
        {
            statusText.Text = String.Format("{0} Titles", imageCount);
        }
    };

    QueryMovies(year, imageCount, pageSize, action);
}

你一定已经发现了,现在的代码已经很难让人保持愉快了。不过它的确是异步的了,运行时界面响应良好。效果是有了,不过这代码变得乱七八糟。想象一下,如果要加上异常处理该怎么做?我们可能要提供两个回调函数,一个处理正常情况,一个处理错误,还到处需要有try...catch,很快麻烦就会接踵而来了。如果不想面对这些麻烦,你可能就要去启用后台线程,这样又有了线程方面的问题。

显然我们可以做的更好。首先让我们回到原来的同步代码,然后再用上我们为异步编程设计的新特性。

如果要把QueryMovies变为异步,则先把它的返回值改为Task<Movie[]>,你如果了解.NET 4则一定已经知道这个类型是任务并行库的一部分。事实上Task类型只是表示一个“开始计算并在未来返回结果”的任务,因此Task<T>表示一个会在将来返回T类型的计算任务,在科学计算领域这通常被称为Future或是Promise。现在方法的返回值是Task<Movie[]>,而最后返回的是Movie[],这显然不匹配,但我们可以将其标记为一个async方法。对于async方法,编译器会重写整个方法实现来表示一个异步任务,以后我们会来观察它是如何实现这点的。

async Task<Movie[]> QueryMoviesAsync(int year, int first, int count)
{
    var client = new WebClient();
    var url = String.Format(query, year, first, count);
    var data = client.DownloadString(new Uri(url));

    var movies =
        from entry in XDocument.Parse(data).Descendants(xs + "entry")
        let properties = entry.Element(xm + "properties")
        select new Movie
        {
            /* ... */
        };

    return movies.ToArray();
}

不过只做到这点还不够,我们的方法还没有异步化,这还是个同步任务。不过,如今在一个async方法中,我们有能力组合调用另一个async方法,并异步地等待。这里使用了一个扩展方法DownloadStringTaskAsync,以后也会包含在框架中。这个方法返回Task<string>类型,表示未来某一时刻将会得到一个string对象。于是在async方法中,我们使用一个新的await操作符来等待其返回。

async Task<Movie[]> QueryMoviesAsync(int year, int first, int count)
{
    var client = new WebClient();
    var url = String.Format(query, year, first, count);
    var data = await client.DownloadStringTaskAsync(new Uri(url));

    var movies =
        from entry in XDocument.Parse(data).Descendants(xs + "entry")
        let properties = entry.Element(xm + "properties")
        select new Movie
        {
            /* ... */
        };

    return movies.ToArray();
}

在执行时,方法会执行到await操作符这里,并确保接下来的代码是在一个回调函数/continuation中执行的。编译器会在这里重写这个方法,就像为yield重写迭代器那样,于是我们就不需要做其他事情了,任务结束后自然会执行await后面的代码。

这里的美妙之处在于可以任意组合,对于LoadMovies方法来说,我们也可以将其转化为async方法,并await之前的QueryMoviesAsync方法返回。

async void LoadMoviesAsync(int year)
{
    resultsPanel.Children.Clear();
    statusText.Text = "";
    var pageSize = 10;
    var imageCount = 0;

    while (true)
    {
        var movies = await QueryMoviesAsync(year, imageCount, pageSize);
        if (movies.Length == 0) break;
        DisplayMovies(movies);
        imageCount += movies.Length;
    }

    statusText.Text = String.Format("{0} Titles", imageCount);
}

于是异步实现就这么完成了,代码和之前几乎完全一致。您可以看出,这使得我们在执行异步代码时保留原本的逻辑实现。

那么再为应用程序添加一点功能吧。首先是异常处理:

async void LoadMoviesAsync(int year)
{
    resultsPanel.Children.Clear();
    statusText.Text = "";
    var pageSize = 10;
    var imageCount = 0;

    try
    {
        while (true)
        {
            var movies = await QueryMoviesAsync(year, imageCount, pageSize);
            if (movies.Length == 0) break;
            DisplayMovies(movies);
            imageCount += movies.Length;
        }

        statusText.Text = String.Format("{0} Titles", imageCount);
    }
    catch (XmlException)
    {
        statusText.Text = "Data Error";
    }
}

我们无需分离代码或是逻辑,这一切都和同步代码完全一致。再来看看“取消(cancellation)”,对于async方法来说,我们可以传递一个CancellationToken,表示任务需要监听这个对象的改变。如QueryMoviesAsync便可以增加一个参数:

async Task<Movie[]> QueryMoviesAsync(int year, int first, int count, CancellationToken ct)
{
    var client = new WebClient();
    var url = String.Format(query, year, first, count);
    var data = await client.DownloadStringTaskAsync(new Uri(url), ct);

    var movies =
        from entry in XDocument.Parse(data).Descendants(xs + "entry")
        let properties = entry.Element(xm + "properties")
        select new Movie
        {
            /* ... */
        };

    return movies.ToArray();
}

这样便得到了一个可取消的async方法。对于逻辑流来说,取消操作就相当于一个异常,代码里需要处理一个TaskCanceledException:

CancellationTokenSource cts;

async void LoadMoviesAsync(int year)
{
    resultsPanel.Children.Clear();
    statusText.Text = "";
    var pageSize = 10;
    var imageCount = 0;

    cts = new CancellationTokenSource();
    try
    {
        while (true)
        {
            var movies = await QueryMoviesAsync(year, imageCount, pageSize, cts.Token);
            if (movies.Length == 0) break;
            DisplayMovies(movies);
            imageCount += movies.Length;
        }
        statusText.Text = String.Format("{0} Titles", imageCount);
    }
    catch (TaskCanceledException) { }

    cts = null;
}

private void cancelButton_Click(object sender, RoutedEventArgs e)
{
    if (cts != null)
    {
        cts.Cancel();
        statusText.Text = "Canceled";
    }
}

那么超时又怎么说?超时其实就类似一段时间之后的取消。于是我们可以另写一个小方法来处理这个问题:

async void StartTimeoutAsync()
{
    await TaskEx.Delay(5000);
    if (cts != null)
    {
        cts.Cancel();
        statusText.Text = "Timeout";
    }
}

private void searchButton_Click(object sender, RoutedEventArgs e)
{
    LoadMoviesAsync(Int32.Parse(textBox.Text));
    StartTimeoutAsync();
}

第一步,我们先等待5秒钟,如果任务还在执行,那么我们就取消掉。所以无论是超时,取消还是错误处理,程序的逻辑结构都得以最大限度的保留,就好比编写普通的代码一样。例如上面的Delay,看上去是顺序逻辑流,但实际上是异步的。为了表现出这点,我们可以为程序新加上一个有趣的功能:

async void ShowDateTimeAsync()
{
    while (true)
    {
        Title = "Movie Finder " + DateTime.Now;
        await TaskEx.Delay(1000);
    }
}

public MainWindow()
{
    InitializeComponent();
    textBox.Focus();
    ShowDateTimeAsync();
}

于是在标题栏上便会每隔一秒刷新显示当前时间,与此同时搜索也好,超时也罢,在程序执行时UI都可以获得响应。

值得强调的是,上面实现的这些功能都没有启用额外的线程,所有这些都在UI线程上执行。那么什么时候需要额外的线程呢?这便是计算密集型操作。例如这里我要执行五千万次平方根计算,这需要耗费一段时间。不过这样的操作,对于UI线程来说,这也不过是一个异步操作,不是吗?启动操作,然后等待其完成,在它完成之后再对结果做些处理:

async void ComputeStuffAsync()
{
    double result = 0;
    await TaskEx.Run(() =>
    {
        for (int i = 1; i < 500000000; i++)
        {
            result += Math.Sqrt(i);
        }
    });

    MessageBox.Show("The result is " + result, "Background Task",
        MessageBoxButton.OK, MessageBoxImage.Information);
}

private void searchButton_Click(object sender, RoutedEventArgs e)
{
    LoadMoviesAsync(Int32.Parse(textBox.Text));
    StartTimeoutAsync();
    ComputeStuffAsync();
}

TaskEx.Run方法会构造一个后台线程,并返回异步操作,我们使用await等待其返回,这体现了绝佳的组合能力。启动后在任务管理器中便会发现CPU占用率明显上升。

我在这里宣布,之前演示的技术预览版已经可以下载了。我们已经创建了C#和VB编译器的原型,并提供了一些示例。您可以在开发者中心下载,我在演讲最后会给出URL。

相关文章

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

29 条回复

  1. Dozer
    58.218.11.*
    链接

    Dozer 2010-10-31 03:21:25

    我还以为已经可以这么用了呢…… 打开VS后发现还不行 =。=

  2. 老赵
    admin
    链接

    老赵 2010-10-31 03:44:06

    @Dozer

    大哥你还真是不看文章内容啊……

  3. 链接

    小城故事 2010-10-31 17:02:07

    很不错的创意!只是可惜,.net又要臃肿一些了。

  4. 木野狐
    58.38.58.*
    链接

    木野狐 2010-10-31 19:05:23

    @小城故事

    明显不能叫臃肿嘛,语言功能越高级自然要增加相应的实现了。语言添加2个关键字,比通过类库实现语法要简洁和清晰太多了。

  5. 老赵
    admin
    链接

    老赵 2010-10-31 19:13:45

    @小城故事

    明显没有臃肿,就像C# 2.0的匿名方法,完全就是编译器做的事情,和框架或运行时没有任何影响。现在的功能也是基于.NET 4.0中已有的Task<T>,最多加点封装用的代码而已。

  6. 木野狐
    58.38.58.*
    链接

    木野狐 2010-10-31 19:14:25

    @mcpssx

    I/O Bound 的程序多用异步要明显优于多线程。线程是非常昂贵的资源,基于 IOCP 的异步 I/O 操作在发起后,线程就能立刻得到释放。这样可以用很少的工作线程服务上千个客户端请求,恰恰是对编写服务端程序非常有用的特性。你正好说反了。

    建议你完整观看 Anders 的视频后再发表诸如“搞不清出微软编译后到底是幕后起了个线程”这样的评论。

  7. 老赵
    admin
    链接

    老赵 2010-10-31 22:57:22

    @木野狐: 建议你完整观看 Anders 的视频后再发表诸如“搞不清出微软编译后到底是幕后起了个线程”这样的评论。

    C#和F#编译器无论怎么写,都是很容易就能搞清楚幕后是什么样的,有太多说明,没有说明自己看也行,根本没有困难和障碍,所以没有“搞不搞得清楚”,只有“愿不愿意搞清楚”。

  8. 链接

    小城故事 2010-10-31 23:39:27

    .Net类库肯定还要扩充的了,文章里也说,“DownloadStringTaskAsync,以后也会包含在框架中”

  9. 老赵
    admin
    链接

    老赵 2010-11-01 00:30:59

    @小城故事

    我又没说不会扩充,我说得是“最多加点封装用的代码”。

  10. Bo
    206.220.172.*
    链接

    Bo 2010-11-01 09:02:35

    他们会如何解决以下问题呢: await期间如果我改变了UI里的一些输入,或者直接把窗口给关闭了,会取消执行?如果代码是以MVVM的方式放在其它地方,它又会怎样呢?这些问题估计比实现演示的那些功能要更难解决。

  11. zhuisha
    221.216.161.*
    链接

    zhuisha 2010-11-01 09:52:55

    首先,老赵的文章还是依然像暴雪一样必是精品。其次,老赵发文章的频率能不能不像暴雪那样低.(忠实的观众)

    说点技术的,这async和aware终于把异步整合到语言层面了(而不是类库层面). 于是异步可以被我们轻易的玩了(以前都是我们被异步玩),选择C#不为别的,只为我想玩计算机,而不是被计算机玩.

  12. 老赵
    admin
    链接

    老赵 2010-11-01 10:25:07

    @zhuisha

    当然,C#和F#解决异步方面问题的方式是不同的,而且我其实更倾向于F#的方式……还得再思考一下。

  13. doylecnn
    222.68.249.*
    链接

    doylecnn 2010-11-01 16:03:10

    @Bo

    您提到的问题,在没有这两个新关键字的时候好解决么?

    以及,新出来的两个处理异步的关键字,为什么要替你解决那两个问题呢?

  14. Ricepig
    221.223.87.*
    链接

    Ricepig 2010-11-02 07:55:48

    一直看老赵的文章,第一次发帖

    文章写的很好,但是有一个观点不是很同意:

    没错,不过就引入了多线程模型,于是就要处理同步等线程安全问题。而且,在开发带有UI的应用程序时,我们不能在后台线程里操作UI,这样又出现了其他的复杂情况。

    异步的单线程实现,类似于javascript那种,确实没有线程同步操作了,但是限制很多,一般都是多线程模型吧(我猜新版的C#也是?)。另外,即使是现在这种异步操作,对于某些情况也是不能不考虑线程安全的,尤其是异步方法“动”了类的成员变量的时候。新版C#解决的可能更多的是对于返回值的语法糖:对返回值建立依赖图,自动的回调所有依赖这个值的操作。 当然,可以完全用某种数据流的方式来让这些异步操作形成一个异步操作流,而不需要“动”成员函数,这就类似某些函数式语言的编程方法。

    希望老赵以后写写怎么更神奇的使用这种异步方式。

  15. 老赵
    admin
    链接

    老赵 2010-11-02 10:02:17

    @Ricepig

    文章里没有说“任何地方都不需要考虑线程安全”,只是因为它是UI,在UI线程上执行,所以已经保证是顺序的了,仅此而已。其实这个特性也没什么神奇的,有什么便利和限制,我觉得应该还是十分一目了然的,呵呵。

  16. Snake
    117.25.18.*
    链接

    Snake 2010-11-02 12:00:08

    先看了一下篇幅结果看到了熟悉的monaco字体着实激动了一下.

  17. 链接

    2010-11-03 11:29:46

    留下脚印^^^^

  18. luotong
    61.54.57.*
    链接

    luotong 2010-11-03 13:15:24

    本文讲的新特性正是我梦寐以求的功能,这段时间正纠结在异步中。 等待下载新编译器...

  19. edie
    120.199.4.*
    链接

    edie 2010-11-29 17:52:40

    经常看赵博, 看这个异步,不知道内部是如何处理的,应该自动用到了线程池。 他应该我想是演示中所要的效果吧。

    static Action act = () =>
        {
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine("a");//do other action
                System.Threading.Thread.Sleep(1000);
            }
        };
    

    然后调用代码:

    act.BeginInvoke(new AsyncCallback((IAsyncResult ar) =>
                        {
                            act.EndInvoke(ar);
                            Console.WriteLine("finish");//do finish action
                        }), null);
    
  20. edie
    120.199.4.*
    链接

    edie 2010-11-29 17:56:38

    c#的线程池很纠结。

  21. mog
    222.70.192.*
    链接

    mog 2010-12-20 15:27:04

    如果想试用async,await,请去http://msdn.com/vstudio/async下载,目前只支持英文版vs2010。

  22. 链接

    airwolf2026 2010-12-28 11:59:16

    这样便得到了一个可取消的async方法。对于逻辑流来说,取消操作就相当于一个异常

    为啥这样的取消,都用异常来处理? 比如thread 的取消的时候,也是用异常,一直没有搞明白

  23. yghua8
    113.240.179.*
    链接

    yghua8 2012-06-04 14:21:10

    async,await 此新特性.net 2.0下能用就好了。

  24. jack
    183.60.101.*
    链接

    jack 2012-09-04 15:37:04

    真还没搞清楚多线程与异步的区别,作者明明说:"实现的这些功能都没有启用额外的线程",但后面又说:"TaskEx.Run方法会构造一个后台线程",在程序中有使用TaskEx.Run方法呀,怎么说没有启用额外线程呢?

  25. objectboy
    180.184.22.*
    链接

    objectboy 2012-12-10 21:38:49

    赵神, 我觉得c# 应该朝着底层点的方向发展,就说压缩图片失真,大文件转码内存占用高这些方面我个人感觉不地道, 当然我这个菜鸟可能没有什么发语权, 但是我就是这么认为的,只是恼骚而已...

  26. 老赵
    admin
    链接

    老赵 2012-12-10 22:52:28

    @objectboy

    底层是什么意思?底层交给C和C++不是很好么。压缩图片失真这种关语言什么事情,大文件转码还是用现成的Native类库比较合适,或者用Unsafe代码,就直接操作内存了,不需要内存反复复制。

  27. Eysa
    221.11.66.*
    链接

    Eysa 2013-06-17 21:57:14

    如果有demo下载就好了,DisplayMovies(movies);这个方法在哪啊?信任看文章必须得健全啊。拜读你的文章,希望不要出现文章代码里没有的东东。

已自动隐藏某些不合适的评论内容(主题无关,争吵谩骂,装疯卖傻等等),如需阅读,请准备好眼药水并点此登陆后查看(如登陆后仍无法浏览请留言告知)。

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我