一、多线程的相关概念
1.进程:是操作系统结构的基础;是一个正在执行的程序;计算机中正在运行的程序实例;可以分配给处理器并由处理器执行的一个实体;由单一顺序的执行显示,一个当前状态和一组相关的系统资源所描述的活动单元。
2.线程:线程是程序中一个单一的顺序控制流程。是程序执行流的最小单元。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。
3.多线程:在单个程序中同时运行多个线程完成不同的工作,称为多线程。
理解:其实更容易理解一点进程与线程的话,可以举这样一个例子:把进程理解成为一个运营着的公司,然而每一个公司员工就可以叫做一个线程。每个公司至少要有一个员工,员工越多,如果你的管理合理的话,公司的运营速度就会越好。这里官味一点话就是说。cpu大部分时间处于空闲时间,浪费了cpu资源,多线程可以让一个程序“同时”处理多个事情,提高效率。
(一)单线程问题演示
创建一个WinForm应用程序,这里出现的问题是,点击按钮后如果在弹出提示框之前,窗体是不能被拖动的。
private void button1_Click(object sender, EventArgs e) { for (int i = 0; i < 10000000000; i++) { i += 1; } MessageBox.Show("出现后能拖动,提示没出现之前窗体不能被拖动"); }
原因:运行这个应用程序的时候,窗体应用程序自带一个叫做UI的线程,这个线程负责窗体界面的移动大小等。如果点击按钮则这个线程就去处理这个循环计算,而放弃了其它操作,故而窗体拖动无响应。这就是单线程带来的问题。
解决办法:使用多线程,我们自己创建线程。把计算代码放入我们自己写的线程中,UI线程就能继续做他的界面响应了。
(二)线程的创建
线程的实现:线程一定是要执行一段代码的,所以要产生一个线程,必须先为该线程写一个方法,这个方法中的代码,就是该线程中要执行的代码,然而启动线程时,是通过委托调用该方法的。线程启动是,调用传过来的委托,委托就会执行相应的方法,从而实现线程执行方法。
//创建线程 private void button1_Click(object sender, EventArgs e) { //ThreadStart是一个无参无返回值的委托。 ThreadStart ts = new ThreadStart(js); //初始化Thread的新实例,并通过构造方法将委托ts做为参数赋初始值。 Thread td = new Thread(ts); //需要引入System.Threading命名空间 //运行委托 td.Start(); } //创建的线程要执行的函数。 void js() { for (int i = 0; i < 1000000000; i++) { i += 1; } MessageBox.Show("提示出现前后窗体都能被拖动"); }
把这个计算写入自己写的线程中,就解决了单线程中的界面无反应缺陷。
小结:创建线程的4个步骤:
1.编写线程索要执行的方法。
2.引用System.Threading命名空。
3.实例化Thread类,并传入一个指向线程所要运行方法的委托。
4.调用Start()方法,将该线程标记为可以运行的状态,但具体执行时间由cpu决定。
(三)方法重入(多个线程执行一个方法)
由于线程可与同属一个进程的其它线程共享进程所拥有的全部资源。
所以多个线程同时执行一个方法的情况是存在的,然而这里不经过处理的话会出现一点问题,线程之间先后争抢资源,致使数据计算结果错乱。
//创建线程 private void button1_Click(object sender, EventArgs e) { //ThreadStart是一个无参无返回值的委托。 ThreadStart ts = new ThreadStart(js); //初始化Thread的新实例,并通过构造方法将委托ts做为参数赋初始值。 Thread td = new Thread(ts); //需要引入System.Threading命名空间 //运行委托 td.Start(); } //创建的线程要执行的函数。 void js() { for (int i = 0; i < 1000000000; i++) { i += 1; } MessageBox.Show("提示出现前后窗体都能被拖动"); }
出错现象:点击按钮后TextBox1中数据为2000+或2000,如果你看到的数据一直是2000说明你的计算机cpu比较牛X,这样的话你想看到不是2000的话,你可以多点击几次试试,真不行的话,代码中给TextBox1赋值为0,换做在界面中给textBox1数值默认值为0试试看。
出错原因:两个进程同时计算这个方法,不相干扰应该每个线程计算的结果都是2000的,但是这里的结果输出却让人以外,原因是第一个两个线程同时计算,并不是同时开始计算,而是根据cpu决定的哪个先开始,哪个后开始,虽然相差时间不多,但后开始的就会取用先开始计算过的数据计算,这样就会导致计算错乱。
解决办法:解决这个的一个简单办法解释给方法加锁,加锁的意思就是第一个线程取用过这个资源完毕后,第二个线程再来取用此资源。形成排队效果。
下面给方法加锁。
//多线程要重入的方法,这里加锁。 void js() { lock (this) { int a = Convert.ToInt32(textBox1.Text); for (int i = 0; i < 2000; i++) { a++; textBox1.Text = a.ToString(); } } }
给方法加过锁后,线程一前一后取用资源,就能避免不可预计的错乱结果,第一个线程计算为2000,第二个线程计算就是从2000开始,这里的结果就为4000。
小结:多线程可以同时运行,提高了cpu的效率,这里的同时并不是同时开始,同时结束,他们的开始是由cpu决定的,时间相差不大,但会有不可预计的计算错乱,这里要注意类似上面例子导致的方法重入问题。
(四)前台线程后台线程
.Net的公用语言运行时能区分两种不同类型的线程:前台线程和后台线程。这两者的区别就是:应用程序必须运行完所有的前台线程才可以退出;而对于后台线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束。
问题:关闭了窗口,消息框还能弹出。
private void button1_Click(object sender, EventArgs e) { //开启一个线程,对js方法进行计算 ThreadStart ts2 = new ThreadStart(js); Thread td2 = new Thread(ts2); td2.Start(); } void js() { for (int i = 0; i < 2000000000; i++) //如果看不出效果这里的2后面多加0 { i++; } MessageBox.Show("关闭了窗口我还是要出来的!"); }
原因:.Net环境使用Thread建立线程,线程默认为前台线程。即线程属性IsBackground=false,而前台线程只要有一个在运行则应用程序不关闭,所以知道弹出消息框后应用程序才算关闭。
解决办法:在代码中设置td2.IsBackground=true;
(五)线程执行带参数的方法
//创建一个执行带参数方法的线程 private void button1_Click(object sender, EventArgs e) { //ParameterizedThreadStart这是一个参数类型为object的委托 ParameterizedThreadStart pts=new ParameterizedThreadStart(SayHello); Thread td2 = new Thread(pts); td2.Start("张三"); //参数值先入这里 } void SayHello(object name) { MessageBox.Show("你好,"+name.ToString()+"!"); }
(六)线程执行带多参数的方法
其实还是带一参数的方法,只不过是利用参数类型为object的好处,这里将类型传为list类型,貌似多参。
//创建一个执行带多个参数的方法线程 private void button1_Click(object sender, EventArgs e) { List<string> list = new List<string> { "张三", "李四", "王五" }; //ParameterizedThreadStart这是一个参数类型为object的委托 ParameterizedThreadStart pts=new ParameterizedThreadStart(SayHello); Thread td2 = new Thread(pts); td2.Start(list); //参数值先入这里 } void SayHello(object list) { List<string> lt = list as List<string>; for (int i = 0; i < lt.Count; i++) { MessageBox.Show("你好," + lt[i].ToString() + "!"); } }
二、线程的重要属性
(一)确定多线程的结束时间,thread的IsAlive属性
在多个线程运行的背景下,了解线程什么时候结束,什么时候停止是很有必要的。
案例:老和尚念经计时,2本经书,2个和尚念,一人一本,不能撕破,最短时间念完,问老和尚们念完经书最短需要多长时间。
分析:首先在开始念经的时候给计时,记为A,最后在记下慢和尚念完经书时的时间,记为B。求B-A
代码:IsAlive属性:标识此线程已启动并且尚未正常终止或中止,则为 true,再念,没念完,努力中;否则为 false,念完啦,歇着。
//和尚1,和尚2 public Thread td1, td2; public void StarThread() { //开启一个线程执行Hello方法,即和尚1念菠萝菠萝蜜 ThreadStart ts = new ThreadStart(Hello); td1 = new Thread(ts); td1.Start(); } public void StarThread1() { //开启一个线程执行Welcome方法,即和尚2念大金刚经 ThreadStart ts = new ThreadStart(Welcome); td2 = new Thread(ts); td2.Start(); } public string sayh="", sayw=""; //菠萝菠萝蜜 public void Hello() { //念 sayh = "Hellow everyone ! "; } //大金刚经 public void Welcome() { //念 sayw = "Welcome to ShangHai ! "; //偷懒10秒 Thread.Sleep(10000); } protected void btn_StarThread_Click(object sender, EventArgs e) { //记时开始,预备念 Response.Write("开始念的时间: "+DateTime.Now.ToString() + "</br>"); //和尚1就位 StarThread(); //和尚2就位 StarThread1(); int i = 0; while (i == 0) { //判断线程的IsAlive属性 //IsAlive标识此线程已启动并且尚未正常终止或中止,则为 true;否则为 false。 //如果两个都为false说明,线程结束终止 if (!td1.IsAlive && !td2.IsAlive) { i++; if (i == 1) { //念得内容,绕梁三尺。 Response.Write("我们年的内容: "+(sayh + " + " + sayw) + "</br>"); Response.Write("念完时的时间: "+DateTime.Now.ToString()); Response.End(); } } } }
(二)、线程优先级,thread的ThreadPriority属性
线程优先级区别于线程占有cpu时间的多少,当然优先级越高同等条件下占有的cpu时间越多。级别高的执行效率要高于级别低的。
优先级有5个级别:Lowest<BelowNormal<Normal<AboveNormal<Highest;默认为Normal。
案例:老和尚娶媳妇。佛祖说:你们3个和尚,清修刻苦,现特许你们娶媳妇啦,不过娶媳妇的只能是你们三个中间的一人。条件是我手中的经书谁能先念完,谁可以娶。
分析:和尚平时都很刻苦,各有特点,平时和尚1在lowest环境下念经,和尚2在normal环境下念经,和尚3在Highest环境下念经。
protected void btn_StarThread_Click(object sender, EventArgs e) { Write(); } //i为和尚1念的页数 //j为和尚2念的页数 //k为和尚3念的页数 //c为经书总页数 int i=0,j=0,k=0,c=10000000; //和尚1念经 public void Jsi() { while (i <= c) { i+=1; } } //和尚2念经 public void Jsj() { while (j <= c) { j+=1; } } //和尚3念经 public void Jsk() { while (k <= c) { k+=1; } } public void Write() { //开启线程计算i ThreadStart sti = new ThreadStart(Jsi); Thread tdi = new Thread(sti); //设置线程优先级为Lowest。和尚1在Lowest环境下念经 tdi.Priority = ThreadPriority.Lowest; //开启线程计算j ThreadStart stj = new ThreadStart(Jsj); Thread tdj = new Thread(stj); //设置线程优先级为Normal。和尚2在Normal环境下念经 tdj.Priority = ThreadPriority.Normal; //开启线程计算k ThreadStart stk = new ThreadStart(Jsk); Thread tdk = new Thread(stk); //设置线程优先级为Highest。和尚3在Highest环境下念经 tdk.Priority = ThreadPriority.Highest; //开始 tdj.Start(); tdk.Start(); tdi.Start(); int s = 0; while (s==0) { if (k > c) { s++; Response.Write("比赛结束,结果如下:</br></br>"); Response.Write("和尚1在Lowest环境下念经:" + i + "页</br>和尚2在Normal环境下念经:" + j + "页</br>和尚3在Highest环境下念经:" + k + "页</br></br>"); Response.Write("佛祖又说:你念或者不念,苍老师,就在那里!"); Response.End(); } } } 复制代码
为啦方便期间,从这以后,我要用控制台程序演示,操控线程。
(三)线程通信之Monitor类
如果,你的线程A中运行锁内方法时候,需要去访问一个暂不可用资源B,可能在B上需耗费很长的等待时间,那么这时候你的线程A,将占用锁内资源,阻塞其它线程访问锁定内容,造成性能损失。你该怎么解决这样子的问题呢?这样,让A暂时放弃锁,停留在锁中的,允许其它线程访问锁,而等B资源可用时,通知A让他继续锁内的操作。是不是解决啦问题,这样就用到啦这段中的Monitor类,提供的几个方法:Wait(),Pulse(),PulseAll(),这几个方法只能在当前锁定中使用。
Wait():暂时中断运行锁定中线程操作,释放锁,时刻等待着通知复活。
Pulse():通知等待该锁线程队列中的第一个线程,此锁可用。
PulseAll():通知所有锁,此锁可用。
案例:嵩山少林和尚开会。主持人和尚主持会议会不停的上舞台讲话,方丈会出来宣布大会开始,弟子们开始讨论峨眉山怎么走。
分析:主持人一个线程,方丈一个线程,弟子们一个线程,主持人贯彻全场。
public class MutexSample { static void Main() { comm com = new comm(); com.dhThreads(); Console.ReadKey(); } } public class comm { //状态值:0时主持人和尚说,1时方丈说,2时弟子们说,3结束。 int sayFla; //主持人上台 int i = 0; public void zcrSay() { lock (this) { string sayStr; if (i == 0) { //让方丈说话 sayFla = 1; sayStr = Thread.CurrentThread.Name+"今晚,阳光明媚,多云转晴,方丈大师,程祥云而来,传扬峨眉一隅,情况如何,还请方丈闪亮登场。"; Console.WriteLine(sayStr); i++; //此时sayFla=1通知等待的方丈线程运行 Monitor.Pulse(this); //暂时锁定主持人,暂停到这里,释放this让其它线程访问 Monitor.Wait(this); } //被通知后,从上一个锁定开始运行到这里 if (i == 1) { //让弟子说话 sayFla = 2; sayStr = Thread.CurrentThread.Name + "看方丈那幸福的表情,徜徉肆恣,愿走的跟他去吧。下面请弟子们各抒己见"; Console.WriteLine(sayStr); i++; //此时sayFla=12通知等待的弟子线程运行 Monitor.Pulse(this); //暂时锁定主持人,暂停到这里,释放this让其它线程访问 Monitor.Wait(this); } //被通知后,从上一个锁定开始运行到这里 if (i == 2) { sayFla = 3; sayStr = Thread.CurrentThread.Name + "大会结束!方丈幸福!!苍老师你在哪里?!!放开那女孩 ..."; Console.WriteLine(sayStr); i++; Monitor.Wait(this); } } } //方丈上台 public void fzSay() { lock (this) { while (true) { if (sayFla != 1) { Monitor.Wait(this); } if (sayFla == 1) { Console.WriteLine(Thread.CurrentThread.Name + "蓝蓝的天空,绿绿的湖水,我看见,咿呀呀呀,看见一老尼,咿呀呀,在水一方。愿意来的一起来,不愿来的苍老师给你们放寺里。。咿呀呀,我走啦。。。"); //交给主持人 sayFla = 0; //通知主持人线程,this可用 Monitor.Pulse(this); } } } } //弟子上台 public void dzSay() { lock (this) { while (true) { if (sayFla != 2) { Monitor.Wait(this); } if (sayFla == 2) { Console.WriteLine(Thread.CurrentThread.Name + "果真如此的话,还是方丈大师自己去吧!! 祝福啊 .... "); //交给主持人 sayFla = 0; Monitor.Pulse(this); } } } } public void dhThreads() { Thread zcrTd = new Thread(new ThreadStart(zcrSay)); Thread fzTd = new Thread(new ThreadStart(fzSay)); Thread dzTd = new Thread(new ThreadStart(dzSay)); zcrTd.Name = "主持人:"; fzTd.Name = "方丈:"; dzTd.Name = "弟子:"; zcrTd.Start(); fzTd.Start(); dzTd.Start(); } }
(四)线程排队之Join
多线程,共享一个资源,先后操作资源。Join()方法,暂停当前线程,直到指定线程运行完毕,才唤醒当前线程。如果没有Join,多线程随机读取公用资源,没有先后次序。
案例:两个和尚念一本经书,老和尚年前半本书,小和尚念后半本书,小和尚调皮,非要先念,就给老和尚用迷魂药啦。。
分析:一本书6页,小和尚4-6,老和尚1-3,两个和尚,两个线程。
public class 连接线程Join { //小和尚 public static Thread litThread; //老和尚 public static Thread oldThread; //老和尚念经 static void oldRead() { //老和尚被小和尚下药 litThread.Join(); //暂停oldThread线程,开始litThread,直到litThread线程结束,oldThread才继续运行,如果不适用Join将小和尚一句,老和尚一句,随即没有规则的。 for (int i = 1; i <= 3; i++) { Console.WriteLine(i); } } //小和尚念经 static void litRead() { for (int i = 4; i <= 6; i++) { Console.WriteLine(i); } } static void Main(string[] args) { oldThread = new Thread(new ThreadStart(oldRead)); litThread = new Thread(new ThreadStart(litRead)); oldThread.Start(); // FristThread.Join(); //暂停oldThread线程,开始litThread,直到litThread线程结束,oldThread才继续运行 litThread.Start(); Console.ReadKey(); } }
(五)多线程互斥锁Mutex
互斥锁是一个同步的互斥对象,适用于,一个共享资源,同时只能有一个线程能够使用。
共享资源加互斥锁,需要两部走:1.WaitOne(),他将处于等待状态知道可以获取资源上的互斥锁,获取到后,阻塞主线程运行,直到释放互斥锁结束。2.ReleaseMutex(),释放互斥锁,是其它线程可以获取该互斥锁。
案例:和尚写日记。最近寺庙香火不旺,为啦节约用水,方丈发话两个和尚用一个本子写日记。
分析:好比多个线程写日志,同时只能有一个线程写入日志文件。
public class 多线程互斥锁Mutex { static void Main(string[] args) { IncThread ict = new IncThread("大和尚", 3); DecThread dct = new DecThread("小和尚", 3); Console.ReadKey(); } } class SharedRes { public static int count = 0; //初始化互斥锁,没被获取 public static Mutex mtx = new Mutex(); ////初始化互斥锁,被主调线程获取 //public static Mutex mtx = new Mutex(true); } class IncThread { int num; public Thread thrd; public IncThread(string name ,int n) { thrd = new Thread(new ThreadStart(this.run)); thrd.Name = name; num = n; thrd.Start(); } //写日记,过程 void run() { Console.WriteLine(thrd.Name + " , 等待互斥锁 。"); SharedRes.mtx.WaitOne(); Console.WriteLine(thrd.Name + " ,获得互斥锁 。"); do { Thread.Sleep(500); SharedRes.count++; Console.WriteLine("今天我 " + thrd.Name + " 比较强,这样写吧 :" + SharedRes.count); num--; }while(num>0); Console.WriteLine(thrd.Name + " , 释放互斥锁 。"); SharedRes.mtx.ReleaseMutex(); } } class DecThread { int num; public Thread thrd; public DecThread(string name, int n) { thrd = new Thread(new ThreadStart(this.run)); thrd.Name = name; num = n; thrd.Start(); } //写日记,过程 void run() { Console.WriteLine(thrd.Name + ", 等待互斥锁 。"); SharedRes.mtx.WaitOne(); Console.WriteLine(thrd.Name + " ,获得互斥锁 。"); do { Thread.Sleep(500); SharedRes.count--; Console.WriteLine("今天我 " + thrd.Name + " 比较衰,这样写吧 :" + SharedRes.count); num--; } while (num > 0); Console.WriteLine(thrd.Name + " , 释放互斥锁 。"); SharedRes.mtx.ReleaseMutex(); } }
(六)信号量semaphore
类似于互斥锁,只不过他可以指定多个线程来访问,共享资源。在初始化信号量的同时,指定多少个线程可以访问,假如允许2个线程访问,而却有3个线程等待访问,那么他将只允许2个访问,一旦已访问的2个线程中有一个访问完成释放信号量,那么没有访问的线程立马可以进入访问。
案例:和尚抓鸡,3个和尚抓鸡,只有两只鸡,那么鸡圈管理员只允许2个和尚先进,抓到说三句话放下,出来,让第三个和尚进去抓。
分析:三个线程,初始信号量允许2个线程访问。
public class 信号量semaphore { static void Main(string[] args) { MyThread td1 = new MyThread("降龙"); MyThread td2 = new MyThread("伏虎"); MyThread td3 = new MyThread("如来"); td1.td.Start(); td2.td.Start(); td3.td.Start(); Console.ReadKey(); } } public class MyThread { //初始化,新号量,允许2个线程访问,最大2也是2个。 public static Semaphore sem = new Semaphore(2,2); public Thread td; public MyThread(string name) { td = new Thread(new ThreadStart(this.Run)); td.Name = name; } //过程很美好 public void Run() { Console.WriteLine(td.Name+",等待一个信号量。"); sem.WaitOne(); Console.WriteLine(td.Name+",已经获得新号量。"); Thread.Sleep(500); //很有深意的三句话 char[] cr = { 'a','o','e'}; foreach (char v in cr) { Console.WriteLine(td.Name+",输出v: "+v); Thread.Sleep(300); } Console.WriteLine(td.Name+",释放新号量。"); sem.Release(); } }