C#中的多线程与线程死锁

多线程(英语:multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理(Chip-level multithreading)或同时多线程(Simultaneous multithreading)处理器。

线程是程序中一个单一的顺序控制流程.在单个程序中同时运行多个线程完成不同的工作,称为多线程。有时我们为了提高效率也会在实现代码过程中采用多线程,从而达到同时运行多件事情。下面我们看看多线程的优缺点:

多线程优点:
(1)多线程技术使程序的响应速度更快 ,因为用户界面可以在进行其它工作的同时一直处于活动状态;
(2)多线程可以提高CPU的利用率,因为当一个线程处于等待状态的时候,CPU会去执行另外的线程;
(3)占用大量处理时间的任务可以定期将处理器时间让给其它任务;
(4)可以随时停止任务;
(5)可以分别设置各个任务的优先级以优化性能。

多线程缺点:
(1)等候使用共享资源时造成程序的运行速度变慢。这些共享资源主要是独占性的资源 ,如写文件等。
(2)对线程进行管理要求额外的 CPU开销。线程的使用会给系统带来上下文切换的额外负担。当这种负担超过一定程度时,多线程的特点主要表现在其缺点上,比如用独立的线程来更新数组内每个元素。
(3)线程的死锁。即较长时间的等待或资源竞争以及死锁等多线程症状。
(4)对公有变量的同时读或写。当多个线程需要对公有变量进行写操作时,后一个线程往往会修改掉前一个线程存放的数据,从而使前一个线程的参数被修改;另外 ,当公用变量的读写操作是非原子性时,在不同的机器上,中断时间的不确定性,会导致数据在一个线程内的操作产生错误,从而产生莫名其妙的错误,而这种错误是程序员无法预知的。

C#中线程分为前台线程和后台线程:线程创建时不做设置默认是前台线程。即线程属性

IsBackground=falseThread.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.编写验证死锁的程序,从源头避免死锁。

  • 2
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1.几种同步方法的区别 lock和Monitor是.NET用一个特殊结构实现的,Monitor对象是完全托管的、完全可移植的,并且在操作系统资源要求方 面可能更为有效,同步速度较快,但不能跨进程同步。lock(Monitor.Enter和Monitor.Exit方法的封装),主要作用是锁定临界区,使临 界区代码只能被获得锁的线程执行。Monitor.Wait和Monitor.Pulse用于线程同步,类似信号操作,个人感觉使用比较复杂,容易造成死 锁。 互斥体Mutex和事件对象EventWaitHandler属于内核对象,利用内核对象进行线程同步,线程必须要在用户模式和内核模 式间切换,所以一般效率很低,但利用互斥对象和事件对象这样的内核对象,可以在多个进程的各个线程间进行同步。 互斥体Mutex类似于一个接力棒,拿到接力棒的线程才可以开始跑,当然接力棒一次只属于一个线程(Thread Affinity),如果这个线程不释放接力棒(Mutex.ReleaseMutex),那么没办法,其他所有需要接力棒运行的线程都知道能等着看热 闹。 EventWaitHandle 类允许线程通过发信号互相通信。 通常,一个或多个线程在 EventWaitHandle 上阻止,直到一个未阻止的线程调用 Set 方法,以释放一个或多个被阻止的线程。 2.什么时候需要锁定 首先要理解锁定是解决竞争条件的,也就是多个线程同时访问某个资源,造成意想不到的结果。比如,最简单的情况是,一个计数器,两个线程 同时加一,后果就是损失了一个计数,但相当频繁的锁定又可能带来性能上的消耗,还有最可怕的情况死锁。那么什么情况下我们需要使用锁,什么情况下不需要 呢? 1)只有共享资源才需要锁定 只有可以被多线程访问的共享资源才需要考虑锁定,比如静态变量,再比如某些缓存的值,而属于线程内部的变量不需要锁定。 2)多使用lock,少用Mutex 如果你一定要使用锁定,请尽量不要使用内核模块的锁定机制,比如.NET的Mutex,Semaphore,AutoResetEvent和 ManuResetEvent,使用这样的机制涉及到了系统在用户模式和内核模式间的切换,性能差很多,但是他们的优点是可以跨进程同步线程,所以应该清 楚的了解到他们的不同和适用范围。 3)了解你的程序是怎么运行的 实际上在web开发大多数逻辑都是在单个线程展开的,一个请求都会在一个单独的线程处理,其的大部分变量都是属于这个线程的,根本没有必要考虑锁 定,当然对于ASP.NET的Application对象的数据,我们就要考虑加锁了。 4)把锁定交给数据库 数 据库除了存储数据之外,还有一个重要的用途就是同步,数据库本身用了一套复杂的机制来保证数据的可靠和一致性,这就为我们节省了很多的精力。保证了数据源 头上的同步,我们多数的精力就可以集在缓存等其他一些资源的同步访问上了。通常,只有涉及到多个线程修改数据库同一条记录时,我们才考虑加锁。 5)业务逻辑对事务和线程安全的要求 这 条是最根本的东西,开发完全线程安全的程序是件很费时费力的事情,在电子商务等涉及金融系统的案例,许多逻辑都必须严格的线程安全,所以我们不得不牺牲 一些性能,和很多的开发时间来做这方面的工作。而一般的应用,许多情况下虽然程序有竞争的危险,我们还是可以不使用锁定,比如有的时候计数器少一多一, 对结果无伤大雅的情况下,我们就可以不用去管它。 3.InterLocked类 Interlocked 类提供了同步对多个线程共享的变量的访问的方法。如果该变量位于共享内存,则不同进程的线程就可以使用该机制。互锁操作是原子的,即整个操作是不能由相 同变量上的另一个互锁操作所断的单元。这在抢先多线程操作系统是很重要的,在这样的操作系统线程可以在从某个内存地址加载值之后但是在有机会更改 和存储该值之前被挂起。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值