`
isiqi
  • 浏览: 16599463 次
  • 性别: Icon_minigender_1
  • 来自: 济南
社区版块
存档分类
最新评论

线程杂谈2

阅读更多

作者:linzhenqun()

时间:<chsdate w:st="on" isrocdate="False" islunardate="False" day="6" month="2" year="2006">2006-2-6</chsdate>

Blog: http://blog.csdn.net/linzhengqun

-----------------------------------------------------------------------------------------------------

前言

上次写了一篇关于线程的文章,其中有介绍当工作线程有资源存在时,主线程如何释放工作线程的方法,后来仔细读了《Win32多线程程序设计》,对于线程的机制有了更进一步的理解,才知道那并非最佳的方法,这里将给出一个更好的方法,这也是我写这篇文章的最大原因。另外,对于同步机制也将作一些探讨。如果想全面了解多线程的,推荐看候捷翻译的《Win32多线程程序设计》。

干净地终止一个线程

回到上一篇的问题描述:如果线程中有一些资源,Execute正在使用这些资源,而主线程要释放这个线程,这个线程在释放的过程中会释放掉资源。想想会不会有问题呢,两个线程,一个在使用资源,一个在释放资源,会出现什么情况呢。具体的情况请看“线程杂谈”那篇文章。总之是出现了违法访问,而我给出的解决方法是使用一个FFinished成员,最后释放的时候循环判断FFinished,这样可以保证Execute执行完毕之前,线程的成员类不会被释放。利用线程类的Terminated属性和FFinished成员,便可在主线程干净结束工作线程。

但这并非最好的解决方法,循环判断FFinished成员即如下:

While not FFinished do

Sleep(100);

这并不能即时释放线程,最慢的情况还要等上0.1秒才能释放。而如果我们使用下面这种形式:

While not FFinished do

;

则是一种典型的Busy loop,主线程不停的使用While循环,非常浪费CPU的资源,这样的效率比直接等待慢一倍以上。

那么有没有一种方法,让主线程通知工作线程可以结束了,然后主线程等待工作线程结束,工作线程结束后才释放资源呢?让主线程通知工作线程结束可以用线程类的Terminated属性,这一点在上一篇文章已有介绍。而主线程等待Execute结束,事实上线程类的Destroy方法已经有这样的机制:

Terminate;

...

WaitFor;

其中的WaitFor方法一直等待直到Execute方法执行完毕(准确的说应该是ThreadProc函数执行完毕)。这些代码在一般的线程类中已经够用了,但有些线程类有自己的类成员,并会覆盖Destroy方法,在其中释放类成员,既然这样,我们可以把上面的代码包成一个方法,并在自己的Destroy中首先调用该方法。这样即可即时干净地释放线程类,比使用循环更好。

下面给出例子:

unitUnit1;

interface

uses
Windows,Messages,SysUtils,Variants,Classes,Graphics,Controls, Forms, Dialogs,StdCtrls;

type
TMyClass=class
private
FTest:Integer;
public
proceduretest;
end;

TMyThread=class(TThread)
private
FMyClass:TMyClass;
protected
(*等待Execute方法执行完毕*)
procedureWaitExecute;
procedureExecute;override;
public
constructorCreate(CreateSuspended:Boolean);
destructorDestroy;override;
end;

TForm1=class(TForm)
Button1:TButton;
Button2:TButton;
procedureButton1Click(Sender:TObject);
procedureButton2Click(Sender:TObject);
private
{Privatedeclarations}
procedureOnTerminate(Sender:TObject);
public
{Publicdeclarations}
MyThread:TMyThread;
end;

var
Form1:TForm1;

implementation

{$R*.dfm}

{TMyThread}

constructorTMyThread.Create(CreateSuspended:Boolean);
begin
inherited;
FMyClass:=TMyClass.Create;
end;

destructorTMyThread.Destroy;
begin
WaitExecute;
FMyClass.Free;
FMyClass:=nil;
inherited;
end;

procedureTMyThread.Execute;
begin
whilenotTerminateddo
FMyClass.test;
end;

procedureTMyThread.WaitExecute;
begin
Terminate;
WaitFor;
end;

{TMyClass}

procedureTMyClass.test;
begin
//Sleep模拟业务操作
Sleep(
100);
FTest:=
0;
end;

procedureTForm1.Button1Click(Sender:TObject);
begin
MyThread:=TMyThread.Create(False);
MyThread.OnTerminate:=OnTerminate;
end;

procedureTForm1.Button2Click(Sender:TObject);
begin
MyThread.Free;
end;

procedureTForm1.OnTerminate(Sender:TObject);
begin
ShowMessage(
'OK');
end;

end.

先点击Button1,然后点击Button2,线程类可以被正确地释放,试着把Destroy方法中的WaitExecute方法注释掉再点击按钮,接着关闭程序看看,这时就出现违法访问了。

同步机制

当有多个线程同时读写一个资源时,使用同步机制可以保护数据的完整性,也可以防止数据被损坏。线程同步有很多种方式,但最常用的还是临界区(Critical Sections),这里只讨论临界区。不过之前,先说明一下同步(Synchronous)和异步(Asynchronous)的概念。当程序1调用程序2时,程序1停止不动,直到程序2完成回到程序1来,程序1才继续下去,这就是同步;如果程序1调用程序2后,自己继续执行下去,那么两者之间就是异步。很明显多线程是一种异步执行方式,Windows系统的SendMessagePostMessage分别是同步和异步方式。

一个TRTLCriticalSection类型的变量代表了一个临界区,引用《Windows高级编程》中的话,临界区就好像一个只能进一人的洗手间,后面的人只能排队等到进去的人出来才能进去。让我讲得详细一点,假设你有一个数据结构A会被线程BC读写,则你可以声明一个TRTLCriticalSection变量CS,接着调用InitializeCriticalSection(CS),这时你拥有了一个临界区,在BC线程对A进行读写的地方,都套上如下的代码:

EnterCriticalSection(CS);

try

//A进行读写

finally

LeaveCriticalSection(CS);

end;

你便能保证同一时间只有一个线程对A进行读写。

它的过程是这样的:

线程BC一直在运行,某一时刻,BC同时要对A进行操作,但事实上是不可能同时的,总有一个线程会快一点,假设是B吧,B线程的第一句便是EnterCriticalSection(CS),使得线程B进入了临界区,接着BA进行操作。而操作系统总是这样频繁的在不同线程之间切换以制造“多任务”的假象,当BA的操作进行到一半时,系统将执行权切换给线程C,我们知道C也要对A进行操作,它的第一句也是EnterCriticalSection(CS),可是进这个“洗手间”时发现里面已经有“人”了,C没办法只能一直停在Enter的地方等候,过很短的时间,执行权交给了线程BB继续对A进行操作,如此反复,B终于操作完了,最后调用LeaveCriticalSection(CS)离开“洗手间”,这时执行权再交到C时,C发现B已经离开临界区,它终于可以进入临界区对于A进行操作了,而此时别的线程要对A进行操作,也只能等C离开“洗手间”。

对于临界区有些地方值得注意:

1. 一个TRTLCriticalSection变量代表一个临界区,如果你使用两个这样的变量,则它们是毫不相干的,就像两个“洗手间”一样,对于同一个资源不能起到同步的作用。

2. 不要针对多个资源只声明一个Critical Section,这样使每一时刻只能对一个资源进行读写,会对程序的效率有很大的影响。为每个需要同步保护的资源声明一个临界区变量。

3. 需要对资源进行同步保护,则所有对资源操作的地方都要有EnterLeave函数,且要有Try..finally结构,保证最后能Leave

4. 在临界区里面不要进行非常费时的操作,更重要的防止死锁的发生。

死锁

线程同步虽好,但也带来一些问题,最典型的就是死锁,死锁通常发生在线程之间相互等待的情况下。拿临界区来说,假设有两个临界区,两个线程分别进入一个临界区,接着各自又要进入另一个临界区,这时就会落入“你等我,我等你”的轮回。下面我制造这种场景给你给看看:

unitUnit1;

interface

uses
Windows,Messages,SysUtils,Variants,Classes,Graphics,Controls,Forms,
Dialogs,StdCtrls;

const
WM_MYMESSAGE=WM_USER+
001;

type
TMyThread1=class(TThread)
protected
procedureExecute;override;
end;

TMyThread2=class(TThread)
protected
procedureExecute;override;
end;

TForm1=class(TForm)
Button1:TButton;
procedureButton1Click(Sender:TObject);
procedureFormCreate(Sender:TObject);
procedureFormClose(Sender:TObject;varAction:TCloseAction);
private
{Privatedeclarations}
public
{Publicdeclarations}
Thd1:TMyThread1;
Thd2:TMyThread2;
end;

var
Form1:TForm1;
CS1,CS2:TRTLCriticalSection;
implementation

{$R*.dfm}

procedureDeadLock(varCSA,CSB:TRTLCriticalSection);
begin
EnterCriticalSection(CSA);
Sleep(
100);
EnterCriticalSection(CSB);
try
Sleep(
1000);
finally
LeaveCriticalSection(CSA);
LeaveCriticalSection(CSB);
end;
end;

{TMyThread}

procedureTMyThread1.Execute;
begin
DeadLock(CS1,CS2);
end;

{TMyThread2}

procedureTMyThread2.Execute;
begin
DeadLock(CS2,CS1);
end;

{TForm1}

procedureTForm1.Button1Click(Sender:TObject);
begin
Thd1.Resume;
Thd2.Resume;
end;

procedureTForm1.FormCreate(Sender:TObject);
begin
InitializeCriticalSection(CS1);
InitializeCriticalSection(CS2);
Thd1:=TMyThread1.Create(True);
Thd2:=TMyThread2.Create(True);
end;

procedureTForm1.FormClose(Sender:TObject;varAction:TCloseAction);
begin
DeleteCriticalSection(CS1);
DeleteCriticalSection(CS2);
Thd1.Free;
Thd2.Free;
end;

end.

运行上面程序,点击按钮1,你的程序没有什么异样,但关闭程序看看,程序关闭不了,因为两个线程落到相互等待的局面,而线程的Free又在等Execute结束,所以程序没有办法关闭了。

让我说说为什么会发生死锁,点击Button1时,两个线程同时启动,TMyThread1执行了DeadLock(CS1,CS2),而TMyThread2执行了DeadLock(CS2,CS1),注意到两个参数倒过来了。

它的过程是这样,首先线程1先执行DeadLock,第一个进入EnterCriticalSection(CSA),注意这里的CSACS1,接下来Sleep(100),在线程1睡觉到一般时,线程2得到执行权也执行DeadLock,它也进入EnterCriticalSection(CSA),这里的CSA却是CS2,接着线程2Sleep(100),也是睡到一半,换到线程1,线程1睡醒后往下执行,它要EnterCSB,对于线程1来说,CSBCS2,但CS2已经被线程2Enter了,所以线程1挂起等待线程2离开CS2临界区;接着线程2醒来要进入EnterCriticalSection(CSB),这里的CSBCS1,但CS1给线程1进入了,所以线程2也挂起等待线程1离开CS1临界区。就这样,两个线程相互等待,没完没了。

这就是死锁发生的情况之一,当多个临界区一起工作时,死锁就有可能发生,这是要特别注意的,在死锁可能发生的地方,尽量只用一个临界区。

后记

关于线程的杂谈就到这里了,这两篇线程的文章更关注于其应用。

如果要学习Delphi中的TThread对于线程机制的封装,推荐给你一篇技术文章:http://www.delphibbs.com/keylife/iblog_show.asp?xid=19903,非常精彩,我在其中也学到很多。

至于要系统学习多线程原理的,首推《Win32多线程程序设计》

另外,《Windows高级编程》对于线程的篇章也非常好。

Bye

分享到:
评论

相关推荐

    多核多线程杂谈-并行计算

    ### 多核多线程杂谈-并行计算 #### 1. 并行计算概述 随着计算机硬件的发展,单核处理器的性能提升遇到了物理瓶颈,因此多核处理器成为了提高计算能力的关键技术之一。并行计算是利用多核处理器或多台计算机协同...

    炉边夜话---多核多线程杂谈

    #### 2. 并发编程时代的重要性 - **免费午餐结束**:随着处理器频率提升的减缓,单核处理器性能增长放缓,这意味着依赖于单一核心性能提升的应用程序无法继续获得以往那样的性能增长。因此,通过编写并发程序来利用...

    多线程编程指南 SUN出品

    《多线程编程指南》是由SUN公司出版的一本深入探讨多线程编程的重要书籍,对于想要提升在并发处理方面技能的程序员来说,这是一份不可多得的学习资源。本书全面讲解了Java语言中的线程相关知识,涵盖了从基本概念到...

    java陷阱--面试(题集)杂谈

    第二十八,计算2乘以8最有效率的方式就是直接写2 * 8。 第二十九,两个对象值相同(x.equals(y) == true),但hash code可以不同,因为不同的对象可能有相同的值,但哈希码不一定相同。 第三十,对象作为参数传递...

    swing开发杂谈--初版本程序源码

    在"swing开发杂谈--初版本程序源码"中,可能包含了上述部分或全部知识点的实际应用,通过分析`netHelper`这个子文件夹,我们可以看到可能的网络辅助类或其他功能模块的实现。这个源码可能会演示如何使用Swing构建一...

    杂谈,一些工具类的集合

    标题和描述中的"杂谈,一些工具类的集合"可能指的是一个涵盖多种工具的资源包,旨在解决日常开发中的各种问题。 首先,我们来探讨一下工具类在编程中的作用。在编程中,工具类通常是一组静态方法的集合,这些方法...

    高并发场景杂谈.zip

    "高并发场景杂谈.zip"这个压缩包文件集成了多种处理高并发问题的策略和技术,旨在为开发者提供解决高并发问题的思路和实践案例。下面将详细讨论其中涉及的知识点。 首先,我们来看"Redis专场:如何利用Redisson...

    Android开发杂谈

    本文将基于“Android开发杂谈”的主题,结合提供的资源——一个名为"Android_.pdf"的文件,来深入探讨一些重要的知识点。 1. **源码阅读**: 在Android开发中,理解源码是提升技能的关键。Android开源项目(AOSP)...

    充分利用CPU多核心并发特性,创建多个pigz线程, 并将pigz的每个线程绑定的固定核心, 实现高性能 压缩和压缩

    linux

    关于Java的几个经典问题

    (五)——传了值还是传了引用(六)——字符串(String)杂谈 (七)——日期和时间的处理 (八)——聊聊基本类型(内置类型)(九)——继承、多态、重载和重写(十)——话说多线程 (十一)——这些运算符你是否...

    Java面试题解惑系列

    2、到底创建了几个String对象;3、变量(属性)的覆盖;4、final,finally,finalize;5.传了值还是传了引用;6.String杂谈;7.日期与时间的处理;8.基本类型总结;9.继承,多态,重载,重写;10.多线程;11.运算符...

    java杂谈-一个计算机专业学生几年的编程经验汇总谈.pdf

    `client`版本通常适用于内存有限的桌面环境,而`server`版本则针对服务器环境,优化了多线程和大内存应用的性能。 理解这些基础知识对于Java程序员来说至关重要,它们不仅能够帮助我们编写更高效的代码,还能让我们...

    JAVA面试题解惑系列合集

    2. 创建的String对象数量:在JAVA中,字符串(String)是不可变的,当进行字符串的拼接或者创建新的字符串时,JVM会检查字符串池中是否存在相同的字符串,如果存在,则直接使用池中的对象,否则会在堆上创建新的对象...

    深入理解jvm视频资源

    2. **内存模型**:JVM内存模型规定了如何在多个线程之间共享数据,包括堆内存、栈内存、方法区、本地方法栈和程序计数器。堆内存是所有线程共享的,用于存储对象实例;栈内存每个线程独有,存储方法调用时的局部变量...

    JAVA面试题解惑系列

    2. **创建String对象的数量**: Java中的String对象是不可变的,因此创建String时需要理解其内存机制。面试中常见的问题是关于字符串连接或复制操作时,实际创建了多少个对象。例如,"abc" + "def" 实际上会产生几...

    臧圩人--JAVA面试题解惑系列合集.pdf

    **1.6 JAVA面试题解惑系列(六)——字符串(String)杂谈** - **知识点**:深入探讨字符串处理技术,包括字符串拼接、比较、格式化等常见操作的内部实现,以及性能优化建议。 **1.7 JAVA面试题解惑系列(七)——...

Global site tag (gtag.js) - Google Analytics