码迷,mamicode.com
首页 > 编程语言 > 详细

C#多线程开发:并行、并发与异步编程

时间:2020-06-19 00:41:28      阅读:170      评论:0      收藏:0      [点我收藏+]

标签:highlight   显示   nta   ring   invoke   eset   编程模型   基于   received   

概述

现代程序开发过程中不可避免会使用到多线程相关的技术,之所以要使用多线程,主要原因或目的大致有以下几个:

1、 业务特性决定程序就是多任务的,比如,一边采集数据、一边分析数据、同时还要实时显示数据;

2、 在执行一个较长时间的任务时,不能阻塞UI界面响应,必须通过后台线程处理;

3、 在执行批量计算密集型任务时,采用多线程技术可以提高运行效率。

传统使用的多线程技术有:

  1. Thread & ThreadPool
  2. Timer
  3. BackgroundWorker

目前,这些技术都不再推荐使用了,目前推荐采用基于任务的异步编程模型,包括并行编程和Task的使用。

一、并发编程:

  使用ThreadPool轮询并发

方法是使用一个List(或其他容器)把所有的对象放进去,创建一个线程(为了防止UI假死,由于这个线程创建后会一直执行切运算密集,所以使用TheadPool和Thread差别不大),在这个线程中使用foreach(或for)循环依次对每个对象执行ReceiveData方法,每次执行的时候创建一个线程池线程来执行。代码如下:技术图片

使用Task轮询并发

方法与ThreadPool类似,只是每次创建线程池线程执行ReceiveData方法时是通过Task创建的线程。代码如下所示:

技术图片

 

 

二、并行编程:

private static bool IsPrimeNumber(int number)
        {
            if (number < 1)
            {
                return false;
            }

            if (number == 1 && number == 2)
            {
                return true;
            }

            for (int i = 2; i < number; i++)
            {
                if (number % i == 0)
                {
                    return false;
                }
            }

            return true;
        }

 如果不采用并行编程,常规实现方法:

  for (int i = 1; i <= 10000; i++)
            {
                bool b = IsPrimeNumber(i);             
                Console.WriteLine($"{i}:{b}");
            }

采用并行编程方法

 Parallel.For(1, 10000, x=> 
           {
                bool b = IsPrimeNumber(x);              
                Console.WriteLine($"{i}:{b}");
            })

Parallel类还有一个ForEach方法,使用和For类似。

三、 线程(或任务)同步

线程同步还有一个比较好的办法就是采用ManualResetEvent 和AutoResetEvent :

public partial class Form1 : Form
    {  
        private ManualResetEvent manualResetEvent = new ManualResetEvent(false);

        public Form1()
        {
            InitializeComponent();
        }

        private void btnStart_Click(object sender, EventArgs e)
        {
            this.btnStart.Enabled = false;
            this.btnStop.Enabled = true;

            manualResetEvent.Reset();
            Task.Run(() => WorkThread());
        }

        private void btnStop_Click(object sender, EventArgs e)
        {
            manualResetEvent.Set();
        }

        private void WorkThread()
        {
            for (int i = 0; i < 100; i++)
            {
                this.Invoke(new Action(() =>
                {
                    this.progressBar.Value = i;
                }));

               if(manualResetEvent.WaitOne(1000))
                {
                    break;
                }
            }

            this.Invoke(new Action(() =>
            {
                this.btnStart.Enabled = true;
                this.btnStop.Enabled = false;
            }));            
        }
    }

 

采用WaitOne来等待比通过Sleep进行延时要更好,因为当执行manualResetEvent.WaitOne(1000)时,如果manualResetEvent没有调用Set,该方法在等待1000ms后返回false,如果期间调用了manualResetEvent的Set方法,该方法会立即返回true,不用等待剩下的时间。

采用这种同步方式优于采用通过内部字段变量进行同步的方式,另外尽量采用ManualResetEvent 而不是AutoResetEvent 

 

四、异步编程模型(await、async)

public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private async void btnStart_ClickAsync(object sender, EventArgs e)
        {
            this.btnStart.Enabled = false;

            var result = await DoSomethingAsync();
            if(result)
            {
                this.picShow.BackColor = Color.Green;
            }
            else
            {
                this.picShow.BackColor = Color.Red;
            }

            await Task.Delay(1000);
            
            this.picShow.BackColor = Color.White;
            this.btnStart.Enabled = true;
        }

        private async Task<bool> DoSomethingAsync()
        {
            await Task.Run(() =>
            {
                Thread.Sleep(5000);                
            });
            return true;
        }
    }

 

五、 多线程环境下的数据安全

  目标:我们要向一个字典加入一些数据项,为了增加效率,我们使用了多个线程

  

private async static void Test1()
        {
            Task.Run(() => AddData());
            Task.Run(() => AddData());
            Task.Run(() => AddData());
            Task.Run(() => AddData()); 
        }

        private static void AddData()
        {
            for (int i = 0; i < 100; i++)
            {
                if(!Dic.ContainsKey(i))
                {
                    Dic.Add(i, i.ToString());
                }

                Thread.Sleep(50);
            }
        }

 

向字典重复加入同样的关键字会引发异常,所以在增加数据前我们检查一下是否已经包含该关键字。以上代码看似没有问题,但有时还是会引发异常:“已添加了具有相同键的项。”原因在于我们在检查是否包含该Key时是不包含的,但在新增时其他线程加入了同样的KEY,当前线程再增加就报错了。

【注意:也许你多次运行上述程序都能顺利执行,不报异常,但还是要清楚认识到上述代码是有问题的!毕竟,程序在大部分情况下都运行正常,偶尔报一次故障才是最头疼的事情。】

上述问题传统的解决方案就是增加锁机制。对于核心的修改代码通过锁来确保不会重入。

private object locker4Add=new object();
        private static void AddData()
        {
            for (int i = 0; i < 100; i++)
            {
                lock (locker4Add)
                {
                    if (!Dic.ContainsKey(i))
                    {
                        Dic.Add(i, i.ToString());
                    }
                }

                Thread.Sleep(50);
            }
        }

更好的方案是使用线程安全的容器:ConcurrentDictionary

private static ConcurrentDictionary<int, string> Dic = new ConcurrentDictionary<int, string>();
      
        private async static void Test1()
        {
            Task.Run(() => AddData());
            Task.Run(() => AddData());
            Task.Run(() => AddData());
            Task.Run(() => AddData());   
        }

        private static void AddData()
        {
            for (int i = 0; i < 100; i++)
            {
                Dic.TryAdd(i, i.ToString());
                Thread.Sleep(50);
            }
        }

  

 

C#多线程开发:并行、并发与异步编程

标签:highlight   显示   nta   ring   invoke   eset   编程模型   基于   received   

原文地址:https://www.cnblogs.com/zhouyuqiu/p/13160710.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!