临界区 FAQ
- 来源: https://www.freebasic.net/wiki/wikka.php?wakka=ProPgMtCriticalSectionsFAQ
- 最后修订: 2025-04-26
多线程中与“临界区”相关的问题。
1. 何时不需要用互斥锁来保护多个线程间的一个共享变量?
当多个线程访问共享变量时,通常必须在所有线程中将它们的访问都放在 Mutexlock...Mutexunlock 块内:
当共享变量只是
size <= sizeof(integer)的简单预定义数值类型(仅需一条汇编指令访问)时,使用互斥锁可能不是必须的。但如果例如是使用win32编译的一个共享变量
LongInt,这里则建议使用互斥锁(否则,一个线程的读取阶段可能与另一个线程的写入阶段交错)。
这是因为要访问内存中的变量(用于读取或写入),处理器会使用其内部寄存器。
一个N位处理器有N位的寄存器,但没有更大的:
所以,只需一条汇编指令就可以访问内存中的一个N位变量。
相反,要访问一个2N位的变量,它必须使用2条汇编指令。
如果在这两条汇编指令(用于写入)之间,另一个线程访问了同一个变量(用于读取),获得的值可能是不一致的(N位最高位和N位最低位彼此不协调)。
可以通过一个使用两个线程和一个共享的 LongInt(64位)而不使用互斥锁的图形程序来验证此行为:
- 以32位编译时,许多读取值是不一致的。
- 以64位编译时,没有读取值是不一致的。
编译下面的测试程序:
- 以32位编译 => 许多错误的点不在圆上,而是在包含圆的方形内的任意位置。如果您取消注释第37/39/58/60行以激活互斥锁,那么所有获取的点现在都只在圆上。
- 以64位编译 => 所有点都有效,只在圆上,即使没有激活互斥锁。
start GeSHi
' - “用户定义的线程”计算圆上点的坐标,
' 并将这些坐标写入一个 LongInt (32位 & 32位 = 64位)
' - “主线程”从 LongInt 值绘制点。
'
' 行为:
' - 第一个点必须预先确定。
' - 无法防止同一个计算出的点被绘制多次
' (取决于主线程和用户线程循环之间的执行时间)。
' - 无法防止一个计算出的点可能未被绘制
' (对循环时间的相同说明)。
'
' 备注:
' 故意地,每个线程的循环中没有 Sleep(通常强烈不推荐),
' 但这只是为了在这个特殊情况下放大要观察的行为效果。
Union Point2D
Dim As LongInt xy
Type
Dim As Long y
Dim As Long x
End Type
End Union
Dim As Any Ptr handle
Dim Shared As Any Ptr mutex
Dim Shared As Integer quit
Sub Thread (ByVal param As Any Ptr)
Const pi As Single = 4 * Atn(1)
Dim As Point2D Ptr p = param
Do
Dim As Point2D P2D0
Dim As Single teta = 2 * pi * Rnd
P2D0.x = 320 + 200 * Cos(teta)
P2D0.y = 240 + 200 * Sin(teta)
' Mutexlock(mutex)
p->xy = P2D0.xy
' Mutexunlock(mutex)
' Sleep 5, 1
Loop Until quit = 1
End Sub
Screen 12
Dim As Point2D P2D
P2D.x = 520
P2D.y = 240
mutex = MutexCreate
handle = ThreadCreate(@Thread, @P2D)
Dim As Integer c
Do
Dim As Point2D P2D0
' Mutexlock(mutex)
P2D0.xy = P2D.xy
' Mutexunlock(mutex)
PSet (P2D0.x, P2D0.y), c
c = (c Mod 15) + 1
' Sleep 5, 1
Loop Until Inkey <> ""
quit = 1
ThreadWait(handle)
MutexDestroy(mutex)end GeSHi
2. 两个竞争线程之间,两个临界区(带有互斥锁锁定和条件变量发信号)的代码执行顺序是怎样的?
一个线程发信号时的时间顺序:
a) 当另一个线程正在等待时(在谓词的 While 循环内), b) 在另一个线程开始等待之前(在谓词的 While 循环内)。
start GeSHi
#define while_loop_on_predicate
Dim As Any Ptr handle
Dim Shared As Any Ptr mutex
Dim Shared As Any Ptr cond
Dim As Integer sleep0
Dim As Integer sleep1
#ifdef while_loop_on_predicate
Dim Shared As Integer ready
#endif
Sub Thread1 (ByVal param As Any Ptr)
Sleep *Cast(Integer Ptr, param), 1
MutexLock(mutex)
Color 11 : Print " Thread#1 locks the mutex"
Color 11 : Print " Thread#1 executes code with exclusion"
#ifdef while_loop_on_predicate
ready = 1
#endif
Color 11 : Print " Thread#1 is signaling"
CondSignal(cond)
Color 11 : Print " Thread#1 executes post-code with exclusion"
Color 11 : Print " Thread#1 unlocks the mutex"
MutexUnlock(mutex)
End Sub
Sub Thread0 (ByVal param As Any Ptr)
Sleep *Cast(Integer Ptr, param), 1
MutexLock(mutex)
Color 10 : Print " Thread#0 locks the mutex"
Color 10 : Print " Thread#0 executes pre-code with exclusion"
#ifdef while_loop_on_predicate
While ready <> 1
#endif
Color 10 : Print " Thread#0 is waiting"
CondWait(cond, mutex)
Color 10 : Print " Thread#0 is waked"
#ifdef while_loop_on_predicate
Wend
#endif
Color 10 : Print " Thread#0 executes code with exclusion"
#ifdef while_loop_on_predicate
ready = 0
#endif
Color 10 : Print " Thread#0 unlocks the mutex"
MutexUnlock(mutex)
End Sub
mutex = MutexCreate
cond = CondCreate
sleep0 = 0
sleep1 = 1000
Color 7 : Print "Chronology for Thread#1 signaling while Thread#0 is waiting:"
handle = ThreadCreate(@Thread1, @sleep1)
Thread0(@sleep0)
ThreadWait(handle)
Color 7 : Print "Thread#1 finished": Print
Sleep 1000, 1
sleep0 = 1000
sleep1 = 0
Color 7 : Print "Chronology for Thread#1 signaling before Thread#0 is waiting:"
handle = ThreadCreate(@Thread1, @sleep1)
Thread0(@sleep0)
ThreadWait(handle)
Color 7 : Print "Thread#1 finished": Print
MutexDestroy(mutex)
CondDestroy(cond)
Sleepend GeSHi
输出部分 a - Thread#1 在 Thread#0 等待时发信号的时间顺序:
Chronology for Thread#1 signaling while Thread#0 is waiting:
Thread#0 locks the mutex
Thread#0 executes pre-code with exclusion
Thread#0 is waiting
Thread#1 locks the mutex
Thread#1 executes code with exclusion
Thread#1 is signaling
Thread#1 executes post-code with exclusion
Thread#1 unlocks the mutex
Thread#0 is waked
Thread#0 executes code with exclusion
Thread#0 unlocks the mutex
Thread#1 finished输出部分 b - Thread#1 在 Thread#0 等待之前发信号的时间顺序:
Chronology for Thread#1 signaling before Thread#0 is waiting:
Thread#1 locks the mutex
Thread#1 executes code with exclusion
Thread#1 is signaling
Thread#1 executes post-code with exclusion
Thread#1 unlocks the mutex
Thread#0 locks the mutex
Thread#0 executes pre-code with exclusion
Thread#0 executes code with exclusion
Thread#0 unlocks the mutex
Thread#1 finished注意:如果 CondWait 不在谓词的 While 循环中(通过注释上面程序的第一行),可以在第二种情况(Thread#1 在 Thread#0 等待之前发信号)中检查到,Thread#0 在其等待阶段保持阻塞(按 Ctrl-C 退出)。
3. 如果调用 Condsignal() 或 Condbroadcast() 时没有锁定互斥锁会发生什么?
参考 临界区 部分的示例2,借此机会重申:
在执行
Condsignal()或Condbroadcast()唤醒线程时,必须始终锁定互斥锁(可以在Condsignal()或Condbroadcast()之后解锁,但不能在此之前)。如果互斥锁没有锁定(或者甚至在执行
Condsignal()或Condbroadcast()之前刚刚解锁),行为可能变得不可预测(可能有效或无效,取决于线程配置和实际执行时间)。
在 临界区 的示例2“使用 condwait 然后 condbroadcast(和互斥锁)进行所有线程同步的方法示例”中:
如果至少有一个
Mutexunlock()被移到其Condbroadcast()之前,程序很快就会挂起。尽管有些用户确信互斥锁可以总是在
Condsignal()或Condbroadcast()之前解锁,而其他更谨慎的人断言只能在Condbroadcast()时这样做,但实验表明恰恰相反!
一般规则是:
条件不能在某个线程锁定互斥锁之后、等待条件变量 (
CondWait()) 之前这段时间内发出信号(通过Condsignal()或Condbroadcast()),否则似乎会损坏该条件变量上的线程等待队列。因此,为了避免这种情况并遵循此规则,必须在发出条件信号时保持互斥锁锁定。
4. 为什么必须将 Condwait 放在一个用于检查布尔谓词的 While...Wend 循环中(由其他线程在激活 Condsignal 或 Condbroadcast 之前设置,并在 Wend 之后重置)?
While predicate <> True
Condwait(conditionalid, mutexid)
Wend
predicate = False在所有文档中,都强烈建议这样做,主要理由是为了对抗可能的虚假唤醒。
这可能是真的,但也建议这样做,以避免在接收线程尚未在 CondWait 上等待时过早激活 CondSignal(或 CondBroadcast)而导致信号永远丢失:
在这种情况下,接收线程甚至在
CondSignal(或CondBroadcast)被激活之前尚未锁定互斥锁。因此,谓词在接收线程到达
While...Wend循环之前就已经为真,导致CondWait完全被跳过,从而避免了一个确定的阻塞现象。
让两个线程(主程序中的线程#0,用户过程中的线程#1,每个在一个循环中打印其编号),具有大致相同的执行时间,并且每个线程同步另一个线程,以便良好地交错它们的编号(通过使用一个互斥锁、两个条件变量和 CondSignal/CondWait):
- 如果没有在谓词周围使用
While...Wend循环,程序很快就会挂起(按 Ctrl-C 退出): - 如果在每个
CondWait周围使用谓词的While...Wend循环,则没有阻塞现象:
4.1. 当已经有一个用于检查其他线程设置的布尔谓词的 While...Wend 循环时,Condwait(以及 Condsignal 或 Condbroadcast)仍然有用吗?
(由前一个问题引发的另一个问题)
- 推荐的结构如下:
' 用于线程子节的互斥 + CONDWAIT 在带有谓词检查的 While...Wend 循环中的原理
' (连接线连接受序列中每个操作影响的发送方和接收方)
'
'
' 线程 其他线程
' MUTEXLOCK(mutexID) <------------------------- 来自 ( atomic_mutex_unlock(mutexID) ) 或 MUTEXUNLOCK(mutexID)
' .......
' While booleanT <> True <--------------------- 来自 booleanT = True
' ( atomic_mutex_unlock(mutexID) ) -------> 到 MUTEXLOCK(mutexID) 或 ( atomic_mutex_re-lock(mutexID) )
' CONDWAIT(conditionalID, mutexID) <------- 来自 CONDSIGNAL(conditionalID)
' ( atomic_mutex_re-lock(mutexID) ) <------ 来自 ( atomic_mutex_unlock(mutexID) ) 或 MUTEXUNLOCK(mutexID)
' Wend
' booleanT = False
' .......
' MUTEXUNLOCK(mutexID) -----------------------> 到 MUTEXLOCK(mutexID) 或 ( atomic_mutex_re-lock(mutexID) )' 用于线程子节的互斥 + CONDSIGNAL 与谓词检查的原理
' (连接线连接受序列中每个操作影响的发送方和接收方)
'
' 线程 其他线程
' MUTEXLOCK(mutexID) <------------------------- 来自 ( atomic_mutex_unlock(mutexID) ) 或 MUTEXUNLOCK(mutexID)
' .......
' booleanOT = True ---------------------------> 到 While booleanOT <> True
' CONDSIGNAL(conditionalID) ------------------> 到 CONDWAIT(conditionalID, mutexID)
' .......
' MUTEXUNLOCK(mutexID) -----------------------> 到 MUTEXLOCK(mutexID) 或 ( atomic_mutex_re-lock(mutexID) )- 如果不使用
CondWait,则必须在布尔标志的While...Wend循环中放置一条Sleep x, 1指令,以便在循环时释放时间片(此外,此Sleep x, 1必须放在一个 ['Mutexunlock'...'Mutexlock'] 块内以释放另一个线程):
' 用于线程子节的互斥 + SLEEP 在带有谓词检查的 While...Wend 循环中的原理
' (连接线连接受序列中每个操作影响的发送方和接收方)
'
' 线程 其他线程
' MUTEXLOCK(mutexID) <------------------------- 来自 MUTEXUNLOCK(mutexID)
' .......
' While booleanT <> True <--------------------- 来自 booleanT = True
' MUTEXUNLOCK(mutexID) -------------------> 到 MUTEXLOCK(mutexID)
' SLEEP(tempo, 1)
' MUTEXLOCK(mutexID) <--------------------- 来自 MUTEXUNLOCK(mutexID)
' Wend
' booleanT = False
' .......
' MUTEXUNLOCK(mutexID) -----------------------> 到 MUTEXLOCK(mutexID)' 用于线程子节的仅互斥 + 谓词检查的原理
' (连接线连接受序列中每个操作影响的发送方和接收方)
'
' 线程 其他线程
' MUTEXLOCK(mutexID) <------------------------- 来自 MUTEXUNLOCK(mutexID)
' .......
' booleanOT = True ---------------------------> 到 While booleanOT <> True
' .......
' MUTEXUNLOCK(mutexID) -----------------------> 到 MUTEXLOCK(mutexID)在 CondWait 期间,线程执行被挂起,并且不消耗任何 CPU 时间,直到条件变量被发出信号。
但如果改为放置 Sleep x, 1,等待时间是预定的,并且不像 CondWait 那样具有自适应性。
=> CondWait 对于优化执行时间是有用的。
5. 如何实现一个完全线程安全的用户输入行函数?
Input 关键字可能不是线程安全的,当另一个线程也必须访问输入/输出资源时:
执行
Input语句时,其他正在运行的线程不得更改文本光标的位置,这禁止了诸如Locate、Print等指令……此外,不能将
Input关键字封装在互斥锁锁定块内(就像对Inkey关键字那样),因为在输入行未完成和验证时,其他也想访问输入/输出的线程将被完全阻塞(等待互斥锁解锁)。
线程安全的输入行函数(针对输入/输出资源):
输入位置、提示信息、睡眠时间、行清空命令、互斥锁指针可以传递给以下 threadInput() 函数,该函数通过围绕 Inkey 关键字循环来模拟一个简化的输入函数,但它是线程安全的(所有输入/输出关键字必须封装在互斥锁锁定块内,并且光标位置必须在每个互斥锁锁定块结束时恢复):
start GeSHi
Function threadInput (ByVal row As Integer, ByVal column As Integer, ByRef prompt As String = "", _
ByVal sleeptime As Integer = 15, ByVal blank As Integer = 0, ByVal mutex As Any Ptr = 0 _
) As String
Dim As String inputchr
Dim As String inputline
Dim As Integer cursor
Dim As Integer cursor0
Dim As Integer r
Dim As Integer c
MutexLock(mutex)
r = CsrLin()
c = Pos()
Locate row, column
Print prompt & " _";
cursor0 = Pos() - 1
Locate r, c
MutexUnlock(mutex)
Do
MutexLock(mutex)
r = CsrLin()
c = Pos()
inputchr = Inkey
If inputchr <> "" Then
If inputchr >= Chr(32) And inputchr < Chr(255) Then
inputline = Left(inputline, cursor) & inputchr & Mid(inputline, cursor + 1)
cursor += 1
ElseIf inputchr = Chr(08) And Cursor > 0 Then 'BkSp
cursor -= 1
inputline = Left(inputline, cursor) & Mid(inputline, cursor + 2)
ElseIf inputchr = Chr(255) & "S" And Cursor < Len(inputline) Then 'Del
inputline = Left(inputline, cursor) & Mid(inputline, cursor + 2)
ElseIf inputchr = Chr(255) + "M" And Cursor < Len(inputline) Then 'Right
Cursor += 1
ElseIf inputchr = Chr(255) + "K" And Cursor > 0 Then 'Left
Cursor -= 1
End If
If inputchr = Chr(27) Then 'Esc
Locate row, cursor0
Print Space(Len(inputline) + 1);
inputline = ""
cursor = 0
End If
Locate row, cursor0
Print Left(inputline, cursor) & Chr(95) & Mid(inputline, cursor + 1) & " ";
End If
Locate r, c
MutexUnlock(mutex)
Sleep sleeptime, 1
Loop Until inputchr = Chr(13)
If blank <> 0 Then
MutexLock(mutex)
r = CsrLin()
c = Pos()
Locate row, cursor0
Print Space(Len(inputline) + 1);
Locate r, c
MutexUnlock(mutex)
End If
Return inputline
End Functionend GeSHi
- 来自
临界区页面示例1“对所有线程使用互斥锁的异步方法示例”,现在正在运行的多线程代码等待“退出”命令以退出程序:
注意:
否则,如果在线程中仅使用图形关键字(仅使用图形光标位置),如 Line、Draw String、Put,会导致线程安全的过程,与主代码中的 Line Input 关键字兼容,无需互斥锁:
start GeSHi
Type UDT
Dim As Integer number
Dim As Integer tempo
Dim As Any Ptr pThread
Dim As ULongInt count
Dim As Any Ptr img
Static As Integer numberMax
Static As Integer quit
End Type
Dim As Integer UDT.numberMax
Dim As Integer UDT.quit
Const As String prompt = "Enter ""quit"" for exit"
Dim As String s
Sub Counter (ByVal pt As UDT Ptr) ' 对于图形字符大小 8x16
With *pt
Line .img, (0, 0)-(20 * 8 - 1, 16 - 1), 0, BF ' 清空图像缓冲区
Sleep 5, 1
.count += 1
Draw String .img, (0, 0), Str(.count) ' 在图像缓冲区中绘制
Put ((.number - 1) * 8, (.number - 1) * 16), .img, PSet ' 将图像缓冲区复制到屏幕
End With
End Sub
Sub Thread (ByVal p As Any Ptr) ' 对于图形字符大小 8x16
Dim As UDT Ptr pUDT = p
With *pUDT
.img = ImageCreate(20 * 8, 16) ' 使用图像缓冲区以避免闪烁
Do
Counter(pUDT)
Sleep .tempo, 1
Loop Until .quit = 1
ImageDestroy .img ' 销毁图像缓冲区
End With
End Sub
Screen 12
UDT.numberMax = 6
Dim As UDT u(0 To UDT.numberMax)
For I As Integer = 0 To UDT.numberMax
u(I).number = i
u(I).tempo = 100 + 15 * I - 95 * Sgn(I)
Next I
Dim As Single t = Timer
For I As Integer = 1 To UDT.numberMax
u(I).pThread = ThreadCreate(@Thread, @u(I))
Next I
Do
Locate 8, 1, 0
Line Input; prompt; s
Locate , Len(prompt) + 3
Print Space(Len(s));
Loop Until LCase(s) = "quit"
UDT.quit = 1
For I As Integer = 1 To UDT.numberMax
ThreadWait(u(I).pThread)
Next I
t = Timer - t
Dim As ULongInt c
For I As Integer = 1 To UDT.numberMax
c += u(I).count
Next I
Locate UDT.numberMax + 4, 1
Print CULngInt(c / t) & " increments per second"
Sleepend GeSHi
6. 如何与多线程一起使用 Screenlock?
Screenlock...Scrennunlock块与多线程不兼容(否则程序会挂起)。这就是为什么必须在每个这样的块周围使用互斥锁块来确保互斥访问。当屏幕锁定时,输入关键字(如键盘、鼠标)无法安全运行,因此这样的关键字必须位于任何
Screenlock...Screenunlock块之外,也就是在其自己的线程中位于其Screenlock...Screenunlock块之外,并且通过互斥锁块来防止其他线程的所有Screenlock...Screenunlock块。因此,等待按键或行输入的Getkey和Input语句不可用,但Inkey不等待,可以工作。
通过严格遵守一些规则,可以在线程内使用 Screenlock/Screenunlock。
所有线程(包括主代码)的编码原则:
Do
' 不包含显示(打印/绘图,...)也不包含输入(input/inkey/鼠标获取,...)的指令
MutexLock(m)
Screenlock
' 仅包含显示(打印/绘图,...)的指令
Screenunlock
' 仅包含不等待的输入(inkey/鼠标获取,...)的指令
MutexUnlock(m)
Sleep tempo, 1
Loop Until condition- 例如,必须在每个
Screenlock...Screenunlock块周围使用一个Mutexlock...Mutexunlock块,并且在Inkey指令周围使用另一个,而Inkey指令本身必须始终在任何Screenlock...Screenunlock块之外:
7. 如何与多线程一起使用“视频分页(双缓冲或页面翻转)”?
可以使用“视频分页(双缓冲或页面翻转)”来更简单地与多线程一起使用,但请注意,gfxlib2 中的许多状态是线程相关的,比如 Screenset(以及 View 设置、图形光标位置、图形颜色等)。
因此,工作页面和可见页面的设置必须始终在希望使用多视频页配置的每个线程代码中控制。
- 双缓冲方法的示例(在每一步,每个线程都需要更新工作页面并将其复制到可见页面,在互斥代码块内):
- 两页翻转方法的示例(在每一步,每个线程都需要更新和翻转,在相同的互斥代码块内,这两个屏幕页面):
注意:在这两个示例中,两个线程中都需要一个互斥代码块,不仅因为使用了控制台语句 + Inkey,还因为使用了双缓冲方法(如果没有防闪烁处理,图形语句可以放在互斥代码块之外)。
8. 如何将FB运行时库用于多线程应用程序(gfxlib2),并与多线程一起使用?
gfxlib2 的源代码使用 TLS(线程本地存储)来存储许多状态,因此许多东西是线程特定的。
由于 gfxlib2 是线程安全的,线程之间的互斥锁对于图形语句本身(包括 Draw String)不是必需的。
相反,诸如 Locate、Print 等控制台语句如前所述不是线程安全的(例如,文本光标位置对所有线程是公共的)。
- 显示图形状态(如图形光标位置、图形颜色)是线程相关的简单示例:
- 显示线程中的图形语句(如 Line 和 Draw String 和 Screencopy)可以与另一个线程中的控制台语句(如 Inkey)竞争,而无需使用任何互斥锁的示例:
- 从上面的示例中,如果日期显示和时间显示现在是两个单独的线程,则必须在这两个线程之间使用互斥锁代码块,这不是因为图形语句本身竞争,而仅仅是因为使用的双缓冲方法(防止闪烁)导致这两个线程竞争:
9. 如何与多线程一起使用控制台语句和键盘输入?
控制台语句(如 Locate、Print、Color 等),以及在图形窗口上的 Locate 和 Print(但不是图形窗口上的 Color),以及键盘输入(如 Inkey、Getkey、Input 等)不是线程安全的:
因此,当它们在不同线程的竞争部分中使用时,必须通过互斥锁锁定块来进行互斥,在该块中,代码还可以在块结束时(在自身使用后)恢复状态(如文本光标位置、控制台颜色等),就像在块开始时那样。
但是,
Getkey或Input关键字不能封装在互斥锁锁定块内(就像可以对Inkey关键字所做的那样),因为只要键盘输入未完成,竞争中的其他线程将被完全阻塞(等待互斥锁解锁)。显示在控制台窗口上或图形窗口上应用时,关键字
Locate和Print都不是线程安全的示例(在这两种情况下,文本光标状态都不是线程相关的):从上面的示例中,线程代码已通过互斥锁锁定块以及在其自身移动光标之前/之后保存/恢复光标状态,在其竞争部分完成:
显示在控制台窗口上应用时,
Color关键字不是线程安全的,但在图形窗口上应用时是线程安全的示例(在这种情况下,颜色状态是线程相关的):从上面的示例中,线程代码已通过互斥锁锁定块以及在其自身使用颜色值之前/之后保存/恢复颜色状态,在其竞争部分完成:
因此,为了在线程的竞争部分使用 Getkey 或 Input:
只有一个线程(例如,主线程)可以在其竞争部分使用
Getkey或Input以及控制台语句(如Locate、Print、Color等)以及Inkey。其他线程不得在其竞争部分使用任何控制台语句或任何键盘输入关键字,但可以使用图形语句(如
Pset、Line、Circle、Draw String、图形Color等),这些语句本身是线程安全的(它们可以与主线程在图形上交错,而没有任何问题)。Input和Getkey也排除了在线程的竞争部分使用屏幕锁定(建议将双缓冲作为防闪烁方法)。显示线程(此处为用户线程)中的图形语句(如
Line和Draw String和Screencopy)可以与另一个线程(此处为主线程)中的控制台语句(如Locate和Print和Input)竞争,而无需使用任何互斥锁的示例:从上面的示例中,如果日期显示和时间显示现在是两个单独的用户线程,则必须仅在这两个用户线程之间使用互斥锁代码块,这不是因为图形语句本身竞争,而仅仅是因为用于防止闪烁的双缓冲方法仅导致这两个用户线程竞争:
10. 在线程中使用关键字 Sleep 时,最好采取预防措施吗?
在多线程上下文中,对于 Sleep 关键字的完美行为仍然存在一些疑问。
因此,建议采取以下预防措施:
如果在线程的临界区中绝对必要,由于会引发按键内部测试,语法
Sleep x或Sleep x, 0为了最大安全性,最好以与Inkey关键字相同的方式处理,以尽可能避免与其他线程的任何并发冲突。否则,当没有通过互斥进行保护时,更建议使用语法
Sleep x, 1(不引发按键内部测试),这通常是为了释放时间片给其他线程。
11. 所有处理多线程的工具可以封装在一个基类中(用户通过派生类型扩展以实现其自己的功能)吗?
可以定义一个简单的“threadUDT”基类,如下所示:
- 每个句柄有一个私有的、非静态的成员字段“Any Ptr”,
- 一个共享的互斥锁有一个私有的、静态的成员字段“Any Ptr”,
- 一个共享的条件变量有一个私有的、静态的成员字段“Any Ptr”,
- 其自己的公共成员过程“Sub()”调用相应的多线程内置过程(具有相同的过程名称),还包括对上述3个指针的值完整性测试(3个“thread...()”成员 Subs 的非静态过程,以及4个“mutex...()”成员 Subs 和5个“cond...()”成员 Subs 的静态过程),
- 一个抽象的私有“Sub()”线程,由用户派生类型内部的另一个“Sub()”重写(因此,其静态地址在对象的虚表中可用,并且通过引用传递的隐藏“This”参数与要传递给线程的“Any Ptr”参数兼容)。
start GeSHi
#include once "fbthread.bi"
Type threadUDT Extends Object
Public:
Declare Sub ThreadCreate ()
Declare Sub ThreadWait ()
Declare Sub threadDetach ()
Declare Static Sub MutexCreate ()
Declare Static Sub MutexLock ()
Declare Static Sub MutexUnlock ()
Declare Static Sub MutexDestroy ()
Declare Static Sub CondCreate ()
Declare Static Sub CondWait ()
Declare Static Sub CondSignal ()
Declare Static Sub CondBroadcast ()
Declare Static Sub CondDestroy ()
Private:
Declare Abstract Sub thread ()
Dim As Any Ptr pThread
Static As Any Ptr pMutex
Static As Any Ptr pCond
End Type
Dim As Any Ptr threadUDT.pMutex
Dim As Any Ptr threadUDT.pCond
Sub threadUDT.ThreadCreate ()
If This.pThread = 0 Then
This.pThread = .ThreadCreate(Cast(Any Ptr Ptr Ptr, @This)[0][0], @This)
End If
End Sub
Sub threadUDT.ThreadWait ()
If This.pThread > 0 Then
.ThreadWait(This.pThread)
This.pThread = 0
End If
End Sub
Sub threadUDT.threadDetach ()
If This.pThread > 0 Then
.Threaddetach(This.pThread)
This.pThread = 0
End If
End Sub
Sub threadUDT.MutexCreate ()
If threadUDT.pMutex = 0 Then
threadUDT.pMutex = .MutexCreate
End If
End Sub
Sub threadUDT.MutexLock ()
If threadUDT.pMutex > 0 Then
.MutexLock(threadUDT.pMutex)
End If
End Sub
Sub threadUDT.MutexUnlock ()
If threadUDT.pMutex > 0 Then
.MutexUnlock(threadUDT.pMutex)
End If
End Sub
Sub threadUDT.MutexDestroy ()
If threadUDT.pMutex > 0 Then
.MutexDestroy(threadUDT.pMutex)
threadUDT.pMutex = 0
End If
End Sub
Sub threadUDT.CondCreate ()
If threadUDT.pCond = 0 Then
threadUDT.pCond = .CondCreate
End If
End Sub
Sub threadUDT.CondWait ()
If threadUDT.pCond > 0 And threadUDT.pMutex > 0 Then
.CondWait(threadUDT.pCond, threadUDT.pMutex)
End If
End Sub
Sub threadUDT.CondSignal ()
If threadUDT.pCond > 0 And threadUDT.pMutex > 0 Then
.CondSignal(threadUDT.pCond)
End If
End Sub
Sub threadUDT.CondBroadcast ()
If threadUDT.pCond > 0 And threadUDT.pMutex > 0 Then
.CondBroadcast(threadUDT.pCond)
End If
End Sub
Sub threadUDT.CondDestroy ()
If threadUDT.pCond > 0 Then
.CondDestroy(threadUDT.pCond)
threadUDT.pCond = 0
End If
End Subend GeSHi
- 来自
临界区页面示例2“对所有线程使用 condwait 然后 condbroadcast(和互斥锁)进行同步的方法示例”,现在用户实现被修改为与基类“threadUDT”兼容:
12. 线程被 ThreadCreate 创建后,其代码的执行延迟是多少?
人们可能认为线程的第一行代码总是至少在 ThreadCreate() 返回后执行,但这既不能保证,甚至也观察不到。
可以通过在 ThreadCreate() 之后的一行和线程的第一行代码之间尽可能相似地记忆时间来估计 ThreadCreate() 返回和线程启动之间的延迟(正或负)(延迟计算在线程结束后执行)。
经过一段时间的观察,可以找到小的负值和大的正值。
有趣的是看到线程体执行开始和 ThreadCreate() 返回点之间的最小时间、平均时间和最大时间:
start GeSHi
Dim As Any Ptr ptid
Dim As Double t0
Dim As Any Ptr p0 = @t0
Dim As Double t1
Dim As Double count
Dim As Single tmean
Dim As Single tmin = 10 '' 起始值
Dim As Single tmax = -10 '' 起始值
Sub myThread (ByVal p As Any Ptr)
*Cast(Double Ptr, p) = Timer '' 类似于主代码中的代码行
End Sub
Print "Tmin/Tmean/Tmax between begin of thread code and return from ThreadCreate() :"
Do
count += 1
ptid = ThreadCreate(@myThread, @t1)
*Cast(Double Ptr, p0) = Timer '' 类似于线程代码中的代码行
ThreadWait(ptid)
tmean = (tmean * (count - 1) + (t1 - t0)) / count
If t1 - t0 `< tmin Or t1 - t0 >` tmax Then
If t1 - t0 < tmin Then
tmin = t1 - t0
End If
If t1 - t0 > tmax Then
tmax = t1 - t0
End If
Print Time; Using " Tmin=+###.###### ms Tmean=+###.###### ms Tmax=+###.###### ms"; tmin * 1000; tmean * 1000; tmax * 1000
End If
Loop Until Inkey <> ""end GeSHi
输出(例如):
Tmin/Tmean/Tmax between begin of thread code and return from ThreadCreate() :
21:30:13 Tmin= +0.151800 ms Tmean= +0.151800 ms Tmax= +0.151800 ms
21:30:13 Tmin= +0.006000 ms Tmean= +0.078900 ms Tmax= +0.151800 ms
21:30:13 Tmin= +0.006000 ms Tmean= +0.098394 ms Tmax= +0.172500 ms
21:30:13 Tmin= +0.006000 ms Tmean= +0.121555 ms Tmax= +0.884900 ms
21:30:45 Tmin= +0.006000 ms Tmean= +0.055810 ms Tmax= +1.104200 ms
21:30:54 Tmin= +0.006000 ms Tmean= +0.055764 ms Tmax= +4.056600 ms
21:31:44 Tmin= -0.116300 ms Tmean= +0.055516 ms Tmax= +4.056600 ms
21:32:10 Tmin= -0.136800 ms Tmean= +0.057177 ms Tmax= +4.056600 ms
21:32:12 Tmin= -0.150300 ms Tmean= +0.057265 ms Tmax= +4.056600 ms
21:33:17 Tmin= -0.150300 ms Tmean= +0.060048 ms Tmax= +4.979900 ms
21:33:18 Tmin= -0.150300 ms Tmean= +0.060157 ms Tmax= +7.086300 ms
21:33:23 Tmin= -0.150600 ms Tmean= +0.060347 ms Tmax= +7.086300 ms
21:33:38 Tmin= -0.205900 ms Tmean= +0.060878 ms Tmax= +7.086300 ms
21:35:30 Tmin= -0.208700 ms Tmean= +0.061315 ms Tmax= +7.086300 ms注意:
如果用户希望安全地将线程执行始终延迟到 ThreadCreate() 行之后的一些代码行之后,可以使用 ThreadCreate() 行和线程体开始之间的互斥锁,原则如下:
start GeSHi
Dim Shared As Any Ptr pMutexForThreadStart
'-------------------------------------------
Sub Thread (ByVal p As Any Ptr)
MutexLock(pMutexForThreadStart)
MutexUnlock(pMutexForThreadStart)
'
' 用户线程体
'
End Sub
'--------------------------------------------
'
' 用户主代码
'
pMutexForThreadStart = MutexCreate()
'
' 用户主代码继续
'
MutexLock(pMutexForThreadStart)
Dim As Any Ptr pThread = ThreadCreate(@Thread)
'
' 在线程的用户体执行开始之前要执行的代码行
'
MutexUnlock(pMutexForThreadStart)
'
' 用户主代码继续
'
ThreadWait(pThread)
MutexDestroy(pMutexForThreadStart)end GeSHi
13. 无论是通过互斥还是条件变量来同步线程,其同步延迟是多少?
每个线程的同步等待阶段不应像 Sleep 那样消耗任何 CPU 资源,这就是 MutexLock() 和 CondWait() 指令的情况。
通过互斥或条件变量进行线程同步会给线程的初始执行时间增加延迟,但这种延迟(几微秒)比包含最短睡眠(Sleep 1, 1)和标志测试的简单等待循环(至少几毫秒)的延迟要短得多。
以下代码允许通过使用简单标志、互斥或条件变量来估计主线程和子线程之间的这种同步延迟:
start GeSHi
Dim Shared As Any Ptr mutex0, mutex1, mutex2, mutex, cond1, cond2, pt
Dim Shared As Integer flag1, flag2
Dim As Double t
'----------------------------------------------------------------------------------
#if defined(__FB_WIN32__)
Declare Function _setTimer Lib "winmm" Alias "timeBeginPeriod"(ByVal As Ulong = 1) As Long
Declare Function _resetTimer Lib "winmm" Alias "timeEndPeriod"(ByVal As Ulong = 1) As Long
#endif
Sub ThreadFlag(ByVal p As Any Ptr)
MutexUnlock(mutex0) '' 为主线程解锁互斥锁
For I As Integer = 1 To 100
While flag1 = 0
Sleep 1, 1
Wend
flag1 = 0
' 仅子线程代码运行(例如位置)
flag2 = 1
Next I
End Sub
mutex0 = MutexCreate()
MutexLock(mutex0)
pt = ThreadCreate(@ThreadFlag)
MutexLock(mutex0) '' 等待线程启动(来自子线程的互斥锁解锁)
Print "Thread synchronization latency by simple flags:"
#if defined(__FB_WIN32__)
_setTimer()
Print "(in high resolution OS cycle period)"
#else
Print "(in normal resolution OS cycle period)"
#endif
t = Timer
For I As Integer = 1 To 100
flag1 = 1
While flag2 = 0
Sleep 1, 1
Wend
flag2 = 0
' 仅主线程代码运行(例如位置)
Next I
t = Timer - t
#if defined(__FB_WIN32__)
_resetTimer()
#endif
ThreadWait(pt)
Print Using "####.## milliseconds per double synchronization (round trip)"; t * 10
Print
MutexDestroy(mutex0)
'----------------------------------------------------------------------------------
Sub ThreadMutex(ByVal p As Any Ptr)
MutexUnlock(mutex0) '' 为主线程解锁互斥锁
For I As Integer = 1 To 100000
MutexLock(mutex1) '' 等待主线程解锁互斥锁
' 仅子线程代码运行
MutexUnlock(mutex2) '' 为主线程解锁互斥锁
Next I
End Sub
mutex0 = MutexCreate()
mutex1 = MutexCreate()
mutex2 = MutexCreate()
MutexLock(mutex0)
MutexLock(mutex1)
MutexLock(mutex2)
pt = ThreadCreate(@ThreadMutex)
MutexLock(mutex0) '' 等待线程启动(来自子线程的互斥锁解锁)
Print "Thread synchronization latency by mutual exclusions:"
t = Timer
For I As Integer = 1 To 100000
MutexUnlock(mutex1) '' 为子线程解锁互斥锁
MutexLock(mutex2) '' 等待子线程解锁互斥锁
' 仅主线程代码运行
Next I
t = Timer - t
ThreadWait(pt)
Print Using "####.## microseconds per double synchronization (round trip)"; t * 10
Print
MutexDestroy(mutex0)
MutexDestroy(mutex1)
MutexDestroy(mutex2)
'----------------------------------------------------------------------------------
Sub ThreadCondVar(ByVal p As Any Ptr)
MutexUnlock(mutex0) '' 为主线程解锁互斥锁
For I As Integer = 1 To 100000
MutexLock(mutex)
While flag1 = 0
CondWait(cond1, mutex) '' 等待来自主线程的条件信号
Wend
flag1 = 0
' 仅子线程代码运行(例如位置)
flag2 = 1
CondSignal(cond2) '' 向主线程发送条件信号
MutexUnlock(mutex)
Next I
End Sub
mutex0 = MutexCreate()
mutex = MutexCreate()
MutexLock(mutex0)
cond1 = CondCreate()
cond2 = CondCreate()
pt = ThreadCreate(@ThreadCondVar)
MutexLock(mutex0) '' 等待线程启动(来自子线程的互斥锁解锁)
Print "Thread synchronization latency by conditional variables:"
t = Timer
For I As Integer = 1 To 100000
MutexLock(mutex)
flag1 = 1
CondSignal(cond1) '' 向子线程发送条件信号
While flag2 = 0
CondWait(Cond2, mutex) '' 等待来自子线程的条件信号
Wend
flag2 = 0
' 仅子线程代码运行(例如位置)
MutexUnlock(mutex)
Next I
t = Timer - t
ThreadWait(pt)
Print Using "####.## microseconds per double synchronization (round trip)"; t * 10
Print
MutexDestroy(mutex0)
MutexDestroy(mutex)
CondDestroy(cond1)
CondDestroy(cond2)
'----------------------------------------------------------------------------------
Sleepend GeSHi
结果示例:
Thread synchronization latency by simple flags:
(in high resolution OS cycle period)
2.02 milliseconds per double synchronization (round trip)
Thread synchronization latency by mutual exclusions:
5.93 microseconds per double synchronization (round trip)
Thread synchronization latency by conditional variables:
7.54 microseconds per double synchronization (round trip)通过使用条件变量以并发或独占模式执行线程同步的示例:
start GeSHi
Dim Shared As Any Ptr pt, mutex1, mutex2, cond1, cond2
Dim Shared As Integer quit, flag1, flag2
Print "'1': Main thread procedure running (alone)"
Print "'2': Child thread procedure running (alone)"
Print "'-': Main thread procedure running (with the one of child thread)"
Print "'=': Child thread procedure running (with the one of main thread)"
Print
Sub Prnt(ByRef s As String, ByVal n As Integer)
For I As Integer = 1 To n
Print s;
Sleep 20, 1
Next I
End Sub
Sub ThreadCondCond(ByVal p As Any Ptr)
Do
MutexLock(mutex1)
While flag1 = 0 '' 测试来自主线程设置的标志
CondWait(cond1, mutex1) '' 等待来自主线程的条件信号
Wend
flag1 = 0 '' 重置标志
MutexUnlock(mutex1)
If quit = 1 Then Exit Sub '' 退出线程循环
Prnt("=", 10)
MutexLock(mutex2)
flag2 = 1 '' 为主线程设置标志
CondSignal(cond2) '' 向主线程发送条件信号
Prnt("2", 10)
MutexUnlock(mutex2)
Loop
End Sub
mutex1 = MutexCreate()
mutex2 = MutexCreate()
cond1 = CondCreate()
cond2 = CondCreate()
pt = ThreadCreate(@ThreadCondCond)
For I As Integer = 1 To 5
MutexLock(mutex1)
flag1 = 1 '' 为子线程设置标志
CondSignal(cond1) '' 向子线程发送条件信号
MutexUnlock(mutex1)
Prnt("-", 10)
MutexLock(mutex2)
While flag2 = 0 '' 测试来自子线程设置的标志
CondWait(Cond2, mutex2) '' 等待来自子线程的条件信号
Wend
flag2 = 0 '' 重置标志
Prnt("1", 10)
MutexUnlock(mutex2)
Next I
MutexLock(mutex1)
quit = 1 '' 为子线程设置退出标志
flag1 = 1
CondSignal(cond1) '' 向子线程发送条件信号
MutexUnlock(mutex1)
ThreadWait(pt) '' 等待子线程结束
Print
MutexDestroy(mutex1)
MutexDestroy(mutex2)
CondDestroy(cond1)
CondDestroy(cond2)
Sleepend GeSHi
输出:
'1': Main thread procedure running (alone)
'2': Child thread procedure running (alone)
'-': Main thread procedure running (with the one of child thread)
'=': Child thread procedure running (with the one of main thread)
-==-=-=--==--==-=-=-22222222221111111111-=-=-=-==--==-=--==-22222222221111111111-=-=-==-=-=--=-=-==-22222222221111111111
-=-=-=-=-=-=--==-=-=22222222221111111111-==--==--==-=-=--==-2222222222111111111114. 当多个线程等待同一个条件变量时会发生什么?
- 如果使用
CondSignal(): - 如果使用
CondBroadcast():
下面的示例与6个线程一起工作(除了主线程之外)。
前3个线程(#1 到 #3)等待它们自己的条件变量,而后3个线程(#4 到 #6)等待另一个相同的条件变量。
这最后3个线程要么在 CondSignal() 上被唤醒,要么在 CondBroadcast() 上被唤醒。
start GeSHi
Type ThreadData
Dim As Integer id
Dim As Any Ptr mutex
Dim As Any Ptr cond
Dim As Boolean flag
Dim As Boolean quit
Dim As Any Ptr handle
Declare Static Sub Thread(ByVal p As Any Ptr)
End Type
Sub ThreadData.Thread(ByVal p As Any Ptr)
Dim As ThreadData Ptr pdata = p
Print " thread #" & pdata->id & " is running"
Do
MutexLock(pdata->mutex)
While pdata->flag = False
CondWait(pdata->cond, pdata->mutex)
Wend
pdata->flag = False
MutexUnlock(pdata->mutex)
If pdata->quit = False Then
Print " thread #" & pdata->id & " is signaled"
Else
Exit Do
End If
Loop
Print " thread #" & pdata->id & " is finishing"
End Sub
Dim As Any Ptr mutex = MutexCreate()
Dim As Any Ptr cond(0 To 3) = {CondCreate(), CondCreate(), CondCreate(), CondCreate()}
Dim As ThreadData mythreads(1 To 6) = {Type(1, mutex, cond(1)), Type(2, mutex, cond(2)), Type(3, mutex, cond(3)), _
Type(4, mutex, cond(0)), Type(5, mutex, cond(0)), Type(6, mutex, cond(0))}
Print "Threads from #1 to #6 are created:"
For I As Integer = LBound(mythreads) To UBound(mythreads)
mythreads(I).handle = ThreadCreate(@ThreadData.Thread, @mythreads(I))
Next I
Sleep 1000, 1 '' 等待所有线程启动
Print
Print "----------------------------------------------------------"
Print
For I As Integer = 3 To 1 Step -1
Print "Send a CondSignal to thread #" & I &":"
MutexLock(mutex)
mythreads(I).flag = True
CondSignal(cond(I))
MutexUnlock(mutex)
Sleep 1000, 1 '' 等待线程循环完成
Print
Next I
Print "----------------------------------------------------------"
Print
Print "Send a single CondBroadcast to all threads from #4 to #6:"
MutexLock(mutex)
For I As Integer = 4 To 6
mythreads(I).flag = True
Next I
CondBroadcast(cond(0))
MutexUnlock(mutex)
Sleep 1000, 1 '' 等待所有线程循环完成
Print "Send a single CondBroadcast to all threads from #4 to #6:"
MutexLock(mutex)
For I As Integer = 4 To 6
mythreads(I).flag = True
Next I
CondBroadcast(cond(0))
MutexUnlock(mutex)
Sleep 1000, 1 '' 等待所有线程循环完成
Print "Send a single CondBroadcast to all threads from #4 to #6:"
MutexLock(mutex)
For I As Integer = 4 To 6
mythreads(I).flag = True
Next I
CondBroadcast(cond(0))
MutexUnlock(mutex)
Sleep 1000, 1 '' 等待所有线程循环完成
Print
Print "----------------------------------------------------------"
Print
Print "Send a CondSignal to any thread among #4 to #6:"
MutexLock(mutex)
For I As Integer = 4 To 6
mythreads(I).flag = True
Next I
CondSignal(cond(0))
MutexUnlock(mutex)
Sleep 1000, 1 '' 等待一个线程循环完成
Print "Send a CondSignal to any thread among #4 to #6:"
MutexLock(mutex)
For I As Integer = 4 To 6
mythreads(I).flag = True
Next I
CondSignal(cond(0))
MutexUnlock(mutex)
Sleep 1000, 1 '' 等待一个线程循环完成
Print "Send a CondSignal to any thread among #4 to #6:"
MutexLock(mutex)
For I As Integer = 4 To 6
mythreads(I).flag = True
Next I
CondSignal(cond(0))
MutexUnlock(mutex)
Sleep 1000, 1 '' 等待一个线程循环完成
Print
Print "Send a CondSignal to any thread among #4 to #6:"
MutexLock(mutex)
For I As Integer = 4 To 6
mythreads(I).flag = True
Next I
CondSignal(cond(0))
MutexUnlock(mutex)
Sleep 1000, 1 '' 等待一个线程循环完成
Print "Send a CondSignal to any thread among #4 to #6:"
MutexLock(mutex)
For I As Integer = 4 To 6
mythreads(I).flag = True
Next I
CondSignal(cond(0))
MutexUnlock(mutex)
Sleep 1000, 1 '' 等待一个线程循环完成
Print "Send a CondSignal to any thread among #4 to #6:"
MutexLock(mutex)
For I As Integer = 4 To 6
mythreads(I).flag = True
Next I
CondSignal(cond(0))
MutexUnlock(mutex)
Sleep 1000, 1 '' 等待一个线程循环完成
Print
Print "Send a CondSignal to any thread among #4 to #6:"
MutexLock(mutex)
For I As Integer = 4 To 6
mythreads(I).flag = True
Next I
CondSignal(cond(0))
MutexUnlock(mutex)
Sleep 1000, 1 '' 等待一个线程循环完成
Print "Send a CondSignal to any thread among #4 to #6:"
MutexLock(mutex)
For I As Integer = 4 To 6
mythreads(I).flag = True
Next I
CondSignal(cond(0))
MutexUnlock(mutex)
Sleep 1000, 1 '' 等待一个线程循环完成
Print "Send a CondSignal to any thread among #4 to #6:"
MutexLock(mutex)
For I As Integer = 4 To 6
mythreads(I).flag = True
Next I
CondSignal(cond(0))
MutexUnlock(mutex)
Sleep 1000, 1 '' 等待一个线程循环完成
Print
Print "----------------------------------------------------------"
Print
For I As Integer = 1 To 3
Print "Send to finish a CondSignal to thread #" & I &":"
MutexLock(mutex)
mythreads(I).flag = True
mythreads(I).quit = True
CondSignal(cond(I))
MutexUnlock(mutex)
Sleep 1000, 1 '' 等待线程循环完成
Print
Next I
Print "----------------------------------------------------------"
Print
Print "Send to finish a single CondBroadcast to all threads from #4 to #6:"
MutexLock(mutex)
For I As Integer = 4 To 6
mythreads(I).flag = True
mythreads(I).quit = True
Next I
CondBroadcast(cond(0))
MutexUnlock(mutex)
Sleep 1000, 1 '' 等待所有线程循环完成
Print
Print "----------------------------------------------------------"
Print
For I As Integer = 1 To 3
ThreadWait(mythreads(I).handle)
CondDestroy(cond(I))
Next I
For I As Integer = 4 To 6
ThreadWait(mythreads(I).handle)
Next I
Print "All threads from #1 to #6 are finished."
Print
MutexDestroy(mutex)
CondDestroy(cond(0))
Sleepend GeSHi
输出(例如):
Threads from #1 to #6 are created:
thread #1 is running
thread #3 is running
thread #2 is running
thread #5 is running
thread #4 is running
thread #6 is running
----------------------------------------------------------
Send a CondSignal to thread #3:
thread #3 is signaled
Send a CondSignal to thread #2:
thread #2 is signaled
Send a CondSignal to thread #1:
thread #1 is signaled
----------------------------------------------------------
Send a single CondBroadcast to all threads from #4 to #6:
thread #5 is signaled
thread #6 is signaled
thread #4 is signaled
Send a single CondBroadcast to all threads from #4 to #6:
thread #6 is signaled
thread #4 is signaled
thread #5 is signaled
Send a single CondBroadcast to all threads from #4 to #6:
thread #5 is signaled
thread #4 is signaled
thread #6 is signaled
----------------------------------------------------------
Send a CondSignal to any thread among #4 to #6:
thread #5 is signaled
Send a CondSignal to any thread among #4 to #6:
thread #4 is signaled
Send a CondSignal to any thread among #4 to #6:
thread #6 is signaled
Send a CondSignal to any thread among #4 to #6:
thread #5 is signaled
Send a CondSignal to any thread among #4 to #6:
thread #4 is signaled
Send a CondSignal to any thread among #4 to #6:
thread #6 is signaled
Send a CondSignal to any thread among #4 to #6:
thread #5 is signaled
Send a CondSignal to any thread among #4 to #6:
thread #4 is signaled
Send a CondSignal to any thread among #4 to #6:
thread #6 is signaled
----------------------------------------------------------
Send to finish a CondSignal to thread #1:
thread #1 is finishing
Send to finish a CondSignal to thread #2:
thread #2 is finishing
Send to finish a CondSignal to thread #3:
thread #3 is finishing
----------------------------------------------------------
Send to finish a single CondBroadcast to all threads from #4 to #6:
thread #4 is finishing
thread #5 is finishing
thread #6 is finishing
----------------------------------------------------------
All threads from #1 to #6 are finished.15. 如何优化由线程执行的连续用户任务的排序?
ThreadCreate() 返回和线程代码(线程代码的第一行)开始之间的延迟平均估计约为 50 微秒,但最坏情况下可能达到几毫秒。
(见 FAQ )
这就是为什么子线程可以只启动一次(例如通过构造函数)并执行用户任务的永久等待循环(以避免每次线程启动延迟),然后在结束时停止(通过析构函数)。
主线程和子线程之间的同步(每个用户任务的开始和用户任务完成)可以通过2个互斥锁来管理。
估计执行一系列用户任务(具有空的用户过程体)的平均时间示例:
- 要么通过连续线程启动,
- 要么通过单个线程启动。
start GeSHi
Sub userTask(ByVal p As Any Ptr) '' 要执行的任务
End Sub
Dim As Double t
'---------------------------------------------------------------------------
Print "Successive (empty) user tasks executed by one thread for each:"
t = Timer
For i As Integer = 1 To 10000
Dim As Any Ptr p = ThreadCreate(@userTask)
ThreadWait(p)
Next i
t = Timer - t
Print Using "######.### microdeconds per user task"; t * 100
Print
'---------------------------------------------------------------------------
Type thread
Public:
Dim As Sub(ByVal p As Any Ptr) task '' 指向用户任务的指针
Declare Sub Launch() '' 启动用户任务
Declare Sub Wait() '' 等待用户任务完成
Declare Constructor()
Declare Destructor()
Private:
Dim As Any Ptr mutex1
Dim As Any Ptr mutex2
Dim As Any Ptr handle
Dim As Boolean quit
Declare Static Sub proc(ByVal pthread As thread Ptr)
End Type
Constructor thread()
This.mutex1 = MutexCreate
This.mutex2 = MutexCreate
MutexLock(This.mutex1)
MutexLock(This.mutex2)
This.handle = ThreadCreate(CPtr(Any Ptr, @thread.proc), @This)
End Constructor
Destructor thread()
This.quit = True
MutexUnlock(This.mutex1)
ThreadWait(This.handle)
MutexDestroy(This.mutex1)
MutexDestroy(This.mutex2)
End Destructor
Sub thread.proc(ByVal pthread As thread Ptr)
Do
MutexLock(pthread->mutex1) '' 等待启动任务
If pthread->quit = True Then Exit Sub
pthread->task(pthread)
MutexUnlock(pthread->mutex2) '' 任务完成
Loop
End Sub
Sub thread.Launch()
MutexUnlock(This.mutex1)
End Sub
Sub thread.Wait()
MutexLock(This.mutex2)
End Sub
Print "Successive (empty) user tasks executed by a single thread for all:"
t = Timer
Dim As thread Ptr pThread = New Thread
pThread->task = @userTask
For i As Integer = 1 To 10000
pThread->Launch()
pThread->Wait()
Next i
Delete pThread
t = Timer - t
Print Using "######.### microdeconds per user task"; t * 100
Print
Sleepend GeSHi
输出(例如):
Successive (empty) user tasks executed by one thread for each:
145.004 microdeconds per user task
Successive (empty) user tasks executed by a single thread for all:
6.691 microdeconds per user task16. 为什么多线程性能会受到许多共享内存访问(尤其是写入模式)的影响?
每个核心都有自己的缓存内存,可以缓冲共享内存的有用数据(用于读取和写入)。
因此,在核心之间执行缓存一致性算法,以保持(在写入缓存的情况下,以及对于缓存之间公共的内存区域)所有相关缓存中最新的值。
正是这个算法在多线程情况下,在共享内存的多次访问中损害了性能,尤其是在写入模式下。
因此,必须尽可能限制线程对共享内存的访问,尤其是在写入时。
例如,线程的所有中间结果都可以在本地内存中执行,只有最终有用的结果才放入共享内存。
成员线程过程的示例,计算前N个整数的和,通过直接在共享内存中累积(SumUpTo_1())或在其本地内存中内部累积然后复制回去(SumUpTo_2()):
start GeSHi
Type Thread
Dim As UInteger valueIN
Dim As Double valueOUT
Dim As Any Ptr pHandle
Declare Static Sub SumUpTo_1(ByVal pt As Thread Ptr)
Declare Static Sub SumUpTo_2(ByVal pt As Thread Ptr)
End Type
Sub Thread.SumUpTo_1(ByVal pt As Thread Ptr)
pt->valueOut = 0
For I As UInteger = 1 To pt->valueIN
pt->valueOUT += I
Next I
End Sub
Sub Thread.SumUpTo_2(ByVal pt As Thread Ptr)
Dim As Double value = 0
For I As UInteger = 1 To pt->valueIN
value += I
Next I
pt->valueOUT = value
End Sub
Sub MyThreads(ByVal pThread As Any Ptr, ByVal threadNB As UInteger = 1)
Dim As Thread td(1 To threadNB)
Dim As Double t
t = Timer
For i As Integer = 1 To threadNB
td(i).valueIN = 100000000 + i
td(i).pHandle = ThreadCreate(pThread, @td(i))
Next I
For i As Integer = 1 To threadNB
ThreadWait(td(i).pHandle)
Next I
t = Timer - t
For i As Integer = 1 To threadNB
Print " SumUpTo(" & td(i).valueIN & ") = " & td(i).valueOUT, _
"(right result : " & (100000000# + i) * (100000000# + i + 1) / 2 & ")"
Next I
Print " total time : " & t & " s"
Print
End Sub
Print
For i As Integer = 1 To 4
Print "Each thread (in parallel) accumulating result directly in shared memory:"
Mythreads(@Thread.SumUpTo_1, I)
Print "Each thread (in parallel) accumulating result internally in its local memory:"
Mythreads(@Thread.SumUpTo_2, I)
Print "-----------------------------------------------------------------------------"
Print
Next i
Sleepend GeSHi
输出(示例):
Each thread (in parallel) accumulating result directly in shared memory:
SumUpTo(100000001) = 5000000150000001 (right result : 5000000150000001)
total time : 1.668927300015184 s
Each thread (in parallel) accumulating result internally in its local memory:
SumUpTo(100000001) = 5000000150000001 (right result : 5000000150000001)
total time : 1.004467599958389 s
-----------------------------------------------------------------------------
Each thread (in parallel) accumulating result directly in shared memory:
SumUpTo(100000001) = 5000000150000001 (right result : 5000000150000001)
SumUpTo(100000002) = 5000000250000003 (right result : 5000000250000003)
total time : 4.314032700025791 s
Each thread (in parallel) accumulating result internally in its local memory:
SumUpTo(100000001) = 5000000150000001 (right result : 5000000150000001)
SumUpTo(100000002) = 5000000250000003 (right result : 5000000250000003)
total time : 1.032165899962706 s
-----------------------------------------------------------------------------
Each thread (in parallel) accumulating result directly in shared memory:
SumUpTo(100000001) = 5000000150000001 (right result : 5000000150000001)
SumUpTo(100000002) = 5000000250000003 (right result : 5000000250000003)
SumUpTo(100000003) = 5000000350000006 (right result : 5000000350000006)
total time : 6.727616399944395 s
Each thread (in parallel) accumulating result internally in its local memory:
SumUpTo(100000001) = 5000000150000001 (right result : 5000000150000001)
SumUpTo(100000002) = 5000000250000003 (right result : 5000000250000003)
SumUpTo(100000003) = 5000000350000006 (right result : 5000000350000006)
total time : 1.128656100041894 s
-----------------------------------------------------------------------------
Each thread (in parallel) accumulating result directly in shared memory:
SumUpTo(100000001) = 5000000150000001 (right result : 5000000150000001)
SumUpTo(100000002) = 5000000250000003 (right result : 5000000250000003)
SumUpTo(100000003) = 5000000350000006 (right result : 5000000350000006)
SumUpTo(100000004) = 5000000450000010 (right result : 5000000450000010)
total time : 6.829728199980309 s
Each thread (in parallel) accumulating result internally in its local memory:
SumUpTo(100000001) = 5000000150000001 (right result : 5000000150000001)
SumUpTo(100000002) = 5000000250000003 (right result : 5000000250000003)
SumUpTo(100000003) = 5000000350000006 (right result : 5000000350000006)
SumUpTo(100000004) = 5000000450000010 (right result : 5000000450000010)
total time : 1.164915200012842 s
-----------------------------------------------------------------------------可以验证,多线程性能受到许多共享内存(尤其是在写入模式下)访问的严重影响:
对于线程在共享内存中累积结果的情况,多线程不再带来任何增益(甚至略有损失),而对于线程在内部内存中累积结果的情况,增益几乎达到了理论最大值。
另一方面,我们观察到,在只读模式下访问共享内存时,多线程性能的退化较小。
17. 为什么多线程性能会因大量操作变长字符串和变长数组而受到严重影响?
对于所有类似变长字符串和变长数组这样的伪对象,只有描述符可以放入本地内存,而数据本身总是在堆上(只有固定长度的数据可以放入本地内存)。
堆是共享内存,这会导致多线程性能受损,如上文FAQ所述。
(参见FAQ )
对于变长数组,可以使用本地固定长度数组替代,因为这种数组数据始终放置在本地作用域内存中。
你不仅需要为每个数组定义一个要分配的最大固定大小,而且对于每一个,都需要关联一个(如果需要,每个维度一个)索引变量,该变量指向最后一个有用的元素('Redim' 被替换为更新此索引变量)。
对于变长字符串,可以使用本地固定长度[z]字符串替代,因为这些[z]字符串数据始终放置在本地作用域内存中。
所有内置的字符串函数(除了 Len() 和 Asc())以及所有字符串运算符也不应该用于[z]字符串,因为它们在内部使用变长字符串。相反,请使用仅对[z]字符串索引进行操作的用户代码。
注意:
但是固定长度字符串(Dim As String _ N)不如固定长度z字符串(Dim As Zstring _ N)方便使用,因为前者不能通过引用传递给过程(只能通过拷贝),而后者可以(Byref As Zstring)。
此外,所有动态内存分配/重新分配/释放请求(为了线程安全)都在内部通过互斥锁锁定和解锁进行序列化。
以下示例比较了两种类型代码的多线程性能:
- 使用其内置函数和运算符(如
= (赋值)、Instr()、Mid()和Ucase())的变长字符串代码, - 具有等效于先前内置函数和运算符的用户代码的固定长度z字符串代码,但仅对z字符串索引进行操作。
(仅使用 Asc() 和 Len() 是因为它们对性能没有影响)
start GeSHi
Type Thread
Dim As UInteger value
Dim As Any Ptr pHandle
Declare Static Sub thread1(ByVal pt As Thread Ptr)
Declare Static Sub thread2(ByVal pt As Thread Ptr)
End Type
Sub Thread.thread1(ByVal pt As Thread Ptr)
Dim As Integer result
For n As Integer = 1 To pt->value
Dim As String s1
Dim As String s2
Dim As String s3
s1 = "FreeBASIC rev 1.20"
result = InStr(s1, "rev")
s2 = Mid(s1, result)
s3 = UCase(s2)
Next n
End Sub
Sub Thread.thread2(ByVal pt As Thread Ptr)
Dim As Integer result
For n As Integer = 1 To pt->value
Dim As ZString * 256 z1
Dim As ZString * 256 z2
Dim As ZString * 256 z3
' 替代: z1 = "FreeBASIC rev 1.20"
For i As Integer = 0 To Len("FreeBASIC rev 1.20")
z1[i] = ("FreeBASIC rev 1.20")[i]
Next i
' 替代: result = Instr(z1, "rev")
result = 0
For i As Integer = 0 To Len(z1) - Len("rev")
For j As Integer = 0 To Len("rev") - 1
If z1[i + j] <> ("rev")[j] Then Continue For, For
Next j
result = i + 1
Exit For
Next i
' 替代: z2 = Mid(z1, result)
For i As Integer = result - 1 To Len(z1)
z2[i - result + 1] = z1[i]
Next i
' 替代: z3 = Ucase(z2)
For i As Integer = 0 To Len(z2)
z3[i] = z2[i]
If z3[i] >= Asc("a") AndAlso z3[i] <= Asc("z") Then z3[i] -= 32
Next i
Next n
End Sub
Sub MyThreads(ByVal pThread As Any Ptr, ByVal threadNB As UInteger = 1)
Dim As Thread td(1 To threadNB)
Dim As Double t
t = Timer
For i As Integer = 1 To threadNB
td(i).value = 100000
td(i).pHandle = ThreadCreate(pThread, @td(i))
Next I
For i As Integer = 1 To threadNB
ThreadWait(td(i).pHandle)
Next I
t = Timer - t
Print " total time for " & threadNB & " threads in parallel: " & t & " s"
Print
End Sub
Print
For i As Integer = 1 To 8
Print "Each thread using var-len strings, with its built-in functions and operators:"
Mythreads(@Thread.thread1, I)
Print "Each thread using fix-len zstrings, with user code working on zstring indexes:"
Mythreads(@Thread.thread2, I)
Print "------------------------------------------------------------------------------"
Print
Next i
Sleepend GeSHi
输出(示例):
Each thread using var-len strings, with its built-in functions and operators:
total time for 1 threads in parallel: 0.08449090004432946 s
Each thread using fix-len zstrings, with user code working on zstring indexes:
total time for 1 threads in parallel: 0.02201449999120086 s
------------------------------------------------------------------------------
Each thread using var-len strings, with its built-in functions and operators:
total time for 2 threads in parallel: 0.1947050000308082 s
Each thread using fix-len zstrings, with user code working on zstring indexes:
total time for 2 threads in parallel: 0.02090729994233698 s
------------------------------------------------------------------------------
Each thread using var-len strings, with its built-in functions and operators:
total time for 3 threads in parallel: 0.3338784999214113 s
Each thread using fix-len zstrings, with user code working on zstring indexes:
total time for 3 threads in parallel: 0.0279372000368312 s
------------------------------------------------------------------------------
Each thread using var-len strings, with its built-in functions and operators:
total time for 4 threads in parallel: 0.4927077000029385 s
Each thread using fix-len zstrings, with user code working on zstring indexes:
total time for 4 threads in parallel: 0.02361949998885393 s
------------------------------------------------------------------------------
Each thread using var-len strings, with its built-in functions and operators:
total time for 5 threads in parallel: 0.7089884000597522 s
Each thread using fix-len zstrings, with user code working on zstring indexes:
total time for 5 threads in parallel: 0.02638950000982732 s
------------------------------------------------------------------------------
Each thread using var-len strings, with its built-in functions and operators:
total time for 6 threads in parallel: 0.9172402999829501 s
Each thread using fix-len zstrings, with user code working on zstring indexes:
total time for 6 threads in parallel: 0.0310587000567466 s
------------------------------------------------------------------------------
Each thread using var-len strings, with its built-in functions and operators:
total time for 7 threads in parallel: 1.159198799985461 s
Each thread using fix-len zstrings, with user code working on zstring indexes:
total time for 7 threads in parallel: 0.02898070006631315 s
------------------------------------------------------------------------------
Each thread using var-len strings, with its built-in functions and operators:
total time for 8 threads in parallel: 1.403980100061744 s
Each thread using fix-len zstrings, with user code working on zstring indexes:
total time for 8 threads in parallel: 0.03312029992230237 s
------------------------------------------------------------------------------可以验证,多线程性能受到大量变长字符串操作的严重影响:
对于线程使用变长字符串及其内置函数和运算符的情况,多线程不再带来任何增益(甚至损失),而对于线程使用固定长度z字符串和仅对z字符串索引进行操作的用户代码(除了使用 Asc() 和 Len())的情况,增益几乎达到了理论最大值。
另请参阅
- ProPgMultiThreading.md
- ProPgMtThreads.md
- ProPgMtMutualExclusion.md
- ProPgMtConditionalVariables.md
- ProPgMtCriticalSections.md
- ProPgEmulateTlsTp.md