Thread(线程) API

Thread(线程) API为OpenOS提供了协程的一种变体。线程比基础的协程在很多方面都更高级,并且对很多工作流程而言都更易用。OpenOS线程是自主的,非阻塞的,可分离的进程。

  • 自主性:线程在创建后会立即开始异步执行,无需调用resume。与协程不同,线程在yield后会自恢复,且不会回到创建线程的代码行。使用协程进行编程基于用户显式进行的协作yield和resume调用。然而,在线程中yield或调用event.pull,只会暂时阻塞线程,线程将自行继续运行。
  • 非阻塞性:线程可以调用computer.pullSignal(或其更高级别的包装,如event.pullio.pull等),而不会阻塞主内核进程或任何其他线程。当然线程本身会被阻塞,直到出现信号或超时。线程的计算流程与在命令行中运行相同的代码一样。在幕后,thread(线程)库使用pullSignal在线程之间切换,并在适当的时候唤醒线程。这与协程完全不同,在协程中,computer.pullSignal会阻塞系统上的所有其他活动,直到出现信号或超时。
  • 可分离性:默认情况下,线程由创建它们的进程拥有(注意线程本身就是一个进程)。拥有线程的进程被称为其父进程。父进程关闭(即执行完毕)时会关闭所有正在运行的子线程(将子线程join到父进程)。但是在以下情况下,进程不必关闭线程:
    • 线程从父进程上分离。参见t:detach()。请注意线程也可以附着于其他父进程,参见t:attach()
    • 父进程抛出了异常或调用了os.exit,此时所有附着在上面的线程都会被杀死。请注意,重启也会杀死所有线程。
    • 线程被杀死,参见t:kill()
    • 线程被手动暂停,参见t:suspend()

独立事件注册

每个线程都维护了一套独立的事件注册机制;不继承也不分享。线程中创建的所有事件注册记录(即侦听器或定时器)都只从属于此线程。

  • 线程结束时,其所有事件注册记录都会随之结束。
  • suspended(暂停的)线程会忽略事件(参见t:status() suspended
  • 一个线程不可以访问/移除/交互其他线程的事件注册记录。
  • 被推送的事件对系统中的所有运行中线程可见。
  • 两个独立的线程可以对同一个事件event.pull,每个线程都会独立观测到此事件。

概述

线程主要有两种用途是其他选择无法提供:

  • 你需要编写一个函数,其中进行了阻塞式调用,但不阻塞程序其它部分的运行。
  • 你需要编写长期运行的后台函数,而不想手动管理其yield与resum。

函数

此处定义了两类API。

  1. 线程API,或者说静态函数,由require("thread")提供。
  2. 线程句柄API,或者说适用于thread.create创建的线程对象的api。在此文档中这些线程句柄将会仅以t表示。

线程API

  • thread.create(thread_proc: function[, ...]): table

    开启一个执行函数thread_proc的新线程,并返回其线程句柄,参见线程句柄API。此方法可接收可选的...参数,这些参数将会被传递给thread_proc。 线程将自动开始运行。

snippet.lua
local thread = require("thread")
print("Main program start")
thread.create(function(a, b)
  print(a)
  os.sleep()
  print(b)
end, 'A', 'B')
print("Main program end")

输出:

Main program start
A
Main program end
B
  • thread.waitForAll(threads: table[, timeout: number]): boolean, string

    等待给出的threads全部执行完成。此阻塞式操作可以在给出的timeout秒超时时间后返回,若给出此值的话。返回值为是否成功,若失败还会返回报错信息。线程可以在多种条件下被认定为”执行完成“,详见t:join()

snippet.lua
local thread = require("thread")
print("Main program start")
local t = thread.create(function(a, b)
  print(a)
  os.sleep()
  print(b)
end, 'A', 'B')
thread.waitForAll({t})
print("Main program end")

输出:

Main program start
A
B
Main program end
  • thread.waitForAny(threads: table[, timeout: number): boolean, string

    等待给出线程中的某一个完成,其他方面thread.waitForAll()一致。

snippet.lua
local thread = require("thread")
print("Main program start")
local t1 = thread.create(function(a, b)
  print(a)
  os.sleep()
  print(b)
end, 'A', 'B')
local t2 = thread.create(function(c, d)
  print(c)
  os.sleep()
  os.sleep()
  print(d)
end, 'C', 'D')
thread.waitForAny({t1, t2})
print("Main program end")

输出:

Main program start
A
C
B
Main program end
D

请注意线程恢复的顺序并未指定,此样例中也有可能在输出“Main program end”之前就输出“D”。

  • thread.current(): table

    返回当前线程对象t。init进程不代表任何线程,此函数在init进程内而且不在任何线程内调用时不会有返回值。

线程句柄API

  • t:resume(): boolean, string

    恢复(或者说解冻)一个暂停的线程。返回操作是否成功,若失败额外返回一条报错信息。线程在创建时直接就是运行状态,因此在基本的线程工作流程中完全用不到调用t:resume()。“运行中”的线程会自动继续,直到执行完毕。只有在需要恢复被暂停的(t:suspend())线程时才需要t:resume()请注意,因为你不是直接恢复线程,线程抛出的一切异常都会被线程运行库吸收,不会暴露给你的进程。

    • 目前还无法为线程挂载异常处理函数,但是现在可以用event.onError将报错信息输出到“/tmp/event.log”。请注意目前硬中断异常只会抛出一次,并且在抛出硬中断时,带线程的进程执行的行为仍未指定。目前,任何一个线程或父进程都可能接收到异常。这些细节并不是线程规范的一部分,任何实现细节的部分都可能在以后发生变化。
  • t:suspend(): boolean, string

    暂停(或者说冻结)一个运行中的线程。返回操作是否成功,若失败额外返回一条报错信息。“暂停的”线程不会自动唤醒,也不会随其父进程(若附有的话)结束而结束。暂停的线程会忽略出现的事件,代表此线程中定义的任何事件侦听器和定时器都不会响应事件通知。请注意线程不会缓存事件信号,暂停的线程可能会错失其等待的信号。例如,假设一个线程最近一次使用了event.pull("modem_message")等待信号,并且被“暂停”了,此时电脑收到了一条“modem_message”。这样线程将错失事件,永远不知道发生了什么。请注意如果你暂停了一条正在被阻塞以等待事件的线程,线程下次被唤醒时会接收到什么事件是未指定的。
    暂停当前线程就会导致线程立刻yield,在其他地方显式调用t:resume()之前也不会恢复。

t:resumet:suspend的特别注释

不要把这两个方法当作coroutine.resume()coroutine.yield()。这两个方法是间接的,而且线程是独自异步开始或停止运行的。请将这一特性与协程方法相对比,协程方法是直接的,而且会立即唤起或离开协程的执行。参考这些样例:

snippet.lua
local thread = require("thread")
local t -- 此样例需要t的upvalue
t = thread.create(function()
  print("start")
  thread.current():suspend()
  print("after suspend")
  os.sleep()
  print("after sleep")
end)

输出:

start
snippet.lua
local thread = require("thread")
local t -- this example needs an upvalue to t
t = thread.create(function()
  print("start")
  thread.current():suspend()
  print("after suspend")
  os.sleep()
  print("after sleep")
end)
print("outside thread create")
t:resume()
print("after resume")

输出:

start
outside thread create
after suspend
after resume
after sleep
  • t:kill()

    给它一刀!将线程杀死。线程将会被终止,不能再继续执行其线程函数。它的所有事件注册记录也都会随之结束。请注意核心底层实际上是Lua协程,协程不是抢占式线程。因此线程的结束点是可知的,意味着你可以预测线程结束的准确位置。参考此样例:

snippet.lua
local thread = require("thread")
local t = thread.create(function()
  while true do
    print("running")
    print("still running")
    os.sleep()
    print("after sleep")
  end
  print("unreachable code")
end)
print("before kill")
t:kill()
print("after kill")

输出:

running
still running
before kill
after kill
  • t:status(): string

    以字符串形式返回线程状态。

    • “running” 运行

      运行中的线程会在yield和阻塞式调用后继续运行(自动重新开始活动),直到其线程函数退出。这是创建后线程的初始与默认状态。线程就算在阻塞或不活动的状态下也会保持“运行”状态。运行中的线程可以被暂停(t:suspend())或杀死(t:kill()),但是不可以恢复(t:resume())。运行中的线程将会阻塞t:join()的调用,也会阻塞其父进程的关闭。与协程不同,协程在未执行时即为“暂停”状态,而线程就算在等待事件时也是“运行”状态。

    • “suspended” 暂停

      被暂停的线程会保持暂停状态,永远不会自动恢复执行其线程函数。暂停的线程会在其附着的父进程关闭时或你试图t:join()它时自动被杀死。暂停的线程会忽略事件信号,此线程的上下文所创建的,或其创建的任何子线程所创建的事件注册记录,也都会忽略事件信号。暂停线程的子线程表现会与暂停一样,即使其状态为“运行”。暂停的线程可以恢复(t:resume())或者杀死(t:kill()),但不可以暂停(t:suspend())。

    • “dead” 死亡

      死亡的线程是结束了或放弃了自己的运行或被终止的线程。它不可被恢复(t:resume())也不可被暂停(t:suspend())。死亡的线程不会阻塞父进程的关闭。杀死死亡的线程不会报错,而是什么也不做。

状态样例

snippet.lua
local thread = require("thread")
local t = thread.create(function()
  print("before sleep")
  os.sleep()
  print("after sleep")
end)
print(t:status())

输出:

before sleep
running
after sleep
snippet.lua
local thread = require("thread")
local t = thread.create(function()
  print("before sleep")
  os.sleep()
  print("after sleep")
end)
t:suspend()
print(t:status())
os.sleep(10)
print(t:status())
t:resume()
print("after resume")
print(t:status())

输出:

before sleep
suspended
suspended
after resume
dead
  • t:attach([level: number]): boolean, string

    将一个线程附着到一个进程,通常称为子线程或附着线程。level为可选参数,可用于获取父进程,传递0或nil则使用当前运行进程。在最初创建线程时,线程已经被附着于当前进程。若level指向了不存在的线程,则此方法会返回nil与一条报错信息,否则返回true。附着的线程将会阻塞其父进程的关闭,直到线程死亡(或者被杀死,或者父进程放弃执行)。
    - t:detach(): table, string

    将一个线程从其父进程(若有)上解除附着。若未执行动作则返回nil与一条报错信息,否则返回自身(以便于你想在同一行中创建一个线程并将其解除附着)。解除附着的线程会继续执行,直到电脑关机或重启,或者线程死亡。

snippet.lua
local detached_thread = thread.create(function() end):detach()
  • t:join([timeout: number]): boolean, string

    阻塞调用者,直到t不再运行。或者在到达了timeout秒的超时时间后返回false(可选)。在t:join()的调用结束后线程的状态将为“死亡”。任一下列情况都会导致join结束并停止阻塞调用者:

    • 线程继续运行,直到其从线程函数中返回。
    • 线程被放弃,或抛出了未被捕获的异常。
    • 线程被暂停。
    • 线程被杀死

      调用thread.waitForAll({t})在功能上与调用t:join()一致。在进程结束时会对其子线程调用thread.waitForAll(若有)。子线程也通过同样的机制阻塞其父进程。

线程异常样例

此样例演示了线程抛出异常时会发生什么。线程在抛出未捕获的异常时会停止执行并进入“死亡”状态。报错信息将不会输出到stdout或stderr(详见t:resume())。

snippet.lua
local thread = require("thread")
local tty = require("tty")
 
print("p start")
local reader = thread.create(function()
  print("reader start")
  error("thread abort") -- 抛出一个异常
  print("reader done")
end)
print("p end", reader:status())

输出:

p start
reader start
p end	dead

线程中断处理函数样例

此样例演示了你该如何注册处理软中断(^c)的函数,用于关闭文件句柄、释放资源等等,然后退出整个程序。

snippet.lua
local thread = require("thread")
local event = require("event")
 
local cleanup_thread = thread.create(function()
  event.pull("interrupted")
  print("cleaning up resources")
end)
 
local main_thread = thread.create(function()
  print("main program")
  while true do
    io.write("input: ")
    io.read()
  end
end)
 
thread.waitForAny({cleanup_thread, main_thread})
os.exit(0) -- 关闭所有剩余线程

假设用户按下了^c发送了一次软中断

输出:

main program
input: ^c
cleaning up resources

非阻塞式线程Yield/Pull操作样例

此样例演示了现在OpenOS可支持非阻塞式线程。

snippet.lua
local event = require("event")
local thread = require("thread")
thread.create(function()
  a,b,c,d,e,f,g = coroutine.yield()
  print(a,b,c,d,e,f,g)
  print(event.pull())
end)
 
event.push("custom_event_a")
print("done")
event.push("custom_event_b", 2)

输出:

custom_event_a
done
custom_event_b	2

目录