4

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

 3 years ago
source link: https://blogread.cn/it/article/3255?f=hot1
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

浏览:1626次  出处信息

    PDC不愧为微软最高级的技术人员专业会议,看得我直呼过瘾。前几天在PDC 2010会议上Anders Hejlsberg发表了一场名为“The Future of C# and Visual Basic”的演说,谈论了未来C#和VB中最为重要的两个特性:“异步(Async)”及“编译器即服务?ompiler 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 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 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,你如果了解.NET 4则一定已经知道这个类型是任务并行库的一部分。事实上Task类型只是表示一个“开始计算并在未来返回结果”的任务,因此Task表示一个会在将来返回T类型的计算任务,在科学计算领域这通常被称为Future或是Promise。现在方法的返回值是Task,而最后返回的是Movie[],这显然不匹配,但我们可以将其标记为一个async方法。对于async方法,编译器会重写整个方法实现来表示一个异步任务,以后我们会来观察它是如何实现这点的。[]>[]>

async Task 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对象。于是在async方法中,我们使用一个新的await操作符来等待其返回。

async Task 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 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。

建议继续学习:

QQ技术交流群:445447336,欢迎加入!
扫一扫订阅我的微信号:IT技术博客大学习

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK