转载自:https://www.cnblogs.com/xumenger/p/4450659.html
当有多个线程的时候,经常需要去同步这些线程以访问同一个数据或资源。
例如,假设有一个程序,其中一个线程用于把文件读到内存,而另一个线程用于统计文件的字符数。当然,在整个文件调入内存之前,统计它的计数是没有意义的。但是,由于每个操作都有自己的线程,操作系统会把两个线程当做是互不相干的任务分别执行,这样就可能在没有把整个文件装入内存时统计字数。为解决此问题,你必须使两个线程同步工作
存在一些线程同步地址的问题,Win 32 提供了许多线程同步的方式。这里将会讲到:临界区、互斥、信号量和事件
为了检验这些技术,首先来看一个需要线程同步解决的问题。假设有一个整数数组,需要按照升序赋初值。现在要在第一遍把这个数组赋初值为1~128,第二遍将此数组赋初值为128~255,然后结果显示在列表中。要用两个线程来分别进行初始化。下面的代码给出了没有做线程同步的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
|
unit Main;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TMainForm = class (TForm)
Button1: TButton;
ListBox1: TListBox;
procedure Button1Click(Sender: TObject);
private
procedure ThreadsDone(Sender: TObject);
end ;
TFooThread = class (TThread)
protected
procedure Execute; override;
end ;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
const
MaxSize = 128 ;
var
NextNumber: Integer = 0 ;
DoneFlags: Integer = 0 ;
GlobalArray: array [ 1.. MaxSize] of Integer ;
function GetNextNumber: Integer ;
begin
Result:= NextNumber;
Inc(NextNumber);
end ;
procedure TFooThread . Execute;
var
i: Integer ;
begin
OnTerminate:= MainForm . ThreadsDone;
for i:= 1 to MazSize do
begin
GlobalArray[i]:= GetNextNumber;
Sleep( 5 );
end ;
end ;
procedure TMainForm . ThreadsDone(Sender: TObject);
var
i: Integer ;
begin
Inc(DoneFlags);
if DoneFlags = 2 then
for i:= 1 to MaxSize do
ListBox1 . Items . Add(IntToStr(GlobalArray[i]));
end ;
procedure TMainForm . Button1Click(Sender: TObject);
begin
TFooThread . Create( False );
TFooThread . Create(Flase);
end ;
end .
|
因为两个线程同时运行,同一个数组在两个线程中被初始化会出现什么呢?你可以看下面的截图
这个问题的解决方案是:当两个线程访问这个全局数组时,为防止它们同时执行,需要使用线程的同步。这样,你就会得到一组合理的数值
1.临界区
临界区是一种最直接的线程同步方法。所谓临界区,就是一次只能有一个线程来执行的一段代码。如果把初始化数组的代码放在临界区内,那么另一个线程在第一个线程处理完之前是不会被执行的。
在使用临界区之前,必须使用 InitializeCriticalSection()过程初始化它,其声明如下
1
|
procedure InitializeCriticalSection( var lpCriticalSection: TRLCriticalSection); stdcall;
|
lpCriticalSection参数是一个TRTLCriticalSection类型的记录,并且是变参。至于TRTLCriticalSection是如何定义的,这并不重要,因为很少需要查看这个记录中的具体内容。只需要在lpCriticalSection中传递为初始化的记录,InitializeCriticalSection()过程就会填充这个记录
注意:Microsoft 故意隐瞒了TRTLCriticalSection 的细节。因为,其内容在不同的硬件平台上是不同的。在基于Intel 的平台上,TRTLCriticalSection 包含一个计数器、一个指示当前线程句柄的域和一个系统事件的句柄。在Alpha 平台上,计数器被替换为一种Alpha-CPU 数据结构,称为spinlock。
在记录被填充之后,我们就可以开始创建临界区了。这是我们需要使用EnterCriticalSection() 和LeaveCriticalSection() 来封装代码块。这两个过程的声明如下
1
2
3
|
procedure EnterCriticalSection( var lpCriticalSection: TRTLCriticalSection); stdcall;
procedure LeaveCriticalSection( var lpCriticalSection: TRTLCriticalSection); stdcall;
|
正如你所想的,参数 lpCriticalSection 就是有InitializeCriticalSection() 填充的记录
当你不需要TRTLCriticalSection 记录时,应当调用 DeleteCriticalSection() 过程,下面是它的声明
1
|
procedure DeleteCriticalSection( var lpCriticalSection: TRTLCriticalSection); stdcall;
|
下面演示利用临界区来同步数组初始化线程的技术
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
unit Main;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TMainForm = class (TForm)
Button1: TButton;
ListBox1: TListBox;
procedure Button1Click(Sender: TObject);
private
procedure ThreadsDone(Sender: TObject);
end ;
TFooThread = class (TThread)
protected
procedure Execute; override;
end ;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
const
MaxSize = 128 ;
var
NextNumber: Integer = 0 ;
DoneFlags: Integer = 0 ;
GlobalArray: array [ 1.. MaxSize] of Integer ;
CS: TRTLCriticalSection;
function GetNextNumber: Integer ;
begin
Result:= NextNumber;
Inc(NextNumber);
end ;
procedure TFooThread . Execute;
var
i: Integer ;
begin
OnTerminate:= MainForm . ThreadsDone;
EnterCriticalSection(CS);
for i:= 1 to MazSize do
begin
GlobalArray[i]:= GetNextNumber;
Sleep( 5 );
end ;
LeaveCriticalSection(CS);
end ;
procedure TMainForm . ThreadsDone(Sender: TObject);
var
i: Integer ;
begin
Inc(DoneFlags);
if DoneFlags = 2 then
for i:= 1 to MaxSize do
ListBox1 . Items . Add(IntToStr(GlobalArray[i]));
DeleteCriticalSection(CS);
end ;
end ;
procedure TMainForm . Button1Click(Sender: TObject);
begin
InitializeCriticalSection(CS);
TFooThread . Create( False );
TFooThread . Create(Flase);
end ;
end .
|
在第一个线程调用EnterCriticalSection()之后,所有别的线程就不能再进入代码块。下一个线程要等到第一个线程调用LeaveCriticalSection()之后才能被唤醒,输出结果显示如下
2.互斥
互斥非常类似于临界区,除了两个关键的区别:
1)首先,互斥可用于跨进程的线程同步
2)其次,互斥能被赋予一个字符串名字,并且通过引用此名字创建现有互斥对象的附加句柄
提示:临界区与事件对象(比如互斥对象)的最大的区别在性能上。临界区在没有线程冲突时,要用10~15个时间片,而事件对象由于涉及到系统内核,所以要用400~600个时间片
可以调用函数CreatMutex() 来创建一个互斥量。下面是函数的声明
1
|
function CreateMutex(lpMutexAttributes: PSecurityAttributes; bInitialOwner: BOOL; lpName: PChar ): THandle; stdcall;
|
lpMutexAttributes 参数为一个指向TSecurityAttributes记录的指针。此参数通常设为nil , 表示默认的安全属性
bInitalOwner 参数表示创建互斥对象线程是否称为互斥对象的拥有者。当此参数为False时,表示互斥对象没有拥有者。
lpName 参数指定互斥对象的名称。设为nil表示无命名,如果参数不设为nil,函数会搜索是否有同名的互斥对象存在。如果有,函数就会返回同名互斥对象的句柄。否则,就新创建一个互斥对象并返回其句柄。
当使用完互斥对象时,应当调用CloseHandle()来关闭它。
下面演示使用互斥技术来使两个进程对一个数组的初始化同步
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
unit Main;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TMainForm = class (TForm)
Button1: TButton;
ListBox1: TListBox;
procedure Button1Click(Sender: TObject);
private
procedure ThreadsDone(Sender: TObject);
end ;
TFooThread = class (TThread)
protected
procedure Execute; override;
end ;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
const
MaxSize = 128 ;
var
NextNumber: Integer = 0 ;
DoneFlags: Integer = 0 ;
GlobalArray: array [ 1.. MaxSize] of Integer ;
hMutex: THandle = 0 ;
function GetNextNumber: Integer ;
begin
Result:= NextNumber;
Inc(NextNumber);
end ;
procedure TFooThread . Execute;
var
i: Integer ;
begin
FreeOnTerminate:= True ;
OnTerminate:= MainForm . ThreadsDone;
if WaitForSingleObject(hMutex, INFINITE) = WAIT_OBJECT_0 then
begin
for i:= 1 to MazSize do
begin
GlobalArray[i]:= GetNextNumber;
Sleep( 5 );
end ;
end ;
ReleaseMutex(hMutex);
end ;
procedure TMainForm . ThreadsDone(Sender: TObject);
var
i: Integer ;
begin
Inc(DoneFlags);
if DoneFlags = 2 then
for i:= 1 to MaxSize do
ListBox1 . Items . Add(IntToStr(GlobalArray[i]));
CloseHandle(hMutex);
end ;
end ;
procedure TMainForm . Button1Click(Sender: TObject);
begin
hMutex:= CreateMutex( nil , False , nil );
TFooThread . Create( False );
TFooThread . Create(Flase);
end ;
end .
|
你将注意到,在程序中使用 WaitForSingleObject() 来防止其他进程进入同步区域的代码。此函数声明如下
1
|
function WaitForSingleObject(hHandle: Thandle; dwMilliseconds: DWORD): DWORD; stdcall;
|
这个函数可以使当前线程在dwMilliseconds 指定的时间内睡眠,直到 hHandle参数指向的对象进入发信号状态为止。一个互斥对象不再被线程拥有时,它就进入发信号状态。当一个进程要终止时,它就进入发信号状态,而后立即返回。dwMilliSeconds参数设为 INFINITE,表示如果信号不出现将一直等下去。这个函数的返回值列在下表
返回值 |
含义 |
WAIT_ABANDONED |
指定的对象时互斥对象,并且拥有这个互斥对象的线程在没有释放此对象之前就已经终止。此时就称互斥对象被抛弃。这种情况下,这个互斥对象归当前线程所有,并把它设为非发信号状态 |
WAIT_OBJECT_0 |
指定的对象处于发信号状态 |
WAIT_TIMEOUT |
等待的事件已过,对象仍然是非发信号状态 |
再次声明,当一个互斥对象不再被一个线程所拥有,它就处于发信号状态,此时首先调用WaitForSignalObject() 函数的线程就称为该互斥对象的拥有者,此互斥对象设为不发信号状态。当线程调用ReleaseMutex() 函数并传递一个互斥对象的句柄作为参数时,这种拥有关系就被解除,互斥对象重新进入发信号状态
注意 除WaitForSingleObject() 函数外,你还可以使用 WaitForMultipleObject() 和MsgWaitForMultipleObject() 函数,它们可以等待几个对象变为发信号状态。这两个函数的详细情况请看Win32 API联机文档
3.信号量
另外一种使线程同步的技术是使用信号量对象。它是在互斥的基础上建立的,但是信号量增加了资源计数的功能,预定数目的线程允许同时进入要同步的代码。可以用 CreateSemaphore() 来创建一个信号量对象,其声明如下
1
|
function CreateSemaphore(lpSemaphoreAttributes: PSecurityAttributes; lInitialCount, lMaxiMumCount: LongInt ; lpName: PChar ): THandle; stdcall;
|
和CreateMutex() 函数一样,CreateSemaphore() 的第一个参数也是一个指向 TSecurityAttributes 记录的指针,此参数的缺省值可以设为 nil。
lInitialCount 参数用来指定一个信号量的初始计数值,这个值必须在 0 和 lMaximumCount 之间。此参数大于 0,就表示信号量处于发信号状态。当调用 WaitForSingleObject() 函数(或其他函数)时,此计数值就减1。当调用 ReleaseSemaphore() 时,此计数值加1。
参数 lMaximumCount 指定计数值的最大值。如果这个信号量代表某种资源,那么这个值代表可用资源总数
参数 lpName 用于给出信号量对象的名称,它类似于 CreateMutex() 函数的 lpName 参数。
下面是使用信号量技术来同步初始化数组的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
|
unit Main;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TMainForm = class (TForm)
Button1: TButton;
ListBox1: TListBox;
procedure Button1Click(Sender: TObject);
private
procedure ThreadsDone(Sender: TObject);
end ;
TFooThread = class (TThrea |
-
六六分期app的软件客服如何联系?不知道吗?加qq群【895510560】即可!标题:六六分期
阅读:19072|2023-10-27
-
今天小编告诉大家如何处理win10系统火狐flash插件总是崩溃的问题,可能很多用户都不知
阅读:9956|2022-11-06
-
今天小编告诉大家如何对win10系统删除桌面回收站图标进行设置,可能很多用户都不知道
阅读:8310|2022-11-06
-
今天小编告诉大家如何对win10系统电脑设置节能降温的设置方法,想必大家都遇到过需要
阅读:8680|2022-11-06
-
我们在使用xp系统的过程中,经常需要对xp系统无线网络安装向导设置进行设置,可能很多
阅读:8618|2022-11-06
-
今天小编告诉大家如何处理win7系统玩cf老是与主机连接不稳定的问题,可能很多用户都不
阅读:9630|2022-11-06
-
电脑对日常生活的重要性小编就不多说了,可是一旦碰到win7系统设置cf烟雾头的问题,很
阅读:8602|2022-11-06
-
我们在日常使用电脑的时候,有的小伙伴们可能在打开应用的时候会遇见提示应用程序无法
阅读:7984|2022-11-06
-
今天小编告诉大家如何对win7系统打开vcf文件进行设置,可能很多用户都不知道怎么对win
阅读:8626|2022-11-06
-
今天小编告诉大家如何对win10系统s4开启USB调试模式进行设置,可能很多用户都不知道怎
阅读:7523|2022-11-06
|
请发表评论