(一)
基本概念了解
写在最前面
经常看到一些招聘上要求应聘者必须熟练掌握多线程,而对于我这个着实的菜鸟来说多线程这个概念虽然经常听到,但是在实际操作中用的比较少,而且了解的也比较浅显,所以乘着这休息的几天天好好的学习学习,补上这方面的知识。
PS:本文只是多线程这方面学习的记录,并不是对多线程的深入探讨,如果你和我一样对多线程不甚了解,可以和我一起共同学习噢。也欢迎各位大神批评指正,小弟不胜感激!
相关概念学习了解
对于概念这个东东,我历来都不喜欢记,也老是记不住,我更愿意结合概念和实际操作来让它给我留下印象。但是学习前我们还是来看看几个概念,让它首先在我们脑海留下点影子再说。
进程:当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源。而一个进程又是由多个线程所组成的。
线程:线程,有时被称为轻量级进程,是程序执行流的最小单元。
多线程:线程是程序中一个单一的顺序控制流程。在单个程序中同时运行多个线程完成不同的工作,称为多线程。
多线程的利
多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率。线程是在同一时间需要完成多项任务的时候实现的。使用线程可以把耗时比较长的任务放到后台单独开一个线程,使程序运行得更快。同时使用多线程可以开发出更人性化的界面,例如当我们提交某项数据的时候通过使用多线程显示处理进度等效果。
最简单的比喻多线程就像一个工厂的工人,而进程则是工厂的某个车间。工人离开车间就无法生产,同理车间也不可能只有一个员工。多线程的出现就是为了提高效率。
多线程的弊
更过的线程意味着更多的内存消耗;线程的退出可能会对程序带来麻烦;处理不当造成更多的死锁;过多的线程会影响性能(因为操作系统需要在各个线程间切换)
关于其利与弊
既然多线程有利又有弊,那么我们就该扬长避短,发挥它的好处,避开它的不利,在以后的学习过程中慢慢总结,定会弄清楚滴!
来看第一个多线程例子
我们写多线程的代码时一定要引入using System.Threading命名空间哦!
先来看个多线程的简单例子吧,看看是个啥回事儿,具体深入的东东小弟在后面的博客再探讨
using System;
using System.Threading;
namespace ConsoleApplication14
{
class Program
{
[STAThread]
static void Main(string[] args)
{
Console.WriteLine("测试我的线程开始");
Thread test = new Thread(new ThreadStart(myThread));
test.Start();
Thread.CurrentThread.Name = "断桥残雪";
Console.WriteLine(Thread.CurrentThread.Name + Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("测试我的线程结束");
Console.ReadLine();
}
public static void myThread()
{
for (int i = 0; i < 100; i++)
{
Console.WriteLine(i);
}
}
}
}
|
输出如下:
接下来在Console.WriteLine("测试我的线程结束");前加一行代码后:
1 Thread.Sleep(1); 2 Console.WriteLine("测试我的线程结束"); |
运行截图如下:
看了写的代码和运行结果是不是有点奇怪额,为啥我最后的一行代码Console.WriteLine("测试我的线程结束");提前执行了呢?说句实话哈我现在也不能解释得特别清楚,在后面的学习后我相信我可以回来解释的。<此处留下一个大问号?>还有在代码中使用了一个线程的属性ManagedThreadId,在后面的学习中我会介绍更多这些相关属性和方法,也希望各位大神看到不对的尽管拍砖。
(二) 了解线程的属性及方法
写在前面的话
前一篇随笔我们主要介绍了了多线程相关的概念、多线程的其利与弊以及一个基本的多线程例子。那些都算是些常识性的东西,接下来介绍介绍线程相关的属性和方法吧!其实这篇随笔貌似可有可无,因为MSDN确实是个好东西,它告诉我们的着实太多了,其实这篇随笔主要就是参考MSDN然后进行代码实践而已。如果你觉得这些东西觉得木有必要,认为查MSDN就可以那就直接进入第三节吧(7.13更新),从第三节开始小弟就开始实践操作多线程,感兴趣的进噢!
关于Thread的属性
首先我先来一张图吧,此图来自MSDN,介绍的是Thread所有属性:
接下来我写一个简单例子来看看(以下实践和总结全为本人亲自操作尝试,如有疑问可以和我探讨噢):
1 Thread test = new Thread(new ThreadStart(myThread));
2 test.Start();
3 var x = test.CurrentCulture; //此处声明变量使用的var,貌似不合理
|
1.ApartmentState 这个属性在MSDN上显示的为已过时,它是一个枚举变量(STA、MTA、Unknown),其中STA表示Thread将被创建并进入一个单线程单元,MTA表示Thread将被创建并进入一个多线程单元,Unknown表示没有设置线程的单元状态,默认状态为MTA。
2.CurrentContext 表示获取上下文信息(指线程和进程的执行环境,因此包括虚拟内存的设置<告诉我们那个物理内存页面对应那个虚拟内存地址>,句柄转换<因为句柄是基于进程的>,分派器信息,堆栈,以及通用和浮点寄存器的设置)。此句copy过来的额,不甚懂、、、
3.CurrentCulture 我觉得这个蛮有意思的,它用来获取或设置当前线程的区域性。给出上例运行调试的该属性
得到的信息有好多,可以识别地区语言、文本信息、Name等等,那个2052指中文(中国),1033指英语(英国),1042指韩语
。
4.CurrentUICulture 这个属性貌似和CurrentCulture
一样的,用的少忽略之。
5.IsAlive 用于获取一个值,该值指示当前线程的执行状态。当线程处于活动状态为true,否则为false。
6.IsBackground 这个属性挺重要的,获取或设置一个值,该值指示某个线程是否为后台线程。以前用过有印象
7.IsThreadPoolThread 获取一个值,该值指示线程是否属于托管线程池。如果此线程属于托管线程池,则为
true;否则为 false。
8.ManagedThreadId 获取当前托管线程的唯一标识符。其值是一个整数,表示此托管线程的唯一标识符。话说这个东东经常在变,同一个程序几次运行结果不一样哦。
9.Name 获取或设置线程的名称。它包含线程名称的字符串,或者如果未设置名称,则为null。
关于Thread的方法
其实Thread有很多方法,这里列举几个我有实践的方法吧。
1.对于Abort() 这个大家肯定熟悉,用于印发一个异常然后终止线程的运行
2.GetApartmentState() 返回指示单元状态,默认值为MTA
3.GetDomain() 返回当前线程正在其中运行的当前域。不过我试了在VS2010中木有这个方法啊,大神求解
4.GetHashCode() 返回唯一的应用程序域标识符。不过不知道这个标示符怎么来的额,我运行刚才那个,一般总是10或者11
5.Join() 在继续执行标准的 COM 和 SendMessage
消息泵处理期间,阻塞调用线程,直到某个线程终止为止。
6.Sleep(Int32) 这个很简单,就是将当前线程暂停指定的时间(毫秒)
7.Start() 开启某个线
(三) 线程简单基本操作
前传
这几天来到上海,身心疲惫,而且烧钱一大把,吃饭、住房、生活用品......,我那个心痛,昨天终于安顿下来,今天是到公司报道的第一天,没做什么,一直在会议室,到了半下午的时候发给我们入职信息表,说合同现在在总部需要寄过来填写等等,然后这一天就这么过去了。还有且说今天来的实习生10个就有8个本科生,压力山大、、、
好吧,废话少说,听多了影响心情与激情,哈哈哈哈哈
简单的线程
顺便说一句,小弟是学习.net的,所以对C#比较了解些,所以后面的介绍皆以C#来举例
Main函数
我们都知道,在任何一个程序中一般都有一个主函数(在C中是main,在C#中是Main),这就是熟称的程序入口,所有线程都依附于主函数Main(),起始线程也可以称之为主线程。如果所有的前台线程都停止,主线程就可终止,同时后台线程将无条件终止。
class Program
{
static void Main(string[] args)
{
//Console.WriteLine("测试我的线程开始");
Thread test = new Thread(new ThreadStart(myThread));
test.IsBackground = true;
test.Start();
Console.WriteLine("结束");
Console.ReadKey();
}
public static void myThread()
{
Console.WriteLine("我的线程1");
Console.ReadKey(); //加这一句和不加这一句区别
Thread.Sleep(5000); //暂停5秒
Console.WriteLine("我的线程2");
}
}
|
执行效果(左图加Console.ReadKey(),右图不加):
PS:后台线程必须受制于前台线程,当主线程结束所有后台线程必须结束,例如我们聊天软件,我们关闭聊天软件那么软件就木有检查谁谁上线下线了。通常,后台线程非常适合于完成后台任务,应该将被动侦听活动的线程设置为后台线程,而将负责发送数据的线程设置为前台线程,这样,在所有的数据发送完毕之前该线程不会被终止。
关于优先级
是不是听着特别熟悉,对操作系统有了解的园友对这个肯定木有问题,所谓优先级就是在windows上执行的线程在执行了一定时间(一个时间片)后,windows将会进行“调度”,给线程指定优先级,可以影响这个调度。例如现在有一系列的线程,他们的优先级是1,2,4,6,7,那么这个时候操作系统就会优先调用优先级为7的线程,以保证重要的作业可以优先执行(这种调度有时不好,可能导致优先级低的永远都执行不了),看一下C#代码:
class Program
{
static void Main(string[] args)
{
Console.WriteLine("测试我的线程开始");
Thread test1 = new Thread(new ThreadStart(myThread1));
test1.Priority = ThreadPriority.Lowest;
test1.Start();
Thread test2 = new Thread(new ThreadStart(myThread2));
test1.Priority = ThreadPriority.Highest;
test2.Start();
Thread test3 = new Thread(new ThreadStart(myThread3));
test1.Priority = ThreadPriority.Normal;
test3.Start();
Console.WriteLine("结束");
Console.ReadKey();
}
public static void myThread1()
{
Console.WriteLine("我的线程1");
}
public static void myThread2()
{
Console.WriteLine("我的线程2");
}
public static void myThread3()
{
Console.WriteLine("我的线程3");
}
}
|
看运行结果后总结:
嘿嘿,看到木有,在我的代码中,1,2,3本来是依次调用,但是执行的时候出现了变化,原因是我设置了线程的优先级
Priority是Thread类的属性,主要是影响线程的优先级,提示一个枚举类型的值,优先级排列如下:Highest
> AboveNormal > Normal > BelowNormal > Lowest,不过建议不要随便设置优先级,不然容易造成死锁哦。
(四) 锁
写在前面
在进入锁的学习前来看看Thread的方法,之前一直对这个方法不了解,今天学习了下。在学习之前看两段代码吧:
static void Main(string[] args)
{
Thread thread = new Thread(new ThreadStart(myThread1));
thread.Start();
thread.Join(); //关键这一行
Console.WriteLine("主线程");
Console.ReadKey();
}
public static void myThread1()
{
Thread.Sleep(1000);
Console.WriteLine("1测试线程{0}",++count1);
}
|
先来看看效果再说话(左边截图为5行未被注释,右边为被注释):
在这之前,小弟一直不明白为什么加了上面第5行与不加第五行区别是什么,今天终于知道了,原来是线程之间原本并行执行通过使用Join()使其串行化,在这个例子里myThread1()被调用,而此方法存在一个线程阻塞,此时先打印“主线程”(上
右图);然而调用了Join()方法,使其原本并行化的线程串行化,所以主线程必须等待子线程执行完才能执行,此时先打印“1测试线程1”(上
左图)。<以上的串行与并行用词有点不严谨,主要事为了方便理解而已>
进入锁的学习
此句网上COPY来的:我们抛开.NET环境看线程同步,无非是执行两种操作:一是互斥/加锁,目的是保证临界区代码操作的“原子性”;另一种是信号灯操作,目的是保证多个线程按照一定顺序执行,如生产者线程要先于消费者线程执行。
暂时先脑海中留点锁的印象就好啦!下面介绍两个类:
Monitor类
通过查MSDN我们可以发现Monitor类一共有17个方法,但是这17个方法并不是都常用,下面我简单列举几个介绍并结合实例理解理解:
一、Enter()与Exit()
在我看来它目前最主要的功能是设置边界,使原本并行执行的线程顺序化,此句小弟断章取义,不对请指正
static void Main(string[] args)
{
for (int i = 0; i < 15; i++)
{
Thread thread = new Thread(new ThreadStart(myThread1));
thread.Start();
}
Console.WriteLine("主线程");
Console.ReadKey();
}
static object ob = new object();
static int count1 = 0;
public static void myThread1()
{
Monitor.Enter(ob); //作用域开始
Thread.Sleep(10);
Console.WriteLine("1测试线程{0}", ++count1);
Monitor.Exit(ob); //作用域结束
}
|
看看效果再说话(左边截图为使用Enter()和Exit(),右边木有用):
看到区别木有,话说程序员都是聪明的班子,哈哈哈、、、、
二、Wait()与Pulse()
我们先来看看MSDN的官方介绍
Wait()——释放对象上的锁并阻止当前线程,直到它重新获取该锁
Pulse()——通知等待队列中的线程锁定对象状态的更改
简而言之,Wait()方法就是暂时释放资源锁,线程进入等待队列,此时其它线程获取资源锁;Pulse()方法则是唤醒等待队列中的线程,重新得到资源锁。
static void Main(string[] args)
{
for (int i = 0; i < 5; i++)
{
Thread thread = new Thread(new ThreadStart(myThread1));
thread.Start();
Thread thread1 = new Thread(new ThreadStart(myThread2));
thread1.Start();
}
Console.WriteLine("主线程");
}
static object ob = new object();
static int count1 = 0;
static int count2 = 0;
public static void myThread1()
{
Monitor.Enter(ob);
Thread.Sleep(10);
Console.WriteLine("1测试线程{0}", ++count1);
Monitor.Wait(ob);
Console.WriteLine("wait");
Monitor.Pulse(ob);
Monitor.Exit(ob);
}
public static void myThread2()
{
Monitor.Enter(ob);
Thread.Sleep(10);
Console.WriteLine("2测试线程{0}", ++count2);
Monitor.Wait(ob);
Console.WriteLine("wait2");
Monitor.Pulse(ob);
Monitor.Exit(ob);
}
|
运行结果如下图:
上面打印交替次数是没有规律的,每次都会有偏差
再总结几点:
1.Monitor.Pulse()调用后线程还是会执行下一行代码,不会执行另一个线程,除非再调用Monitor.Wait()让线程进入等待状态
2.只有锁的当前所有者可以使用 Pulse 向等待对象发出信号
3.当前拥有指定对象上的锁的线程调用此方法以便向队列中的下一个线程发出锁的信号。
接收到脉冲后,等待线程就被移动到就绪队列中。 在调用 Pulse 的线程释放锁后,就绪队列中的下一个线程(不一定是接收到脉冲的线程)将获得该锁(此句来自MSDN)
ReaderWriterLock类
说句实话,这个类我还真不太熟悉,简单介绍我知道的吧!
我们都知道我们日常代码中,绝大部分都是读取,少部分为写入,而我们前面学习的Monitor类在这里就有些不适合了,因此就出现了ReaderWriterLock这个类,它的好处就是起到,并实现了”写入串行“,”读取并行“的神奇效果。
不过这个类还不慎了解,此处留个坑,待填、、、
(五) 线程池
必须得说点什么
今天在介绍线程池之前,我得来说说上上篇随笔。关于线程的优先级,我们知道在C#中可以设置线程的优先级,使重要的作业可以优先执行,但是这个优先级不是一成不变的,也就是说就算你设置一个线程的优先级非常高,但是也有可能在优先级较低的线程后执行。优先级高不代表就得到了绝对的通行证,给一个例子大家看看:
class Program
{
static void Main(string[] args)
{
Thread test1 = new Thread(new ThreadStart(myThread1));
test1.Priority = ThreadPriority.Lowest;
Thread test2 = new Thread(new ThreadStart(myThread2));
test2.Priority = ThreadPriority.Highest;
Thread test3 = new Thread(new ThreadStart(myThread3));
test3.Priority = ThreadPriority.Normal;
test1.Start();
test2.Start();
test3.Start();
Console.WriteLine("结束");
Console.ReadKey();
}
public static void myThread1()
{
Console.WriteLine("我的线程1");
}
public static void myThread2()
{
Thread.Sleep(5000);
Console.WriteLine("我的线程2");
}
public static void myThread3()
{
Thread.Sleep(3000);
Console.WriteLine("我的线程3");
}
}
|
在上面的例子中我们为线程1设置了最低优先级,线程2设置最高优先级,线程3设置中等优先级,然后在在后面三个方法中分别调用Sleep()方法,使其阻塞,最终我们得到的结果如下所示:
结束
我的线程1
我的线程3
我的线程2
看了这个是不是有点感觉了呢?其实在C#中多线程是个挺怪异的东东,它收到多方面因数的影响,从而导致优先级的执行扑朔迷离,每次执行都可能不同。
线程池
其实我看到线程池这个东西,天生就有种恐惧感,总觉得它很神秘,属于深奥的东西,今天小弟还真得探它一探、、、
关于线程池的概念,其实线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程哦,每个线程池都使用默认的堆栈大小,以默认的优先级执行,并处理多线程单元中。
而且我们都知道一个应用程序可以包含多个线程,这么多的线程管理起来很费力,于是线程池的出现可以帮我们解决相关问题。但是有几点我们需要知道哦,线程池这个东东哇也有不好的地方,有些地方还真使用它不合适,这点后面道来。
ThreadPool类
下面来看看MSDN吧,我发现我越来越爱它了,Wonderful
ThreadPool的概念:提供一个线程池,该线程池可用于发送工作项、处理异步
I/O、代表其他线程等待以及处理计时器。
关于ThreadPool 它有相关的成员
以上线程的成员我们可以没事儿玩一哈:
int x, y,m,n;
ThreadPool.GetMaxThreads(out x, out y); //检索可以同时处于活动状态的线程池请求的数目。
所有大于此数目的请求将保持排队状态,直到线程池线程变为可用。
Console.WriteLine("第一次调用GetMaxThreads" + x + "----" + y); //x指最大线程数,y指异步IO最大线程数
ThreadPool.GetMinThreads(out m, out n); //线程池在新请求预测中维护的空闲线程数
Console.WriteLine("第一次调用GetMinThreads"+m + "----" + m); //m指最小线程数,n指异步IO最小线程数
ThreadPool.SetMaxThreads(100,1000);
ThreadPool.GetMaxThreads(out x, out y); //检索可以同时处于活动状态的线程池请求的数目。
所有大于此数目的请求将保持排队状态,直到线程池线程变为可用。
Console.WriteLine("第二次调用GetMaxThreads" + x + "----" + y); //x指最大线程数,y指异步IO最大线程数
Console.WriteLine("结束");
Console.ReadKey();
|
其实这些东西蛮有意思的,先看看以上代码执行结果:
这段代码执行是没有任何问题的,可是我们改两个地方,如下所示:
1 ThreadPool.SetMaxThreads(100000,1000); 2 ThreadPool.SetMinThreads(3,1000); |
大家猜执行的结果是啥?呵呵试试就知道了,你猜的结果很可能都错了,当我们设置最小线程数大于1023时我们再调用GetMaxThreads方法,我们会发现无论如何最大线程数都是1023。为什么会这样?我们都知道线程有利也有弊,弊端在这里就有所体现,过多的线程导致线程间调度过度频繁,导致线程执行的时间比调度的时间还短,同时又占据更多的内存,所以在这里线程池不允许。
结合上面的成员,练习练习,木有错的、、、、
|