多线程(英语:multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理(Chip-level multithreading)或同时多线程(Simultaneous multithreading)处理器。
线程是程序中一个单一的顺序控制流程.在单个程序中同时运行多个线程完成不同的工作,称为多线程。有时我们为了提高效率也会在实现代码过程中采用多线程,从而达到同时运行多件事情。下面我们看看多线程的优缺点:
多线程优点:
(1)多线程技术使程序的响应速度更快 ,因为用户界面可以在进行其它工作的同时一直处于活动状态;
(2)多线程可以提高CPU的利用率,因为当一个线程处于等待状态的时候,CPU会去执行另外的线程;
(3)占用大量处理时间的任务可以定期将处理器时间让给其它任务;
(4)可以随时停止任务;
(5)可以分别设置各个任务的优先级以优化性能。
多线程缺点:
(1)等候使用共享资源时造成程序的运行速度变慢。这些共享资源主要是独占性的资源 ,如写文件等。
(2)对线程进行管理要求额外的 CPU开销。线程的使用会给系统带来上下文切换的额外负担。当这种负担超过一定程度时,多线程的特点主要表现在其缺点上,比如用独立的线程来更新数组内每个元素。
(3)线程的死锁。即较长时间的等待或资源竞争以及死锁等多线程症状。
(4)对公有变量的同时读或写。当多个线程需要对公有变量进行写操作时,后一个线程往往会修改掉前一个线程存放的数据,从而使前一个线程的参数被修改;另外 ,当公用变量的读写操作是非原子性时,在不同的机器上,中断时间的不确定性,会导致数据在一个线程内的操作产生错误,从而产生莫名其妙的错误,而这种错误是程序员无法预知的。
C#中线程分为前台线程和后台线程:线程创建时不做设置默认是前台线程。即线程属性
IsBackground=false。
Thread.IsBackground = false;//false:设置为前台线程,系统默认为前台线程。
前台线程和后台线程区别:应用程序必须运行完所有的前台线程才可以退出,只要有一个前台线程未退出,进程就不会终止!即说的就是程序不会关闭!(即在资源管理器中可以看到进程未结束);而对于后台线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束。
线程是寄托在进程上的,进程都结束了,线程也就不复存在了!
C#自带线程池
采用多线程中最首先得一个问题就是线程的管理。C#中通过Threadpool类来提供一个有系统维护的线程池。在使用同时,我们需要用ThreadPool.QueueUserWorkItem() 将线程添加到线程池中。它的函数原型如下:
// 将一个线程放进线程池,该线程的 Start() 方法将调用 WaitCallback 代理对象代表的函数
public static bool QueueUserWorkItem(WaitCallback);
// 重载的方法如下,参数 object 将传递给 WaitCallback 所代表的方法
public static bool QueueUserWorkItem(WaitCallback, object);
【注意】因为ThreadPool 类是一个静态类,所以生成它的对象。在整个过程中无需自己建立线程,只需把要做的工作写成函数,然后作为参数传递给ThreadPool.QueueUserWorkItem()方法就行了,传递的方法就是依靠 WaitCallback 代理对象,而线程的建立、管理、运行等工作都是由系统自动完成的,你无须考虑那些复杂的细节问题。
ThreadPool 的用法:
首先程序创建了一个 ManualResetEvent 对象,该对象就像一个信号灯,可以利用它的信号来通知其它线程。本例中,当线程池中所有线程工作都完成以后,ManualResetEvent 对象将被设置为有信号,从而通知主线程继续运行。ManualResetEvent 对象有几个重要的方法:
初始化该对象时,用户可以指定其默认的状态(有信号/无信号);在初始化以后,该对象将保持原来的状态不变,直到它的 Reset() 或者 Set() 方法被调用:
Reset(): 将其设置为无信号状态;
Set(): 将其设置为有信号状态。
WaitOne(): 使当前线程挂起,直到 ManualResetEvent 对象处于有信号状态,此时该线程将被激活。然后,程序将向线程池中添加工作项,这些以函数形式提供的工作项被系统用来初始化自动建立的线程。当所有的线程都运行完了以后,ManualResetEvent.Set() 方法被调用,因为调用了WaitOne() 方法而处在等待状态的主线程将接收到这个信号,于是它接着往下执行,完成后边的工作。
简单线程池的实现:
在实现一个线程池时,我大概将它分为下面及部分:
1、线程管理器(ThreadManager):用于管理线程池(开启线程的个数,所有线程的状态)
2、工作线程(WorkThread): 线程池中单一的线程,管理线程的运行
3、任务类(Task): 用来管理所有的任务【我是以一个队列管理的】,因为每一个任务中有它的一些信息,所以为了方便,我将任务封装成了一个类。
ThreadManager类:
变量:
DefaultThreadNum:默认最大开启线程数。每次开启的线程数以用户传入线程数、任务数以及默认线程数中最小的数值为准,并将DefaultThreadNum设为该值(防止线程过多使效率降低)
TaskQueue:是一个用来存放需要处理任务的队列,每个元素为Task类型(后面介绍)
WorkThreadList:用来存放所有工作线程的链表,每个元素为WorkThread类型(后面介绍)
方法:
LazyInitializer():初始化ThreadManager中的变量
CreatThreadPool():创建一个线程池(线程个数为DefaultThreadNum)
CloseThread():关闭WorkThreadList中所有线程(前提是TaskQueue中的任务处理完成并且该线程当前处理的任务结束),否则,该线程不关闭
ThreadIsAllClosed():WorkThreadList中的线程是否全部关闭。
Workthread类:
变量:
flag:作为该线程是否还要继续获取任务的标志,为false该线程则执行万当前任务后挂起,不在从TaskQueue获取任务,否则只要TaskQueue不为空,则一直获取任务并处理
ThreadsEnd:用来标记该线程是否该被挂起的标志(TaskQueue为空,并且当前处理的任务结束)
thread:当前WorkThread对象中的线程
task:当前WorkThread对象中正在处理的任务
TaskQueue:与ThreadManager类中相同,用来存放需要处理任务的队列,每个元素为Task类型(后面介绍)
方法:
WorkThread():初始化上面的变量
GetThreadEnd():获取现在Thread的状态,是否为结束状态
ThreadRun():线程获取任务并执行任务函数【在实现该函数是,因为可能会多个线程在从TaskQueue中获取任务,所以在操作TaskQueue时要注意上锁】
CloseThread():关闭当前WorkThread实例中的线程
Task类:
该类中是用来存放每个任务中的一些信息。比如我的每个任务中有一个CaseName字符串标识该任务的名称,还有一个CaseLevel字符串标识该任务的重要性。所以,该类中包含下列信息:
变量:
CurrentCaseName:当前任务的名字
CurrentCaseLevel:当前任务的重要等级
方法:
GetCurrentCaseName():获取当前任务的名字
GetCurrentCaseLevel():获取当前任务的重要等级。
在多线程中因为会有共享资源的操作,所以资源上锁是很常见的一种解决方式,但这样也出现了一个问题,就是线程死锁问题。那么线程为什么会产生线程死锁呢?
线程死锁问题:
线程死锁产生原因:
1)互斥条件:指线程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个线程占用。如果此时还有其它线程请求资源,则请求者只能等待,直至占有资源的线程用毕释放。
情景:在函数返回时忘了释放所:
void test()
{
EnterCriticalSection();
if(....) //满足if中的条件后直接return,这样永远不会执行释放锁语句,其他线程无法得到资源,就会形成死锁
{
return;
}
LeaveCriticalSection();
}
2)请求和保持条件:指线程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它线程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
void test1()
{
EnterCriticalSection(&cs1); // 得到了资源1,一直在等待资源2
EnterCriticalSection(&cs2);
do_something1();
LeaveCriticalSection(&cs2);
LeaveCriticalSection(&cs1);
}
void test2()
{
EnterCriticalSection(&cs2); // 得到了资源2,一直在等待资源1
EnterCriticalSection(&cs1);
do_something2();
LeaveCriticalSection(&cs1);
LeaveCriticalSection(&cs2);
}
3)不剥夺条件:指线程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
上述的例子一样,如果资源1和资源2都是可剥夺资源时,可能会因为优先级某一个线程争夺到另一个线程占有的资源,从而开始运行;但是当两个资源都是不可剥夺时,谁也无法得到对方占有的资源,那么就会一直等待资源,形成死锁。
4)环路等待条件:指在发生死锁时,必然存在一个线程——资源的环形链,即线程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
原理和(2)中一样,只是2中时两个线程在互相等待资源,而该情况下时多个线程唤醒等待。比如:A有资源1,还需要资源2;B有资源2,还需要资源3;C有资源3,还需要资源4;D有资源4,还需要资源1。这样所有线程都会一直处于等待状态,导致资源死锁
/* 多个线程申请锁的顺序形成相互依赖的环形:
* A - B
* | |
* C - D
*/
防止死锁的方法:
1.保证单线程下程序正确,然后再移植到多线程。
2.时刻检查自己写的程序有没有在跳出时忘记释放锁。
3.如果自己的模块可能重复使用一个锁,建议使用嵌套锁。
4.对于某些锁代码,不要临时重新编写,建议使用库里面的锁,或者自己曾经编写的锁。
5.如果某项业务需要获取多个锁,必须保证锁的按某种顺序获取,否则必定死锁。
6.编写简单的测试用例,验证有没有死锁。
7.编写验证死锁的程序,从源头避免死锁。