C# 多线程学习一

1:什么是多线程:

1:进程:电脑有很多的独立允许的程序,每个程序就是一个进程,进程之间是独立的,例如QQ、微信等

2:线程:进程要想执行任务就需要线程,线程是进行最小的执行单位,一个进程至少有一个线程

3:多线程:一个进程中有多个线程去同时执行

即:一个程序运行后至少有一个进程,一个进程由一个或多个线程去完成

2:为什么要使用进程

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

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

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

3:如何使用线程

大部分情况下,多线程的应用场景是在后台执行一个较长时间的任务时,不能阻塞界面响应,同时,任务还是可以取消的

例子,用户点击start按钮启动一个任务,任务执行过程中通过进度条显示任务进度,点击stop按钮停止任务

 public partial class Form1 : Form
    {
        private volatile bool CancelWork = false;

        public Form1()
        {
            InitializeComponent();
        }

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

            CancelWork = false;
            Task.Run(() => WorkThread());
        }

        private void btnStop_Click(object sender, EventArgs e)
        {
            CancelWork = true;
        }

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

                Thread.Sleep(1000);

                if(CancelWork)
                {
                    break;
                }
            }

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

 3.1 并行编程

目标:通过一个计算素数的方法,循环计算并打印出10000以内的素数。

计算一个数是否素数的方法:

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实际是通过线程池进行任务的分配,线程池的最小线程数和最大线程数将影响到整个程序的性能,需要合理设置。(最小线程默认为8。)

ThreadPool.SetMinThreads(10, 10);
ThreadPool.SetMaxThreads(20, 20);

 按照上述设置,假设线程任务耗时比较长不能很快结束。在启动前面10个线程时速度很快,第10~20个线程就比较慢一点,大约0.5秒,到达20个线程以后,如果前期任务没有结束就不能继续分配任务了。

和Task类似,Parallel类仍然是对ThreadPool的封装,但Parallel有一个优势,它能知道所有任务是否完成,如果采用线程池来实现批量任务,我们需要自己通过计数的方式确定所有子任务是否全部完成。

 Parallel类还有一个ForEach方法,使用和For类似,就不重复描述了。

3.2 线程同步

有时我们需要通知一个任务结束,或一个任务等待某个条件进入下一个状态,这就需要用到任务同步的技术。

 这是我们常用的方法,可以称为线程状态机同步(虽然只有两个状态)。需要注意的是在通过轮询去读取状态时,循环体内至少应该有1ms的Sleep,不然CPU会很高。

线程同步还有一个比较好的办法就是采用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 。

3.3 异步编程模型(await async)

假设我们要实现一个简单的功能:当点击启动按钮时,运行一个任务,任务结束时要报告是否成功,如果成功就显示绿色图标、如果失败就显示红色图标,1秒后图标颜色恢复为白色;任务运行期间启动按钮要不可用。

 public partial class Form1 : Form
    {
        private void btnStart_Click(object sender, EventArgs e)
        {
            this.btnStart.Enabled = false;

            if(DoSomething())
            {
                this.picShow.BackColor = Color.Green;
            }
            else
            {
                this.picShow.BackColor = Color.Red;
            }

            Thread.Sleep(1000);
           
            this.picShow.BackColor = Color.White;
            this.btnStart.Enabled = true;
        }

        private bool DoSomething()
        {
            Thread.Sleep(5000);
            return true;
        }
    }

 这段代码逻辑清晰、条理清楚,一看就能明白,但存在两个问题:

1、运行期间UI线程阻塞了,用户界面没有响应;

2、根本不能实现需求,点击启动后,程序卡死6秒种,也没有看到颜色变化,因为UI线程已经阻塞,当重新获得句柄时图标已经是白色了。

为了实现需求,我们改用多任务来实现相关功能:

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

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

            Task.Run(() => 
            {  
                if (DoSomething())
                {
                    this.Invoke(new Action(() =>
                    {
                        this.picShow.BackColor = Color.Green;
                    }));                   
                }
                else
                {
                    this.Invoke(new Action(() =>
                    {
                        this.picShow.BackColor = Color.Red;
                    }));                   
                }

                Thread.Sleep(1000);

                this.Invoke(new Action(() =>
                {
                    this.btnStart.Enabled = true;
                    this.picShow.BackColor = Color.White;
                }));               
            });           
        }

        private bool DoSomething()
        {
            Thread.Sleep(5000);
            return true;
        }
    }

 以上代码完全实现了最初的需求,但有几个不完美的地方:

1、主线程的btnStart_Click方法除了启动一个任务以外,啥事也没干;

2、由于非UI线程不能访问UI控件,代码里有很多Invoke,比较丑陋;

3、界面逻辑和业务逻辑掺和在一起,使得代码难以理解。

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;
        }
    }

这段代码看起来就像是同步代码,其业务逻辑是如此的清晰优雅,让人一目了然,关键是它还不阻塞线程,UI正常响应。

可以看到,通过使用await关键字,我们可以专注于业务功能实现,特别是后续任务需要前序任务的返回值的情况下,可以大量减少任务之间的同步操作,代码的可读性也大大增强。

猜你喜欢

转载自blog.csdn.net/hyyjiushiliangxing/article/details/121125306