1. 概念介绍web
1.1 线程编程
线程是操做系统可以进行运算调度的最小单位,包含在进程之中,是进程中的实际运做单位。一条线程指的时进程中一个单一顺序的控制流,一个进程中能够并发多个线程,每条线程并行执行不一样的任务。.NET 中System.Thread下能够建立线程。c#
1.2 主线程windows
每一个windows进程都包含一个用作程序入口点的主线程。进程入口点(main方法)中建立的第一个线程称为主线程,调用main方法时,主线程被建立。 api
1.3 前台线程安全
默认状况下,Thread.Start()方法建立的线程都是前台线程,属性isBackground=true/false可以设置线程的线程是否为后台线程。前台线程能阻止应用程序的终结,只有全部的前台线程执行完毕,CLR(Common Language Runtime,公共语言运行库)才能关闭应用程序。前台线程属于工做者线程。多线程
1.4 后台线程并发
后台线程经过isBackground设置,它不会影响应用程序的终结,当全部前台线程执行完毕后,后台线程不管是否执行完毕,都会被终结。通常后台线程用来作可有可无的任务(如邮箱天气更新等),后台线程也属于工做者线程。异步
2.多线程实现async
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种:
其中,参数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后的代码。