• C#多线程(一) 入门


    本文你会了解如下内容:

    1、计算机程序、进程、线程的概念

    2、多线程的概念、为什么需要多线程、多线程的好处与坏处

    3、C# 线程的一些概念与操作(创建线程、像线程中传递参数、给线程取名、前后台线程、线程优先级、异常处理)

    4、线程池


    一、计算机程序、进程、线程的概念

    计算机程序:是计算机能识别和处理的指令集合,通常情况下是计算机操作系统可以执行的二进制文件,如在Win32系统上符合PE(Portable Executable)格式的二进制文件就可算是计算机程序。

    进程:是计算机程序在计算机操作系统中运行的产物。一个计算机程序可以有多个进程,如一个游戏程序运行多次就会得到多个游戏程序的进程。

    :在早期的操作系统(如早期的 Unix)是基于进程设计的,进程既是掌管资源的单位又是执行的单位;在现代的操作系统中,进程本身不是执行的单位,而是线程的容器,此时的进程仅仅是掌管资源的单位。

    线程:是一个独立的执行流,每个线程有自己独立的线程栈。在同一进程下的线程共享进程的资源。

    二、多线程的概念、为什么需要多线程、多线程的好处与坏处

    多线程:通过软件或硬件的技术实现多个线程并发执行的技术,在通常情况下就是指一个进程中有多个线程(并发执行)的现象。

    我们知道在现在主流计算机体系结构(冯诺依曼体系机构)中CPU是很宝贵的资源,我们要尽可能的提高CPU的利用率。在日常的任务操作中,有大量任务是与IO设备交互,应为IO设备的运行速度比CPU的运行速度慢好几个数量级,在需要大量IO操作的任务或程序(IO密集型)中CPU会有一段不短的时间等待IO设备处理完毕,这就造成了CPU资源的浪费。为了解决这种浪费,为了更充分的利用CPU资源,我们引入多线程技术。针对刚才的例子,我们可以通过把需要和IO设备交互的工作交给一个线程(通常是后台线程)处理,主线程继续执行其他任务,这样就解决问题了,这也就是为什么需要需要多线程技术。

    多线程的好处

    1、提高了CPU的利用率

    2、提升了程序的效率

    多线程的坏处

    1、增加了程序的复杂度(创建线程不复杂、复杂的是线程间的协调工作),可能导致一些很难调试的BUG

    2、比单线程程序消耗更多的资源(每个线程有独立的线程栈,这是需要资源滴)

    建议

    1、将多线程逻辑封装到一个可重用的类库中,这个类库可以被充分的测试。最好不要将多线程处理逻辑和业务处理逻辑耦合得太紧,这样做是在给自己挖坑 悲伤

    三、C# 线程的一些概念与操作

    光说不练假把式,先来看个例子(为了节省文章版幅,因此代码有时候采用缩行处理)

    例1:创建线程

    using System;
    using System.Threading;
    
    class App
    {
        static void Main()
        {
            Thread t = new Thread(WriteY);
            t.Start();
            while(true) {Console.Write("x");}
        }
        
        static void WriteY()
        {
            while(true) {Console.Write("y");}
        }
    }

    编译: csc 1.cs

    运行 1.exe > result.txt

    上面的 “>” 是一个DOS运算符,含义是将 1.exe 打印的字符写入到 result.txt 文本中

    效果:

    QQ截图20141230110550

    引用 Thread in C# 中的一幅图

    QQ截图20141230111130

    刚才我们说每个线程有独立的线程栈,我们写代码验证下

    using System;
    using System.Threading;
    
    // 每个线程内的变量互相不影响,CLR为每个线程在堆上分配独立的线程栈控件
    class App
    {
        static void Main()
        {
            new Thread(Go).Start();
            Go();
        }
        
        static void Go()
        {
            for(int cycles = 0; cycles<5; cycles++) {Console.Write("?");}
        }
    }

    运行效果:??????????

    10 个问号,不是5个问号,正好验证了我们的猜测。

    当线程引用公有变量(全局变量)时它们会共享此公有变量的数据,看下例:

    using System;
    using System.Threading;
    // 实例字段和实例方法
    class App
    {
        bool done;
        static void Main()
        {
            App app = new App();
            new Thread(app.Go).Start();
            app.Go();
        }
        
        void Go()
        {
            if(!done){done = true; Console.WriteLine("This Only Print Once");}
        }
    }

    运行效果: This Only Print Once

    貌似这个程序得到了我们要的效果,其实这样做是存在打印出两次的风险的(虽然概率极小),为了让出现打印两次的概念变大许多,我们改变以下 done = true; 和Console.WriteLine("This Only Print Once"); 的次序变为:

    void Go()
    {
        if(!done){Console.WriteLine("This Only Print Once");done = true; }
    }

    运行结果变为:

    This Only Print Once
    This Only Print Once

    打印了两次,这并不是我们想要的结果。通过这个例子,我们引入线程安全的概念(其实这是一个非线程安全的例子)即在多线程情况下,每次运行结果和单线程运行的结果是一样的,相关变量的值也是在预期范围内的,这样的情况成为线程安全,反之称为非线程安全(说白了,线程安全,就是程序跑出的结果和你猜想的一致,非线程安全就是和你想的不一致)

    在C#中解决上面非线程安全问题的最简单的方式就是用 lock 语句锁住多线程交互的地方,看代码:

    using System;
    using System.Threading;
    
    class App
    {
        static bool done;
        static object _locker = new object();            
        static void Main()
        {
            new Thread(Go).Start();
            Go();
        }
        
        static void Go()
        {
            lock(_locker)
            {
                if(!done) {Console.WriteLine("This Only Print Once");done = true;}
            }
        }    
    }

    运行结果:This Only Print Once

    注意

    1、线程间共享公有数据是多线程复杂性以及产生难以发现BUG的主要原因,尽管多线程之间经常需要共享公共数据,但尽量使共享数据交互的代码尽量简单吧 悲伤

    2、通过lock语句每次只有一个线程进入临界区,其余线程被挡在临界区外(或等待或被阻塞Blocked)直到获取到lock的锁对象。

    3、lock 语句是比较耗资源的,lock语句锁住的范围最好尽量的小。考虑一个极端的情况,在一个多线程程序中用lock语句锁住的范围是整个程序的执行范围,这是神马情况?这种情况不就是和单线程执行效果一样了么,那还用毛线多线程技术和lock语句呀,对吧?!

    Join 与 Sleep语句

    你可以通过Join语句实现线程A等待线程B执行完的功能,看代码:

    using System;
    using System.Threading;
    
    class App
    {
        static void Main()
        {
            Thread t = new Thread(() =>
            {
                for(int i = 0; i< 1000; i++) {Console.Write("y ");}
            });                        
            t.Start();        
            t.Join();        // 主线程等待 线程t执行完毕再结束    
            Console.WriteLine("Thread t has ended!");
        }
    }

    编译 :csc 10_join.cs

    运行 : 10_join.exe > joinResult.txt

    程序执行效果:

    2014-12-30_132428

    其实本例的t.Join(); 加不加效果都一样,原因是用 Thread 创建的线程是前台线程,程序要等到所有前台线程执行完毕后才会退出,这个下面会谈到。

    Thread.Sleep 暂停当前线程指定时间

    Thread.Sleep(TimeSpan.FromHours(1));    // 暂停1小时
    Thread.Sleep(500);    // 暂停 500 毫秒

    注意

    1、Thread.Sleep(0) 或 Thread.Yield() 表示主动放弃当前线程执行权,把CPU资源让给别的线程。Yield(有放弃、让步、屈服之意)

    ThreadStart委托与 ParameterizedThreadStart 委托

    先看看MSDN上的定义:

    public delegate void ThreadStart() // 无参

    public delegate void ParameterizedThreadStart(Object obj)  // 带参数,可以向线程中传递数据,但注意这里只能用Object,因此涉及到装箱拆箱操作

    初始化委托有四种方法(原始方式、方法方式、匿名方法方式、Lambda 方式)详见 类库探源——System.Delegate 

    using System;
    using System.Threading;
    
    class App
    {
        static void Main()
        {        
            Thread t = new Thread(new ThreadStart(WriteY));     // 这是原始的方法
            /* 还可以
            Thread t = new Thread(WriteY);        // 方法方式
            Thread t = new Thread(delegate(){while(true) {Console.Write("y");}});    // 匿名方法
            Thread t = new Thread(() => {while(true) {Console.Write("y");}});    // Lambda 表达式        
            */
            Console.WriteLine(t.IsBackground);        // 用 Thread 创建的线程是前台线程 结果是 False
             t.Start();
            while(true) {Console.Write("x");}
        }
        
        static void WriteY()
        {
            while(true) {Console.Write("y");}
        }
    }

    运行结果:

    2014-12-30_140636

    向线程中传递数据

    首先我们可以用 ParameterizedThreadStart 委托传递参数,看代码:

    using System;
    using System.Threading;
    
    class App
    {
        static void Main()
        {
            string str = "Contents";
            var t = new Thread(Invoke);
            t.Start(str);    // 传参
        }    
        static void Invoke(object str)
        {
            Console.WriteLine(str);
        }    
    }

    上面的代码可以用 Lambda 表达式写得更精练

    static void Main()
    {
        string str = "Contents";
        new Thread((obj) =>{Console.WriteLine(obj);}).Start(str);
    }

    第二种方法用匿名方法(或Lambda表达式)调用一个普通方法变通解决:

    using System;
    using System.Threading;
    
    class App
    {
        static void Main()
        {
            Thread t = new Thread(delegate(){WriteText("hello Thread Param");});
            //可用Lambda表达式 var t = new Thread(() =>WriteText("hello Thread Param"));        
            t.Start();
        }    
        static void WriteText(string txt)
        {
            Console.WriteLine(txt);
        }
    }

    Lambda 表达式值捕获问题

    如我们所见 Lambda 表达式是向线程传值的最强大的方式,但是你必须小心值捕获(captured variables)问题,也有人把这个问题称为闭包下值的问题,看代码:

    using System;
    using System.Threading;
    
    class App
    {
        static void Main()
        {
            for(int i = 0; i < 10; i++)
            {
                new Thread(() => Console.Write(i)).Start();
            }
        }
    }

    运行几次的效果:

    2014-12-30_143858

    可以看出每次都不一样悲伤,也没有一次值为 0123456789 这是为什么呢?

    原因是在循环指向过程中的变量 i 指向了相同的内存位置(i variable refers to the same memory location throughout the loop’s lieftime.)每个线程获取到i的值不会那么巧正好是 0123456789的顺序

    解决方法:用个临时变量存储i

    int temp = i;
    new Thread(() => Console.Write(temp)).Start();

    效果:

    2014-12-30_1451192222

    每次都是 0123456789

    再用更简单例子来说明:

    string text = "t1";
    var t1 = new Thread(() =>Console.WriteLine(text));
    
    string text = "t2";
    var t2 = new Thread(() =>Console.WriteLine(text));
    
    t1.Start();
    t2.Start();

    结果是打印 两个 t2

    给线程取名

    给线程命名在调试的时候很有用,在VS下可以查看线程名字

    Thread.CurrentThread 获取当前正在运行的线程

    using System;
    using System.Threading;
    
    class App
    {
        static void Main()
        {
            Thread.CurrentThread.Name = "Main Thread";
            var worker = new Thread(Go);
            worker.Name = "Worker Thread";
            worker.Start();    // 另取线程调用Go
            Go();    // 主线程中调用Go
        }
        
        static void Go()
        {
            Console.WriteLine("Hello from " + Thread.CurrentThread.Name);
        }
    }

    运行效果:

    2014-12-30_151023

    前台线程与后台线程

    用 Thread 类创建的线程默认是前台线程,应用程序的所有前台线程执行完后才结束,如何特殊设置(如Join)前台线程结束后,后台线程也会终止因为程序都terminate 了嘛 微笑

    看下刚才Join的例子,我们将用后台线程演示,看代码:

    using System;
    using System.Threading;
    
    class App
    {
        static void Main()
        {
            Thread t = new Thread(() =>
            {
                for(int i = 0; i< 1000; i++) {Console.Write("y ");}
            });            
            t.IsBackground = true;
            t.Start();        
            
            Console.WriteLine("Thread t has ended!");
        }
    }

    运行2次效果:

    2014-12-30_15185711111111111

    第一次执行打印一个y 第二次执行打印一个y ,反正都没打印完

    现在用 Join 语句阻塞下

    using System;
    using System.Threading;
    
    class App
    {
        static void Main()
        {
            Thread t = new Thread(() =>
            {
                for(int i = 0; i< 1000; i++) {Console.Write("y ");}
            });            
            t.IsBackground = true;
            t.Start();        
            t.Join();
            Console.WriteLine("Thread t has ended!");
        }
    }

    效果:

    image

    后台线程执行完了。

    注意

    1、Thread 创建的线程默认是前台线程

    2、线程池创建的线程默认是后台线程

    3、线程的前后台状态与线程的优先级没有关系

    线程优先级

    public enum ThreadPriority{Lowest,BelowNormal,Normal,AboveNormal,Highest}

    异常处理

    参见 类库探源——System.Exception 中第四和第五小节

    未完

    本系列参考Joseph Albaharis Thread in C#

  • 相关阅读:
    12.2 ROS NavFn全局规划源码解读_2
    12.1 ROS NavFn全局规划源码_1
    12 ROS Movebase主体源码解读
    11 ROS 动态参数调节
    10. ROS costmap代价地图
    无人驾驶汽车1: 基于Frenet优化轨迹的无人车动作规划方法
    VC下加载JPG/GIF/PNG图片的两种方法
    vc++加载透明png图片方法-GDI+和CImage两种
    供CImage类显示的半透明PNG文件处理方法
    使用MFC CImage类绘制PNG图片时遇到的问题
  • 原文地址:https://www.cnblogs.com/Aphasia/p/4193778.html
Copyright © 2020-2023  润新知