zoukankan      html  css  js  c++  java
  • 【C#多线程】1.Thread类的使用及注意要点

    Thread随便讲讲

      因为在C#中,Thread类在我们的新业务上并不常用了(因为创建一个新线程要比直接从线程池拿线程更加耗费资源),并且在.NET4.0后新增了Task类即Async与await关键字,使得我们基本不再用Thread了,不过在学习多线程前,有必要先了解下Thread类,这里就先随便讲讲Thread。

    1.使用多线程的几种方式

      多线程Thread类只支持运行两种方法,一种是无参数并且无返回值的方法,第二种是有一个Object类型参数(有且只能有一个参数,并且必须是Object类型)且无返回值的方法。如果想让多线程方法携带多个参数,可以将多个参数放入一个集合或数组中传入方法。

      下面例子使用了控制台来演示多线程的简单使用:

    using System;
    using System.Threading;
    
    namespace ConsoleApplication1
    {
        class Program
        {
            //无参数无返回值方法
            public static void DoSomething()
            {
                for (int i = 0; i < 100; i++)
                {
                    Thread.Sleep(500);
                }
            }
            //有参数无返回值方法
            public static void DoSomethingWithParameter(object obj)
            {
                for (int i = 0; i < (int)obj; i++)
                {
                    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
                    Thread.Sleep(500);
                }
            }
    
       
            static void Main(string[] args)
            {
                //获取主线程ID
                int currentThreadId = Thread.CurrentThread.ManagedThreadId;
                Console.WriteLine($"---------主线程<{currentThreadId}>开始运行---------");
                //多线程运行无参数方法方式1
                ThreadStart ts = DoSomething;//ThreadStart是一个无参数,无返回值的委托
                Thread thread1 = new Thread(ts);
                thread1.Start();
                
                //多线程运行无参数方法方式2
                Thread thread2 = new Thread(DoSomething);//可省略ThreadStart
                thread2.Start();
    
                //多线程运行有参数方法方式1
                //ParameterizedThreadStart是一个有一个Object类型参数,但是无返回值的委托。
                ParameterizedThreadStart pts = DoSomethingWithParameter;
                Thread thread3 = new Thread(pts);
                thread3.Start(100);
                
                //多线程运行有参数方法方式2
                //可以省略ParameterizedThreadStart
                Thread thread4 = new Thread(DoSomethingWithParameter);
                thread4.Start(100);
    
                //还可以使用lamda表达式简化多线程写法
                new Thread(() =>
                {
                    for (int i = 0; i < 100; i++)
                    {
                        Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
                        Thread.Sleep(500);
                    }
                }).Start();
                Console.WriteLine($"---------主线程<{currentThreadId}>运行结束---------");
            }
        }
    }

      运行结果如下:

      

     2.前台线程与后台线程

    • 前台线程

      如主线程(或称为UI线程)就是前台线程,默认Thread的实例均为前台线程,前台线程的特点是,如果当前应用的前台线程没有全部运行完毕,那么当前应用就无法退出。举个例子,我们知道正常情况下,控制台应用在Main方法结束后会自动结束当前进程,如果我们在Main方法中创建了一个新Thread线程,并使其保持运行,那么即使Main方法执行完毕,控制台进程也无法自动关闭(除非手动右上角点×)。就如下图情况,画红圈的地方表示Main方法执行完毕,可是程序依旧在运行,所以我们一般在用Thread的时候会将Thread设置为后台线程。

    • 后台线程

      后台线程与前台线程的唯一区别是,它不会去影响程序的生老病死,当程序的前台线程全部关闭(即程序退出),那么即使程序的后台线程依旧在执行任务,那么也会强制关闭。

      设置Thread为后台线程的方式:

            Thread tt = new Thread(DoSomething);
            tt.IsBackground = true;//设置tt为后台线程
            tt.Start();

      前台线程与后台线程对程序的影响效果看似好像不算大,但是如果我们在做Winform或者WPF项目时,若在某窗体内执行一个新线程任务(这个新线程是前台线程),如果在任务执行期间关闭程序,此时会发现,虽然界面都被关闭,但是计算机任务管理器中此程序依旧还在运行(并且如果在新线程中执行的任务异常导致线程无法关闭,那么这个程序就会一直在后台跑下去),再次开启程序可能会导致打不开等后果,这种行为是非常不好的。所以我们一般使用多线程Thread类时,最好顺手将它设置为后台线程。我们可以举个例子。

            static void Main(string[] args)
            {
                //获取主线程ID
                int currentThreadId = Thread.CurrentThread.ManagedThreadId;
                Console.WriteLine($"---------主线程<{currentThreadId}>开始运行---------");
    
                //执行一个大概可以运行50秒的新线程
                Thread t = new Thread(() =>
                {
                    for (int i = 0; i < 100; i++)
                    {
                        Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
                        Thread.Sleep(500);
                    }
                });
                t.IsBackground = true;//设置t为后台线程
                t.Start();
    
                Console.WriteLine($"---------主线程<{currentThreadId}>运行结束---------");
            }

      这个例子的运行结果就不截图了,因为控制台会一闪而过(立即执行完Main方法便关闭),即使后台线程t还在执行任务,但是也会强制关闭。

    3.让主线程等待新线程执行完成后再继续执行(使用Thread的Join方法)

      直接上代码:

            static void Main(string[] args)
            {
                //获取主线程ID
                int currentThreadId = Thread.CurrentThread.ManagedThreadId;
                Console.WriteLine($"---------主线程<{currentThreadId}>开始运行---------");
    
                //执行一个大概可以运行50秒的新线程
                Thread t = new Thread(() =>
                {
                    for (int i = 0; i < 20; i++)
                    {
                        Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
                        Thread.Sleep(500);
                    }
                });
                t.IsBackground = true;//设置t为后台线程
                t.Start();
    
                t.Join();//在t线程执行期间,如果主线程调用t线程的Join方法,主线程会卡在这个地方直到t线程执行完毕
    
                Console.WriteLine($"---------主线程<{currentThreadId}>运行结束---------");
            }

    4.Thread实例的其他常用方法

      直接看代码注释吧:

            static void Main(string[] args)
            {
                //执行一个大概可以运行50秒的新线程
                Thread t = new Thread(DoSth);
                t.IsBackground = true;//设置t为后台线程
                t.Start();
    
                t.Join();//在t线程执行期间,如果主线程调用t线程的Join方法,主线程会卡在这个地方知道t线程执行完毕
                t.Priority = ThreadPriority.Normal;//设置线程调度的优先级
                ThreadState rhreadState = t.ThreadState;//获取线程运行状态。
                bool b = t.IsAlive;//获取线程当前是否存活
                t.Interrupt();//中断当前线程
                t.Abort();//终止线程          
            }

    5.Thread类的常用方法

      直接看代码注释吧:

            static void Main(string[] args)
            {
                //使得当前线程暂停1秒再继续执行,此处会暂停主线程1秒钟
                //如果写在其他线程执行的方法中,会让执行那个方法的线程暂停1秒再继续执行)
                Thread.Sleep(1000);
    
                //获取当前执行线程的线程实例
                Thread t = Thread.CurrentThread;             
            }

      

    6.使用多线程需要注意的要点

       (1)子线程不可以直接调用UI线程(即主线程)的UI对象,但是可以调用在主线程自定义的对象

      我们在做Winform或WPF开发时,例如在前端有一个TextBox文本框,其Name属性为textBox,那么如果我们在此窗体内开启了个子线程,并在子线程内对textBox.Text赋值,是会报错的,因为子线程无法访问主线程的UI元素(实质是UI元素必须由创建它的线程去操作)。

      如下代码,子线程操作主线程创建的对象时不会报错,但是子线程操作主线程创建的UI对象时会报错:

            private void button1_Click(object sender, EventArgs e)
            {
                Student stu = new Student();//主线程创建的Student类实例
                new Thread(() =>
                {
                    stu.Name = "ccc";//子线程操作主线程创建的对象并不会报错。
                    textBox1.Text = "abc";//子线程直接调用UI线程textBox1会报错
                }).Start();                            
            }

       解决思路:在子线程想操作UI线程的UI元素时,呼叫主线程去操作即可,代码如下:

            delegate void DoSth(string str);//创建一个委托
            public void SetTextBox(string str)//创建一个委托方法用于改变主线程textBox的值
            {
                textBox1.Text = str;
            }
         //按钮点击事件
    private void button1_Click(object sender, EventArgs e) {
           //在子线程内执行....
    new Thread(() => { //----------------------详细写法------------------------ DoSth delegateMethod = new DoSth(SetTextBox);//创建方法的委托 //this指当前Window //this.Invoke指让创建窗体的线程执行某委托方法 //第二个参数是传入委托方法即SetTextBox的参数 this.Invoke(delegateMethod, "abc"); //----------------------简写方式---------------------- this.Invoke(new Action(() => { textBox1.Text = "abc";//子线程直接调用UI线程textBox1会报错 })); }).Start();

      补充:上面代码是Winform跨线程操作UI元素的常用方式,那么WPF怎么跨线程操作UI呢?直接看下面代码吧

        //方式1(常用):获取当前应用的UI线程,执行某方法
        App.Current.Dispatcher.Invoke(() =>
        {
            textBox1.Text="abc"
        });
    
        //方式2(只能在this是当前Window或可以获取到窗体实例的情况下使用):
        this.Dispatcher.Invoke(new Action(()=>
        {
            textBox1.Text="abc"
        })); 

      (2)多线程同时访问一个资源时,要注意同步问题。

      比如两个及以上的线程同时访问一个资源(可以是文件,可以是对象),如果没有注意同步问题,会导致以下问题。直接看代码

            private void button1_Click(object sender, EventArgs e)
            {
                int num = 0;
                //创建两个线程对num进行累加,各加100000,理论上线程执行完毕后最后的值应该是200000
                Thread t1 = new Thread(() =>
                {
                    for (int i = 0; i < 100000; i++)
                    {
                        num++;
                    }
                });
                Thread t2 = new Thread(() =>
                {
                    for (int i = 0; i < 100000; i++)
                    {
                        num++;
                    }
                });
                //两个子线程同时执行
                t1.Start();
                t2.Start();
                //等待t1与t2线程执行完毕再继续执行
                t1.Join();
                t2.Join();
                Console.WriteLine(num.ToString());//输出num的值
            }

      结果:

      结果并不是200000,因为t1与t2线程同时对num进行自增操作时候,经常会出现t1读取到了num为99,自增1,结果100赋值给num,但是在t1刚读取到num值并且还没进行自增操作时,t2也读取到了num为99,自增下也是100赋值给num。也就是说t1与t2进行了相同的操作。

      

       如何避免当前这个问题呢?就需要在多线程访问一个资源时,进行资源同步处理。那什么是同步呢?同步是指我用完了你才能用,你我不能同时使用一个资源。这个问题的详细解决方法会在该系列以后的博客中写。

      

      下节我们会简单讲讲线程池+以前的两种异步处理机制。

  • 相关阅读:
    配置数据同步
    NET移动设备开发
    计算两个日期之间的工作日数
    ActionScript3.0程序开发工具
    常用JS积累之获取节点高度(基于浏览器)
    推荐40个优秀的免费CSS工具
    #include语法
    CSS3属性boxshadow使用教程
    CSS元素背景透明
    js获取网页高度
  • 原文地址:https://www.cnblogs.com/512kd/p/11802321.html
Copyright © 2011-2022 走看看