并发与异步

Posted xianchengzhang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发与异步相关的知识,希望对你有一定的参考价值。

1、概述

最常见的并发场景包括

  编写快速响应的用户界面在WPF、移动应用和Windows Forms应用程序中,都需要并发执行耗时任务以保证用户界面的响应性。

可以处理同时出现的请求

  在服务器上,客户端的请求可能会并发到达,必须通过并行处理才能够保证程序的可伸缩性。如果使用ASP.NET、WCF或者WebServices,则.NET Framework会自动执行并行处理。然而,程序员仍然需要关注某些共享的状态(例如使用静态变量作为缓存)。

并行编程

  如果可以将负载划分到多个核心上,那么多核、多处理器计算机就可以提升密集计算代码的执行速度

预测执行

  在多核主机上,有时可通过预测的方式提前执行某些任务来改善程序性能。例如,LINQPad使用这种方式来提高查询的创建速度。而如果事先无法知道哪种方法是最优的,则可以并行执行多个解决同一任务的不同算法,最先完成的算法就是最优的。

这种程序同时执行代码的机制称为多线程(multithreading)。CLR和操作系统都支持多线程,它是并发的基础概念。因此,要介绍并发编程,首先就要具备线程的基础知识,特别是线程的共享状态。

 

2、线程

  线程是一个可以独立执行的执行路径。每一个线程都运行在一个操作系统进程中。这个进程提供了程序执行的独立环境。在单线程(single threaded)程序中,进程中只有一个线程运行,因此线程可以独立使用进程环境。而在多线程程序中,一个进程中会运行多个线程。它们共享同一个执行环境(特别是内存)。这在一定程度上说明了多线程的作用。例如,可以使用一个线程在后台获得数据,同时使用另一个线程显示所获得的数据。而这些数据就是所谓的共享状态(shared state)。

 

技术图片
1             //创建并启动一个线程 需要首先实例化Thread 对象并调用Start方法
2             System.Threading.Thread th = new System.Threading.Thread(Print_y);
3             //启动线程
4             th.Start();
创建线程

 

技术图片
1              //线程的执行状态true || false
2             Console.WriteLine(th.IsAlive);
当前线程执行状态 (IsAlive)属性

 

技术图片
1 //获取当前运行的线程的名称
2 Console.WriteLine(System.Threading.Thread.CurrentThread.Name);
获取当前正在运行的线程名称(CurrentThread)属性
技术图片
1             //创建并启动一个线程 需要首先实例化Thread 对象并调用Start方法
2             System.Threading.Thread th = new System.Threading.Thread(Print_y);
3             //启动线程
4             th.Start();
5             //等待线程结束,可以指定超时时间
6             th.Join(1000);    
7          
8             //暂停2秒钟
9              System.Threading.Thread.Sleep(2000);
休眠
技术图片
1             ////可以使用ThreadState属性测试线程的阻塞状态
2             bool b = (th.ThreadState & System.Threading.ThreadState.WaitSleepJoin)!=0;
阻塞状态

 I/O密集和计算密集

  如果一个操作的绝大部分时间都在等待事件的发生,则称为I/O密集,例如下载网页或者调用Console.ReadLine。(I/O密集操作一般都会涉及输入或者输出,但是这并非硬性要求。例如Thread.Sleep也是一种I/O密集的操作)。而相反的,如果操作的大部分时间都用于执行大量的CPU操作,则称为计算密集。

阻塞与自旋

  I/O密集操作主要表现为以下两种形式:要么在当前线程同步进行等待,直至操作完成(例如Console.ReadLine、Thread.Sleep以及Thread.Join);要么异步进行操作,在操作完成的时候或者之后某个时刻触发回调函数(之后将详细介绍)。同步的I/O密集操作的大部分时间都花费在阻塞线程上,但是也可能在一个定期循环中自旋:

  

while(DataTime.Now<nextStartTime)
{
      Thread.Sleep(1000);      
}

本地状态与共享状态

  CLR为每一个线程分配了独立的内存栈,从而保证了局部变量的隔离。下面的示例定义了一个拥有局部变量的方法,并同时在主线程和新创建的线程中调用该方法:

        static void Main(string[] args)
        {
            //开启一个新的线程
            new System.Threading.Thread(Go).Start();
            //主线程
            Go();

            Console.ReadKey();
        }

        static void Go()
        {
            for (int count = 0; count < 5; count++)
            {
                Console.WriteLine("? ");
            }
        }

由于每一个线程的内存栈上都会有一个独立的cycles变量的副本,因此我们可以预测,程序的输出将是10个问号。

 

如果不同的线程拥有同一个对象的引用,则这些线程之间就共享了数据:

技术图片

 

 

 由于两个线程均在同一个ThreadTest实例上调用了Go()方法,因此它们共享_done字段。因此,“Done”只会打印一次,而非两次。

 

锁与线程安全

   我们可以在读写共享字段时首先获得一个排它锁来修正之前示例的问题。使用C#的lock语句就可以实现这个目标:

 class Program
    {
        static bool _done;
        static readonly object locker = new object();
        static void Main(string[] args)
        {
            new System.Threading.Thread(()=>Go("Down")).Start();
            Go("ss");
            Console.ReadKey();

        }
        public static void Go(string str)
        {
            lock (locker)
            {
                if (!_done)
                {
                    Console.WriteLine(str);
                    _done = true;
                }
            }
        }
    }

 

 向线程传递数据

  有时我们需要给线程的启动方法传递参数。最简单的方案是使用Lambda表达式,并在其中使用指定的参数调用相应的方法。

 技术图片

 

 

 Lambda表达式是在C#3.0之后引入的,你可能会遇到一些代码使用了较老的技术,即向Thread的Start方法传递一个参数:

技术图片

 前台线程与后台线程

  一般情况下,显式创建的线程称为前台线程(Foreground thread)。只要有一个前台线程还在运行,应用程序就仍然保持运行状态。而后台线程(background thread)则不然。当所有前台线程结束时,应用程序就会停止,且所有运行的后台线程也会随之终止。

  线程的前台/后台状态和线程的优先级(执行时间的分配)无关。可以使用线程的IsBackground属性来查询或修改线程的前后台状态:

技术图片

 

 信号发送

  有时一个线程需要等待来自其他线程的通知,即所谓的信号发送(signaling)。最简单的信号发送结构是ManualResetEvent。调用ManualResetEvent的WaitOne方法可以阻塞当前线程,直到其他线程调用了Set“打开”了信号。以下的示例启动了一个线程,并等待ManualResetEvent。它会阻塞两秒钟,直至主线程发送信号为止:

           //是否将初始值设置为终止
            var signal = new ManualResetEvent(false);
            new Thread(() =>
            {

                Console.WriteLine("Waiting for signal....");
                signal.WaitOne();
                signal.Dispose();
                Console.WriteLine("Got signal!");

            }).Start();
            Thread.Sleep(2000);
            signal.Set();

在Set调用后,信号发送结构仍然会保持“打开”状态,可以调用Reset方法再次将其“关闭”。

跨线程操作

//· 在WPF中,调用元素上的Dispatcher对象的BeginInvoke或Invoke方法。
//· 在UWP应用中,可以调用Dispatcher对象的RunAsync或Invoke方法。
//· 在Windows Forms应用中:调用控件的BeginInvoke或Invoke方法。

        private void button3_Click(object sender, EventArgs e)
        {
            Thread th = new Thread(Print);
            th.IsBackground = true;
            th.Start();
        }
        public  void Print()
        {
            if (textBox1.InvokeRequired)//是否对文本框进行跨线程访问
            {
                //invoke:去找创建TextBox的线程
                this.textBox1.Invoke(new Action<TextBox, string>(showTxt), this.textBox1, "123");
            }
            else
            {
                textBox1.Text = "123";
            }
        }
        public void showTxt(TextBox tet, string msg)
        {
            tet.Text = msg;
        }

线程池

  在线程池上运行代码的最简单的方式是调用Task.Run(我们将在下一节深入介绍这个方法):

            //在线程池上运行代码的最简单的方法是 Task.Run
            Task.Run(() => { Console.WriteLine("hello world"); });
            //.net4.0 之前没有Task类 因此可以调用下面的方法
            ThreadPool.QueueUserWorkItem(n => Console.WriteLine(n));

 

以上是关于并发与异步的主要内容,如果未能解决你的问题,请参考以下文章

golang goroutine例子[golang并发代码片段]

QT 信号槽 异步事件驱动 单线程 多并发

并发编程

Java并发编程核心概念一览

python网络编程基础(线程与进程并行与并发同步与异步)

并发编程 - 总结