c# 并发编程系列之三:使用 Parallel 类开始多线程编码之旅

用 C# 进行多线程编程有很多方式,比如使用 Thread 对象开启一个新线程,但这已经是一种落后的写法了,

现在推荐的写法是使用 Parallel 类,它可以让我们像写传统代码一样编写多线程的程序,Parallel 类有三个常用

的方法如下:

Parallel.For():开启多线程循环执行一段代码

Parallel.ForEach():开启多线程遍历处理一个对象集合

Parallel.Invoke():开启多线程执行多个方法

下面我们来看看如何使用它们。

 

一、Parallel.For()的使用

新建一个.net core的 Console 项目,在项目根目录下新增类 ConcurrencyDemo.cs 用来演示,如下:

在 ConcurrencyDemo.cs 中新增方法 ParallelForPrint(),其作用是调用 Parallel.For() 循环输出1~9,如下:

public void ParallelForPrint()
{
   Parallel.For(1, 10, i => {
      Console.WriteLine(string.Format("  i = {0}", i));
   });
}

此方法有3个参数,第1个是循环的起始值,第2个是结束值,第3个是要执行的动作,这里用 Lambda 表达式输出 i.

在 Program.cs 的 Main() 方法中调用,代码如下:

static void Main(string[] args)
{
    Console.WriteLine("Hello World!");

    ParallelDemo pdemo = new ParallelDemo();
    pdemo.ParallelForPrint();

    Console.ReadLine();
}

编译后运行程序,得到如下结果:

可以看到,并没有按顺序输出1~9,因为多线程的其中一个特点就是乱序执行,代码的执行顺序是不可控的。

下面我们将方法稍作修改,并与 for 循环做一下对比,看使用 Parallel.For() 做多线程编程比用传统的 for 能快多少。

     public void ParallelForCompareFor()
        {
            //计算 Parallel.For() 的时间
            int total1 = 0;
            Stopwatch watch1 = new Stopwatch();
            watch1.Start(); //开始计时
            Parallel.For(1, 20000, i => { //循环 19999 次
                total1 += i;
            });
            watch1.Stop(); //结束计时
            Console.WriteLine(string.Format("  Parallel.For 循环花了 {0} 毫秒。",watch1.ElapsedMilliseconds));

//计算 for() 的时间 int total2 = 0; Stopwatch watch2 = new Stopwatch(); watch2.Start(); //开始计时 for(int j=1;j<20000;j++) //循环 19999 次 { total2 += j; } watch2.Stop(); //结束计时 Console.WriteLine(string.Format(" for 循环花了 {0} 毫秒。", watch2.ElapsedMilliseconds)); }

在 Program.cs 的 Main() 方法中调用,代码如下:

       static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");

            ParallelDemo pdemo = new ParallelDemo();
            pdemo.ParallelForCompareFor();

            Console.ReadLine();
        } 

编译后运行程序,得到如下结果:

和我们期望的不一样,使用 Parallel.For() 花费了更多的时间。

如果我们将循环的次数由 20000 改为 20 时候,结果如下:

使用 Parallel.For() 花费的时间依然比 for 多。

这说明 Parallel.For() 的使用是有条件的,如果循环内的代码运行的时间很短,它反而更慢,这是因为使用

多线程的时候,线程的创建、撤销等是有时间开销的,这一点在本系列中的第2篇中有过说明。

 

下面我们再对方法做一下修改,在循环体内让当前线程休眠 10 毫秒,代码如下,看看会发生什么:

       public void ParallelForCompareFor()
        {
            //计算 Parallel.For() 的时间
            int total1 = 0;
            Stopwatch watch1 = new Stopwatch();
            watch1.Start(); //开始计时
            Parallel.For(1, 20000, i => {
                total1 += i;
                Thread.Sleep(10);
            });
            watch1.Stop(); //结束计时
            Console.WriteLine(string.Format("  Parallel.For 循环花了 {0} 毫秒。",watch1.ElapsedMilliseconds));

            //计算 for() 的时间
            int total2 = 0;
            Stopwatch watch2 = new Stopwatch();
            watch2.Start(); //开始计时
            for(int j=1;j< 20000; j++)
            {
                total2 += j;
                Thread.Sleep(10);
            }
            watch2.Stop(); //结束计时
            Console.WriteLine(string.Format("  for 循环花了 {0} 毫秒。", watch2.ElapsedMilliseconds));
        }

编译后运行结果如下:

此时 Parallel.For() 的优势便体现出来了。继续将循环次数由 20000 改成 200,结果如下:

使用 Parallel.For() 会快4倍左右。如果再进一步调整休眠时间为 1 毫秒,运行结果如下:

从上面的演示可以看到,Parallel.For() 开启多线程比 for 快的前提是循环体中的代码执行要有一定的

时间开销,否则是达不到更快的效果的。

 

二、Parallel.ForEach() 的使用

从 ForEach() 这个名字可以看出该方法是用来遍历泛型集合的,新建一个 ASP.NET Core 的项目,如下:

在 Index.cshtml.cs 文件中增加一个 UserInfo.cs 的类,代码如下:

    public class UserInfo
    { 
        public int UserId { get; set; }
        public string UserName { get; set; }
    }

在 Index.cshtml.cs 文件的 IndexModel 类中添加测试方法 ParallelForEachDemo(),代码如下:

        public string DemoStr; //定义页面取值的变量
        public void ParallelForEachDemo()
        {
            //构造泛型集合数据
            List<UserInfo> userList = new List<UserInfo>
            {
                new UserInfo{ UserId=1,UserName="张三" },
                new UserInfo{ UserId=2,UserName="李四" },
                new UserInfo{ UserId=3,UserName="王五" },
                new UserInfo{ UserId=4,UserName="赵六" },
                new UserInfo{ UserId=5,UserName="大师兄" }
            };

            //foreach 循环时间统计
            string allName1 = string.Empty;
            Stopwatch sw1 = new Stopwatch();
            sw1.Start(); //计时开始
            foreach (UserInfo user in userList)
            {
                allName1 += user.UserName + ",";
                Thread.Sleep(10);//模拟一个耗时操作,以免看不到效果
            }
            sw1.Stop(); //计时结束


            //Parallel.ForEach 循环时间统计
            string allName2 = string.Empty; 
            Stopwatch sw2 = new Stopwatch();
            sw2.Start(); //计时开始
            Parallel.ForEach(userList, user => //多线程遍历
            {
                allName2 += user.UserName + ",";
                Thread.Sleep(10);//模拟一个耗时操作,以免看不到效果
            });
            sw2.Stop(); //计时结束

            //记录花费时间
            DemoStr = string.Format("foreach 循环花费时间为:{0},Parallel.ForEach 循环花费时间为:{1}", 
                sw1.ElapsedMilliseconds, sw2.ElapsedMilliseconds);
        }

在 Index.cshtml.cs 中输出 DemoStr 变量的值,代码如下:

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <br />
    <p> @Model.DemoStr </p>
</div>

编译后运行,结果如下:

显然使用多线程更快,单线程和多线程的时间差距大概是5倍左右(不同CPU时间不同,这里仅代表本次执行结果)。

 

三、Parallel.Invoke() 的使用

在 Index.cshtml.cs 文件的 IndexModel 类中新增方法,分别访问 bing.com,360.cn,baidu.com 这3个网站,

然后统计响应的字符数,代码如下:

        /// <summary>
        /// Thread.CurrentThread.ManagedThreadId 用于显示当前的线程ID
        /// </summary>
        /// <param name="from"></param>
        /// <param name="url"></param>
        public void CountString(string from, string url)
        {
            long cnt = 0;

            HttpWebRequest request = WebRequest.CreateHttp(url); //根据给定的网址创建一个请求
            HttpWebResponse response = (HttpWebResponse)request.GetResponse(); //得到响应对象
            cnt = response.ContentLength; //获取响应内容长度

            //用table标签格式化输出便于查看
            ContentStr += "<tr><td>" + from + "</td><td>" + Thread.CurrentThread.ManagedThreadId + "</td>" +
                          "<td>" + url + "</td><td>" + cnt + "</td></tr>";
            return;
        }

单线程顺序执行3次函数:

        /// <summary>
        /// 单线程执行3次函数取三个不同网址的内容
        /// </summary>
        /// <returns></returns>
        public string SingleTotal()
        {
            Stopwatch sw1 = new Stopwatch();
            sw1.Start();
            CountString("single", "http://www.bing.com");
            CountString("single", "http://www.360.cn");
            CountString("single", "http://www.baidu.com");
            sw1.Stop();

            return sw1.ElapsedMilliseconds.ToString();
        }

多线程并发执行3次函数:

     public string MultiTotal()
        {
            Stopwatch sw2 = new Stopwatch();
            sw2.Start(); 
            // 使用Lambda表达式构造 Action,这里可以传多个方法来并行执行,不限于3个
            Parallel.Invoke(
          ()=>CountString("Multi", "http://www.bing.com"), ()=>CountString("Multi", "http://www.360.cn"), ()=>CountString("Multi", "http://www.baidu.com")); sw2.Stop(); return sw2.ElapsedMilliseconds.ToString(); }

在 OnGet() 函数中分别调用单线程方法和多线程方法: 

     public string DemoStr; //定义页面取值的变量来显示执行时间
        public string ContentStr; //定义页面取值变量来显示函数执行情况
        public void OnGet()
        {
            string time1 = SingleTotal(); //单线程花费时间
            string time2 = MultiTotal(); //多线程花费时间

            DemoStr = string.Format("单线程花费时间为:{0},多线程花费时间为:{1}", time1, time2);

            //用表格来展示数据更清晰
            ContentStr = "<table border='1' width='600' style='margin:0 auto;'>" +
                        "<tr><td>类型</td><td>线程ID</td><td>网址</td><td>响应内容长度</td></tr>" + 
                        ContentStr + "</table>"; 
        }

在 Index.cshtml 中修改代码如下:

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <br />
    <p> @Model.DemoStr </p>
    <br />
    <p> @Html.Raw(Model.ContentStr) </p>
</div>

编译后运行结果如下:

显然,使用多线程时花费的时间更少。

从表格的执行明细中也可以看到:使用多线程的时候3个方法分别是3个不同线程来执行的。

 

posted @ 2021-06-09 01:04  屏风马  阅读(748)  评论(0编辑  收藏  举报