zoukankan      html  css  js  c++  java
  • c# 多线程——入门学习

    1. 概念介绍

    1.1 线程

      线程是操作系统能够进行运算调度的最小单位,包含在进程之中,是进程中的实际运作单位。一条线程指的时进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。.NET 中System.Thread下可以创建线程。

    1.2 主线程

      每个windows进程都包含一个用做程序入口点的主线程。进程入口点(main方法)中创建的第一个线程称为主线程,调用main方法时,主线程被创建。 

    1.3 前台线程

      默认情况下,Thread.Start()方法创建的线程都是前台线程,属性isBackground=true/false能够设置线程的线程是否为后台线程。前台线程能阻止应用程序的终结,只有所有的前台线程执行完毕,CLR(Common Language Runtime,公共语言运行库)才能关闭应用程序。前台线程属于工作者线程。

    1.4 后台线程

      后台线程通过isBackground设置,它不会影响应用程序的终结,当所有前台线程执行完毕后,后台线程无论是否执行完毕,都会被终结。一般后台线程用来做无关紧要的任务(如邮箱天气更新等),后台线程也属于工作者线程。

    2.多线程实现

    2.1 创建线程

      在VS2019中,建立一个控制台应用程序,测试多线程服务。首先开启2个线程workThread、printThread,分别实现数字计数、打印字母。代码实现如下:

       class Program
        {
            static void Main(string[] args)
            {
                //新建两个线程,单独运行
                Thread workThread=new Thread(NumberCount);
                Thread printThread=new  Thread(printNumber);
                workThread.Start();
                printThread.Start();
                Console.WriteLine("Hello World!");
            }
    
            public static void NumberCount()
            {
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine("the number is {0}",i);
                }
            }
    
            public static void printNumber()
            {
                for (char i = 'A'; i < 'J'; i++)
                {
    
                    Console.WriteLine("print character {0}", i);
                }
            }
        }

    运行结果如下:

       根据上述运行结果可以看出,主线程workThread和其他线程printThread运行时相互独立,互不干扰。

    2.2  线程基本属性了解

        static void Main(string[] args)
            {
                Thread th = Thread.CurrentThread;//访问当前正在运行的线程
                bool aliveRes=th.IsAlive;//当前线程的执行状态
                Console.WriteLine("IsAlive= {0}", aliveRes);
                th.IsBackground =false;//线程是否为后台线程
                Console.WriteLine("IsBackground= {0}", th.IsBackground);
                bool isPool= th.IsThreadPoolThread;//当前线程是否属于托管线程池
                Console.WriteLine("isPool= {0}", isPool);
                int sysbol = th.ManagedThreadId;//获取当前托管线程的唯一标识
                Console.WriteLine("ManagedThreadId= {0}", sysbol);
                ThreadPriority pry=th.Priority;//设置线程调度优先级
                Console.WriteLine("pry= {0}", pry);
                ThreadState state=th.ThreadState;//获取当前线程状态值
                Console.WriteLine("state= {0}", state);
                th.Name = "main thread";
                Console.WriteLine("this is {0}",th.Name);
                Console.ReadKey();
                Console.WriteLine("Hello World!");
            }

    2.3 暂停线程

      暂停线程通过调用sleep()方法实现,使得线程暂停但不占用计算机资源,实现代码如下:

        static void NumberCountCouldDelay()
            {
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine("the number is {0}", i);
                    Thread.Sleep(TimeSpan.FromSeconds(1));
                }
            }
            public static void printNumber()
            {
                for (char i = 'A'; i < 'J'; i++)
                {
                    Console.WriteLine("print character {0}", i);
                    Thread.Sleep(TimeSpan.FromSeconds(1));
                }
            }

    运行结果如下:

    2.4 线程池

      线程池是一种多线程处理形式,将任务添加到队列,然后再创建线程后自动启动这些任务。通过线程池创建的任务属于后台任务,每个线程使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有的处理器保持繁忙。

    实现代码及运行结果如下:

     
        static void Main(string[] args)
            {
                Console.WriteLine("this is main thread: ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
                ThreadPool.QueueUserWorkItem(printNumber);
                ThreadPool.QueueUserWorkItem(Go);
                Console.Read();
            }
           
            public static void printNumber(object data)
            {
                for (char i = 'A'; i < 'D'; i++)
                {
                    Console.WriteLine("print character {0}", i);
                    Console.WriteLine("the print process threadId is {0}", Thread.CurrentThread.ManagedThreadId);
                }
            }
            public static void Go(object data)
            {
                Console.Write("this is another thread:ThreadId={0}",Thread.CurrentThread.ManagedThreadId);
            }

    2.5 中止线程

      线程中止采用abort方法,实现如下:

    static void Main(string[] args)
            {
                ThreadStart childref = new ThreadStart(CallToChildThread);
                Console.WriteLine("In Main: Creating the child thread");
                Thread childThread = new Thread(childref);//创建线程,扩展的Thread类
                childThread.Start();//调用start()方法开始子线程的执行
                //停止主线程一段时间
                Thread.Sleep(2000);
                //现在中止子线程
                Console.WriteLine("In Main: Abort the child thread");
                childThread.Abort();
                Console.WriteLine("Hello World!");
            }
            public static void CallToChildThread()
            {
                try
                {
                    //调用abort()方法销毁线程
                    Console.WriteLine("Child thread start");
                    for (int counter=0; counter<=10;counter++)
                    {
                        Thread.Sleep(500);
                        Console.WriteLine(counter);
                    }
                    Console.WriteLine("child thread abort");
                }
                catch (ThreadAbortException e)
                {
                    Console.WriteLine(e);
                    throw;
                }
                finally
                {
                    Console.WriteLine("Couldn't catch the Thread Exception");
                }
            }

      运行程序,出现如下错误:

      经查找,发现.NET CORE平台不支持线程中止,在调用abort方法时会抛出ThreadAbortException异常。

    2.5 跨线程访问

      新建一个winform窗体应用程序,实现点击按钮为textbox赋值,代码如下:

     private void Button1_Click(object sender, EventArgs e)
            {
                Thread thread=new Thread(test);
                thread.IsBackground = true;
                thread.Start();
                Console.ReadLine();
            }
    
            private void test()
            {
                for (int i = 0; i < 10; i++)
                {
                    this.textBox1.Text = i.ToString();
                }
            }

      然而,运行时出现以下错误,内容显示“线程间操作无效:从不是创建控件textBox1的线程访问它”。是因为控件textBox1是由主线程创建的,thread作为另外一个线程,在.NET上执行的是托管代码,c#强制要求代码线程安全,不允许跨线程访问。

      上述问题解决办法如下:(参考https://docs.microsoft.com/en-us/dotnet/framework/winforms/controls/how-to-make-thread-safe-calls-to-windows-forms-controls)

       利用委托实现回调机制,回调过程如下:

      (1)定义并声明委托;

      (2)初始化回调方法;

      (3)定义回调使用的方法

     public partial class UserControl1: UserControl
        {
            private delegate void SetTextboxCallBack(int value);//定义委托
    
            private SetTextboxCallBack setCallBack;
    
            /// <summary>
            ///定义回调使用的方法
            /// </summary>
            /// <param name="value"></param>
            private void SetText(int value)
            {
                textBox1.Text = value.ToString();
            }
           public UserControl1()
            {
                InitializeComponent();
            }
            private void Button1_Click(object sender, EventArgs e)
            {
                //初始化回调函数
                setCallBack=new SetTextboxCallBack(SetText);
                //创建一个线程去执行这个回调函数要操作的方法
                Thread thread = new Thread(test);
                thread.IsBackground = true;
                thread.Start();
                Console.ReadLine();
            }
            public void test()
            {
                for (int i = 0; i < 10; i++)
                {
                    //控件上执行回调方法,触发操作
                    textBox1.Invoke(setCallBack,i);
                }
            }
      }        

    运行结果如下:

     

    2.5 多线程使用委托

      线程的创建通过new Thread来实现,c#中该构造函数的实现有以下4种:

    • public Thread(ThreadStart start){}
    • public Thread(ParameterizedThreadStart start){}
    • public Thread(ThreadStart start, int maxStackSize){}
    • public Thread(ParameterizedThreadStart start, int maxStackSize){}

    其中,参数ThreadStart定义为:

    public delegate void ThreadStart();//无参数无返回值的委托

    参数ParameterizedThreadStart 定义为:

    public delegate void ParameterizedThreadStart(object obj);//有参数无返回值的委托

    因此,对无返回值的委托实现如下。

    2.5.1 无参数无返回值的委托

      对于无参数无返回值的委托,是最简单原始的使用方法。Thread thread= new Thread(new ThreadStart(()=>参数),其中参数为ThreadStart类型的委托。此类多线程代码实现如下:

    class Program
        {
            public delegate void ThreadStart();//新建一个无参数、无返回值的委托
            static void Main(string[] args)
            {
                Thread thread=new Thread(new System.Threading.ThreadStart(NumberCount));
                thread.IsBackground = true;
                thread.Start();
                for (char i = 'A'; i < 'D'; i++)
                {
                    Console.WriteLine("print character {0},the threadId id ={1}", i, Thread.CurrentThread.ManagedThreadId);
                }
                Console.WriteLine("Hello World!");
            }
    
            public static void NumberCount()
            {
                for (int i = 0; i < 3; i++)
                {
                    Console.WriteLine("the number is {0},the threadId id ={1}", i, Thread.CurrentThread.ManagedThreadId);
                }
            }
        }

    2.5.2 有参数无返回值的委托

      对于有参数无返回值的委托,实现代码如下:

    class Program
        {
            public delegate void ThreadStart(int i);//新建一个无参数、无返回值的委托
            static void Main(string[] args)
            {
                Thread thread=new Thread(new ParameterizedThreadStart(NumberCount));
                thread.IsBackground = true;
                thread.Start(3);
                for (char i = 'A'; i < 'D'; i++)
                {
                    Console.WriteLine("print character {0},the threadId id ={1}", i, Thread.CurrentThread.ManagedThreadId);
                }
                Console.WriteLine("Hello World!");
            }
    
            public static void NumberCount(object i)
            {
               Console.WriteLine("the number is {0},the threadId id ={1}", i, Thread.CurrentThread.ManagedThreadId);
            }
    
        }

    运行结果为:

    2.5.2 有参数有返回值的委托

       对于有参数有返回值的委托,采用异步调用实现,如下所示:

    2.6 异步实现

    2.6.1 Task.Result

       ..NET中引入了System.Threading.Tasks,简化了异步编程的方式,而不用直接和线程、线程池打交道。 System.Threading.Tasks中的类型被称为任务并行库(TPL),TPL使用CLR线程池(TPL创建的线程都是后台线程)自动将应用程序的工作动态分配到可用的CPU的中。

      Result方法可以返回Task执行后的结果。但是在.NET CORE的webapi中使用result方法来获取task的输出值,会造成当前API线程阻塞等待到task执行完成后再继续。以下代码中,get方法中的线程id-57,调用一个新线程执行task后,等待TaskCaller()执行结果(threadid=59),待TaskCaller()方法执行完成后,原来的线程继续之后之后的语句,输出threadid=57

      public class ValuesController:Controller
        {
            //async/await是用来进行异步调用的形式,
            [HttpGet("get")]
            public async Task<string> Get()
            {
                var info = string.Format("api执行线程{0}",Thread.CurrentThread.ManagedThreadId);//get方法中的线程
                //调用新线程执行task任务
                var infoTask = TaskCaller().Result;//调用result方法获取task的值
                var infoTaskFinished = string.Format("api执行线程(taks调用completed){0}", Thread.CurrentThread.ManagedThreadId);
                return string.Format("{0},{1},{2}", info, infoTask, infoTaskFinished);
            }
            public async Task<string> TaskCaller()
            {
                await Task.Delay(5000);
                return string.Format("task 执行线程{0}", Thread.CurrentThread.ManagedThreadId);
            }
        }

    运行结果如下:

     2.6.2 Async&Await

      c#中async关键字用来指定方法,Lambda表达式或匿名方法自动以异步的方式来调用。async/await是用来进行异步调用的形式,内部采用线程池进行管理。如果使用await,在调用await tasjCall()是不会阻塞get方法的主线程,主线程会被释放,新的线程执行完task后继续执行await后的代码,从而减少了线程切换的开销,而之前的线程则空闲了。

     public class ValuesAwaitController : Controller
        {
            [HttpGet("get")]
            public async Task<string> Get()
            {
                var info = string.Format("api执行线程{0}",Thread.CurrentThread.ManagedThreadId);//get方法中的线程
                //调用新线程执行task任务
                var infoTask = await TaskCaller();//使用await调用不会阻塞Get()中线程
                var infoTaskFinished = string.Format("api执行线程(taks调用completed){0}", Thread.CurrentThread.ManagedThreadId);
                return string.Format("{0},{1},{2}", info, infoTask, infoTaskFinished);
            }
            public async Task<string> TaskCaller()
            {
                await Task.Delay(5000);
                return string.Format("task 执行线程{0}", Thread.CurrentThread.ManagedThreadId);
            }
        }

    运行结果如下:

     

       Task.result与await关键字具有类似的功能可以获取到任务的返回值,但本质上Task.result会让外层函数执行线程阻塞知道任务完成,而使用await外层函数线程不会阻塞,而是通过任务执行线程来执行await后的代码。

    • 默认创建的Thread是前台线程,创建的Task为后台线程;
    • ThreadPool创建的线程都是后台线程;
    • 任务并行库(TPL)使用的是线程池计数;
    • 调用async标记的方法,刚开始是同步执行,只有当执行到await标记的方法中的异步任务时,才会挂起。
  • 相关阅读:
    javac 小记
    安全专家的工具箱
    MyBatis 缓存机制(十三)
    SpringMVC 环境搭建
    MyBatis 模糊查询的 4 种实现方式
    MyBatis 项目开发中是基于 XML 还是注解?
    MyBatis 动态 SQL 语句中出现 '<' 的问题
    数据库设计的三大范式
    mybatis 同时使用 XML 和注解
    数据库事务
  • 原文地址:https://www.cnblogs.com/mo-lu/p/11093914.html
Copyright © 2011-2022 走看看