Skip to content

线程 (Threads)


用于创建和分离/等待线程的内置过程。

前言

程序开始执行时,已有一个隐式线程在运行(这本身就是一个完整的线程)。

这个"主"线程执行程序的主函数。

同一程序可以显式启动其他线程,这些线程将以竞争方式运行(线程之间以及与主线程之间均相互竞争)。

所有线程(包括主线程)共享同一内存,因此可以访问相同的全局变量、相同的堆内存、相同的文件描述符集等。

所有这些线程并行执行(即使用时间片,或者如果系统有多个处理器/核心,则真正并行执行)。

创建线程

创建线程有两种方法:

  • "经典"方法 Threadcreate:在独立执行线程中启动特定的用户定义子程序类型(该子程序有且仅有一个 'Any Ptr' 类型指针参数),此方法 100% 安全。

  • "特殊"方法 Threadcall:应能在独立执行线程中启动任意用户定义子程序类型(可以有几乎任意数量和类型的参数),但目前此方法存在 bug。

经典方法(100% 安全)— Threadcreate

  • 语法:

Declare Function Threadcreate ( ByVal procptr As Sub ( ByVal userdata As Any Ptr ), ByVal param As Any Ptr = 0, ByVal stack_size As Integer = 0 ) As Any Ptr

  • 用法:

threadid = Threadcreate ( procptr [, [ param ] [, stack_size ] ] )

  • 参数:

procptr

指向用作线程的 Sub 的指针。该子程序必须具有以下签名(相同参数、相同调用约定)才能与 procptr 兼容:

Declare Sub myThread ( ByVal userdata As Any Ptr )

userdata

用作线程的 SubAny Ptr 参数。FreeBASIC 要求此参数必须存在,不可省略!

param

将通过 userdata 参数传递给 procptr 所指向线程 SubAny Ptr 参数。例如,这可以是指向结构体或数组的指针,包含线程子程序所需的各种信息。如果未提供 param,则将 0(零)传递给线程子程序的 userdata 参数。

stack_size

为此线程栈保留的可选字节数。

  • 返回值:

已创建线程的 any ptr 句柄 (threadid),失败时返回空指针(0)。

注意:

  • userdata 参数可以在 myThread 子程序体中不使用,但在头部将其声明为 Any Ptr 参数始终是强制要求的。在这种情况下,调用 Threadcreate 时可以省略对应的 param 参数,或者仍然传递一个无意义的参数(通常使用 '0',因为该值直接与任意指针兼容)。参见第1和第2个示例。

  • 如果需要向 myThread 传递数据,Any Ptr 类型的 param 可用于引用这些数据,通常需要在将其传递给 Threadcreate 之前进行(隐式或显式的)类型转换为 Any Ptr,以及在 myThread 体中使用前进行反向类型转换。参见第3个示例。

特殊方法(存在 bug)— Threadcall

  • 语法:

Declare function Threadcall subname ( [paramlist] ) as any ptr

  • 用法:

threadid = Threadcall subname ( [paramlist] )

  • 参数:

subname

子程序的名称

paramlist

传递给子程序的参数列表,与普通子程序调用相同。

  • 返回值:

已创建线程的 any ptr 句柄 (threadid),失败时返回空指针(0)。

警告:

目前,当 Threadcall 涉及向线程传递参数时,无法保证在 Threadcall 语句结束后到线程真正启动之前,对应数据仍然有效。这可能导致异常行为。

因此,目前更建议使用 Threadcreate(100% 安全)来代替。

说明

可以从同一个 Sub 创建多个不同的线程,通过传递不同参数来定义每个线程的行为。

Threadcreate/Threadcall 语句执行结束与线程实际启动之间可能有较长时间。因此,Threadcreate/Threadcall 语句之后的某些语句可能在线程实际启动之前执行。

反之,线程体也可能在 Threadcreate/Threadcall 返回之前开始执行。

不同线程的执行顺序没有保证,也无法对多个已创建线程实际开始执行的顺序做任何假设(Linux 除外)。

默认情况下,线程总是以"可连接"(joinable)状态创建,即其句柄可通过 'threadid' 标识符访问。

如果线程以此状态(可连接)结束,分配给它的资源不会自动释放(只有在主线程终止时才会释放)。

因此,良好的编程习惯是始终使用以下两种方法之一来正确结束线程(参见下面的段落):

  • 等待线程结束,

  • 或者分离线程(线程变为不可连接)。

每个正在运行的线程都可以通过其唯一的句柄进行标识。

创建新线程时,创建函数会返回该线程的句柄。

当线程运行代码时,ThreadSelf(从 fbc 版本 1.08 起)也允许返回线程的句柄(隐式主线程也有其自己的唯一句柄)。

ThreadSelf 可用于基于每个线程(包括隐式主线程)的唯一句柄来实现某种 TLS(线程本地存储)。因此,可以定义一个相同的全局变量名,但为访问它的线程存储特定值。这允许编写通用过程,但参数依赖于执行它的线程。

等待线程结束或分离线程

有两种方法可以触发正确的线程终止:

  • 方法一 ThreadWait:另一个线程等待此线程完成,

  • 方法二 ThreadDetach:另一个线程分离此线程并继续执行。

方法一 — ThreadWait

  • 语法:

Declare Sub ThreadWait ( Byval threadid As Any Ptr )

  • 用法:

ThreadWait ( threadid )

  • 参数:

threadid

ThreadCreateThreadCall 创建的线程的 Any Ptr 句柄

  • 注意:

在其他语言(如 C++)中,'wait()' 后缀称为 'join()'

方法二 — ThreadDetach

  • 语法:

Declare Sub ThreadDetach ( Byval threadid As Any Ptr )

  • 用法:

#include "fbthread.bi"

ThreadDetach ( threadid )

  • 参数:

threadid

ThreadCreateThreadCall 创建的线程的 Any Ptr 句柄

说明

创建线程后,程序员必须确保线程被等待(连接)或被分离,且必须从另一个线程(包括主线程)执行此操作。

ThreadWait 等待线程完成执行,然后释放与线程句柄相关联的资源。ThreadWait 在(由标识符指定的)线程结束之前不会返回。

等待期间,调用方不消耗 CPU 时间。

ThreadWait 不会强制线程结束。如果线程需要一个信号来强制结束,必须使用共享标志等机制。

ThreadDetach 释放与线程句柄相关联的资源。ThreadDetach 将销毁线程句柄,之后不能再使用。

ThreadWait 不同,ThreadDetach 不等待线程结束,其执行独立继续。线程完成后,所有分配的资源将被释放。

ThreadWaitThreadDetach 执行后,线程不再可连接,因此句柄标识符的值不得再次用于这两个命令中的任何一个。

通常,在结束之前,"父"线程会等待"子"线程完成。

但如果程序员选择不等待线程结束(仅分离线程),则必须确保该线程访问的数据在线程完成使用之前始终有效。否则,可能会遇到"父"线程持有局部变量的指针/引用,但"子"线程在"父"线程结束时尚未完成(变量因超出作用域而被销毁)的情况。

示例

  • 证明多线程能力的小示例:
  • "Main"线程显示十个 "M" 字符,同时"Child"线程并行显示十个 "C" 字符。

在每个线程(主线程和子线程)的 For 循环中放置一个 'Sleep x, 1' 时间片,以释放时间片允许其他线程执行。

设置时间片使子线程 For 循环的执行时间大于主线程 For 循环的执行时间。

  • 使用 Threadcreate ..... ThreadWait

start GeSHi

vb
Declare Sub thread (ByVal userdata As Any Ptr)

Dim As Any Ptr threadID  '' 声明子线程的 'Any Ptr' 线程ID

Print """M"": from 'Main' thread"
Print """C"": from 'Child' thread"
Print

threadID = ThreadCreate(@thread)  '' 从主线程创建子线程

For I As Integer = 1 To 10  '' 主线程的 'For' 循环
    Print "M";
    Sleep 150, 1
Next I

ThreadWait(threadID)  '' 等待子线程结束
Print
Print "'Child' thread finished"

Sleep

Sub thread (ByVal userdata As Any Ptr)  '' 由子线程执行的子程序
    For I As Integer = 1 To 10          '' 子线程的 'For' 循环
        Print "C";
        Sleep 350, 1
    Next I
End Sub

end GeSHi

输出示例:

vb
"M": from 'Main' thread
"C": from 'Child' thread

MCMMCMMCMMCMMMCCCCCC
'Child' thread finished
  • 使用 Threadcreate + ThreadDetach ..... (在子线程结束时添加全局结束标志):

start GeSHi

vb
#include "fbthread.bi"

Declare Sub thread (ByVal userdata As Any Ptr)

Dim As Any Ptr threadID          '' 声明子线程的 'Any Ptr' 线程ID
Dim Shared As Boolean threadEnd  '' 声明子线程的全局 'Boolean' 结束标志

Print """M"": from 'Main' thread"
Print """C"": from 'Child' thread"
Print

threadID = ThreadCreate(@thread)  '' 从主线程创建子线程
Threaddetach(threadID)            '' 分离子线程

For I As Integer = 1 To 10  '' 主线程的 'For' 循环
    Print "M";
    Sleep 150, 1
Next I

While threadEnd = False  '' 等待子线程的结束标志变为 'True'
Wend
Print
Print "'Child' thread finishing or finished"

Sleep

Sub thread (ByVal userdata As Any Ptr)  '' 由子线程执行的子程序
    For I As Integer = 1 To 10          '' 子线程的 'For' 循环
        Print "C";
        Sleep 350, 1
    Next I
    threadEnd = True                    '' 将结束标志设为 'True'
End Sub

end GeSHi

输出示例:

vb
"M": from 'Main' thread
"C": from 'Child' thread

MCMMCMMCMMCMMMCCCCCC
'Child' thread finishing or finished
  • 多定时器功能的 UDT:

用户事件由定时器循环中的分离线程回调触发,请求的超时时间仅受 Threadcreate + ThreadDetach 的执行时间(小且基本恒定)的影响,而不受 ThreadWait 等待时间的影响:

start GeSHi

vb
'    仅有4个公共访问成员过程(前3个成功时返回 'true',否则返回 'false'):
'        - 函数 'Set' 用于参数化指定定时器(超时时间,单位ms;指向用户线程的指针)
'        - 函数 'Start' 用于启动指定定时器
'        - 函数 'Stop' 用于停止指定定时器(之后可以重新 Set 和 Start)
'        - 属性 'Counter' 用于获取定时器触发次数
'    另有一个公共访问的 'Any Ptr':
'        - 指针字段 'userdata' 指向任意用户数据结构(可选使用)
'
'    备注:
'        - 向用户线程过程提供所考虑定时器实例的指针,
'          以便能够对每组定时器进行统一处理,
'          并在使用时寻址正确的用户数据结构(用法见示例)。
'
'    私有访问:
'        - 4个内部变量(超时值、指向用户线程的指针、定时器线程句柄、触发次数计数器)
'        - 静态定时器线程

#include "fbthread.bi"
Type UDT_timer_thread
    Public:
        Declare Function Set (ByVal time_out As UInteger, _
                              ByVal timer_procedure As Sub(ByVal param As Any Ptr)) _
                              As Boolean
        Declare Function Start () As Boolean
        Declare Function Stop () As Boolean
        Declare Property Counter () As UInteger
        Dim As Any Ptr userdata
    Private:
        Dim As UInteger tempo
        Dim As Sub(ByVal param As Any Ptr) routine
        Dim As Any Ptr handle
        Dim As UInteger count
        Declare Static Sub thread (ByVal param As Any Ptr)
End Type

Function UDT_timer_thread.Set (ByVal time_out As UInteger, _
                              ByVal timer_procedure As Sub(ByVal param As Any Ptr)) _
                              As Boolean
    If timer_procedure > 0 And This.handle = 0 Then
        This.tempo = time_out
        This.routine = timer_procedure
        This.count = 0
        Function = True
    Else
        Function = False
    End If
End Function

Function UDT_timer_thread.Start () As Boolean
    If This.handle = 0 And This.routine > 0 Then
        This.handle = ThreadCreate(@UDT_timer_thread.thread, @This)
        Function = True
    Else
        Function = False
    End If
End Function

Function UDT_timer_thread.Stop () As Boolean
    If This.handle > 0 Then
        Dim p As Any Ptr = 0
        Swap p, This.handle
        ThreadWait(p)
        Function = True
    Else
        Function = False
    End If
End Function

Property UDT_timer_thread.Counter () As UInteger
    Return This.count
End Property

Static Sub UDT_timer_thread.thread (ByVal param As Any Ptr)
    Dim As UDT_timer_thread Ptr pu = param
    While pu->handle > 0
        Sleep pu->tempo, 1
        pu->count += 1
        If pu->routine > 0 Then
            Dim As Any Ptr p = ThreadCreate(Cast(Any Ptr, pu->routine), param)
            Threaddetach(p)
        End If
    Wend
End Sub

'---------------------------------------------------------------------------------------------------

Dim As UInteger tempo1 = 950
Dim As UInteger tempo2 = 380
Dim As UDT_timer_thread timer1
    timer1.userdata = New String("        callback from timer #1 (" & tempo1 & "ms)")
Dim As UDT_timer_thread timer2
    timer2.userdata = New String("        callback from timer #2 (" & tempo2 & "ms)")

Sub User_thread (ByVal param As Any Ptr)
    Dim As UDT_timer_thread Ptr pu = param
    Dim As String Ptr ps = pu->userdata
    Print *ps & ", occurrence: " & pu->Counter
End Sub

Print "Beginning of test"
If timer1.Set(tempo1, @User_thread) Then
    Print "    timer #1 set OK"
    If timer1.Start Then
        Print "        timer #1 start OK"
    End If
End If
If timer2.Set(tempo2, @User_thread) Then
    Print "    timer #2 set OK"
    If timer2.Start Then
        Print "        timer #2 start OK"
    End If
End If
Print "    Then, any key to stop the timers"

Sleep

If timer1.Stop Then
    Print "    timer #1 stop OK"
End If
If timer2.Stop Then
    Print "    timer #2 stop OK"
End If
Sleep 500, 1
Print "End of test"
Delete Cast(String Ptr, timer1.userdata)
Delete Cast(String Ptr, timer2.userdata)

Sleep

end GeSHi

输出示例:

vb
Beginning of test
timer #1 set OK
timer #1 start OK
timer #2 set OK
timer #2 start OK
Then, any key to stop the timers
callback from timer #2 (380ms), occurrence: 1
callback from timer #2 (380ms), occurrence: 2
callback from timer #1 (950ms), occurrence: 1
callback from timer #2 (380ms), occurrence: 3
callback from timer #2 (380ms), occurrence: 4
callback from timer #1 (950ms), occurrence: 2
callback from timer #2 (380ms), occurrence: 5
callback from timer #2 (380ms), occurrence: 6
callback from timer #2 (380ms), occurrence: 7
callback from timer #1 (950ms), occurrence: 3
callback from timer #2 (380ms), occurrence: 8
callback from timer #2 (380ms), occurrence: 9
callback from timer #1 (950ms), occurrence: 4
callback from timer #2 (380ms), occurrence: 10
callback from timer #2 (380ms), occurrence: 11
callback from timer #2 (380ms), occurrence: 12
timer #1 stop OK
callback from timer #1 (950ms), occurrence: 5
timer #2 stop OK
callback from timer #2 (380ms), occurrence: 13
End of test

参见

返回 目录

基于 FreeBASIC 官方文档翻译 如有侵权请联系我们删除
FreeBASIC 是开源项目,与微软公司无隶属关系