Elixir实战:8 容错基础知识 (3) 监督者
liebian365 2025-01-20 14:33 22 浏览 0 评论
监督者是一个通用进程,用于管理系统中其他进程的生命周期。监督者进程可以启动其他进程,这些进程被视为其子进程。通过链接、监视器和退出陷阱,监督者可以检测任何子进程的可能终止,并在需要时重新启动它。
不属于监督者的进程称为工作进程。这些是提供系统实际服务的进程。您当前版本的待办事项系统仅由工作进程组成,例如待办事项缓存和待办事项服务器进程。
如果任何工作进程崩溃,可能是由于一个错误,您系统的某些部分将永远消失。这就是监督者可以提供帮助的地方。通过在监督者下运行工作进程,您可以确保失败的进程被重启,并恢复系统的服务。
要做到这一点,系统中至少需要一个监督进程。在 Elixir 中,可以使用 Supervisor 模块 (https://hexdocs.pm/elixir/Supervisor.xhtml) 来实现。通过调用 Supervisor.start_link/2 ,您可以启动监督进程,随后其工作方式如下:
- 监控进程捕获退出,然后启动子进程。
- 如果在任何时间点,子进程终止,监控进程会收到相应的退出消息并执行纠正措施,例如重启崩溃的进程。
- 如果监督进程终止,它的子进程也会被终止。
启动监督者有两种不同的方法。在基本方法中,您调用函数 Supervisor.start_link ,传递一个描述要在监督者下启动的每个子项的列表,以及一些额外的监督者选项。或者,您可以传递一个定义回调函数的模块,该函数返回这些信息。我们将从基本方法开始,稍后再解释第二种版本。
让我们向待办事项系统介绍一位监督者。图 8.3 回顾了系统中的这些过程:
- Todo.Server —允许多个客户端在单个待办事项列表上协作
- Todo.Cache —维护一个待办服务器的集合,并负责它们的创建和发现
- Todo.DatabaseWorker —对数据库执行读写操作
- Todo.Database —管理数据库工作者池并将数据库请求转发给他们
待办缓存过程是系统的入口点。当您启动缓存时,所有所需的进程都会启动,因此缓存可以被视为系统的根。现在,我们将介绍一个新的监督进程,它将监督待办缓存过程。
8.3.1 准备现有代码
在开始与主管工作之前,您需要对缓存进行一些更改。首先,您将注册缓存进程。这将允许您与该进程进行交互,而无需知道其 PID。
您还需要在启动待办缓存过程时创建一个链接。如果您希望在监视器下运行该过程,这是必需的。为什么监视器使用链接而不是监控?因为链接是双向的,因此监视器的终止意味着它的所有子进程将自动被终止。这反过来允许您正确终止系统的任何部分,而不会留下悬挂的进程。在本章和下一章中,您将看到这如何运作,当您处理更细粒度的监视时。
创建与调用进程的链接就像用 GenServer.start_link 替代 GenServer.start 一样简单。在此过程中,您还可以将相应的 Todo.Cache 接口函数重命名为 start_link 。
最后,您将使 start_link 函数接受一个参数并忽略它。这看起来有些混乱,但这使得启动一个监督过程变得稍微容易一些。原因将在稍后讨论子规范时解释。更改显示在以下列表中。
清单 8.1 待办事项缓存的变化 (supervised_todo_cache/lib/todo/cache.ex)
defmodule Todo.Cache do
use GenServer
def start_link(_) do ?
GenServer.start_link(__MODULE__, nil, name: __MODULE__) ?
end
def server_process(todo_list_name) do
GenServer.call(__MODULE__, {:server_process, todo_list_name}) ?
end
def init(_) do
IO.puts("Starting to-do cache.") ?
...
end
...
end
重命名接口函数
? 以名称注册并链接到调用进程
? 使用注册名称的接口功能
? 调试消息
请注意,您还可以从 init/1 回调中调用 IO.puts/1 以进行调试。此调试表达式包含在所有其他 GenServer 回调模块中( Todo.Database 、 Todo.DatabaseWorker 和 Todo.Server )。
8.3.2 启动监督进程
通过这些更改,您可以立即尝试启动监督进程,待办缓存作为其唯一子进程。将当前文件夹更改为 supervised_todo_cache,并启动 shell ( iex -S mix )。现在,您可以启动监督者:
iex(1)> Supervisor.start_link([Todo.Cache], strategy: :one_for_one) ?
Starting to-do cache.
Starting database server.
Starting database worker.
Starting database worker.
Starting database worker.
? 启动一个监督者和待办事项缓存作为其子项
从控制台输出可以看出,调用 Supervisor.start_link/2 导致待办事项缓存开始。缓存进程随后启动了数据库进程。
让我们仔细看看 Supervisor.start_link/2 的调用:
Supervisor.start_link(
[Todo.Cache], ?
strategy: :one_for_one ?
)
? 子规格列表
? 监督者策略
如函数名称所示, Supervisor.start_link/2 启动一个监督进程并将其链接到调用者。
第一个参数是所需子项的列表。更准确地说,这个列表的每个元素都是一个子项规范,描述了如何启动和管理子项。我们稍后会详细讨论子项规范。在这种简单形式中,提供的子项规范是一个模块名称。在这种情况下,子项由 Todo.Cache 模块中的某个回调函数描述。
当监督进程启动时,它将遍历此列表并根据规范启动每个子进程。在此示例中,监督将调用 Todo.Cache.start_link/1 。一旦所有子进程启动, Supervisor.start_link/2 将返回 {:ok, supervisor_pid} 。
Supervisor.start_link/2 的第二个参数是特定于监督者的选项列表。 :strategy 选项,也称为重启策略,是必需的。此选项指定监督者应如何处理其子进程的终止。 one_ for_one 策略表示如果一个子进程终止,则应启动另一个子进程来替代它。还有其他几种策略(例如,“如果单个子进程崩溃,则重启所有子进程”),我们将在第 9 章中讨论它们。
注意 这里的重启一词使用得比较随意。从技术上讲,进程无法被重启。它只能被终止;然后,可以在其位置启动另一个由同一模块驱动的进程。新进程具有不同的 PID,并且与旧进程不共享任何状态。
无论如何,在 Supervisor.start_link/2 返回后,系统中的所有必需进程都在运行,您可以与系统进行交互。例如,您可以启动一个待办事项服务器:
iex(2)> bobs_list = Todo.Cache.server_process("Bob's list")
Starting to-do server for Bob's list.
#PID<0.161.0>
缓存进程作为监督进程的子进程启动,因此我们说它是被监督的。这意味着如果缓存进程崩溃,它的监督者将重新启动它。
您可以通过引发缓存进程的崩溃快速验证这一点。首先,您需要获取缓存的 PID。如前所述,缓存现在以一个名称(它自己的模块名称)注册,因此可以借助 Process.whereis/1 轻松获取其 PID:
iex(3)> cache_pid = Process.whereis(Todo.Cache)
#PID<0.155.0>
现在,您可以使用 Process.exit/2 函数终止进程,该函数接受一个 PID 和退出原因,然后向给定进程发送相应的退出信号。退出原因可以是任意术语。在这里,您将使用原子 :kill ,它以特殊方式处理。退出原因 :kill 确保目标进程被无条件终止,即使该进程正在捕获退出。让我们看看它的实际效果:
iex(4)> Process.exit(cache_pid, :kill)
Starting to-do cache.
如您从输出中所见,过程立即重新启动。您还可以证明待办缓存现在是一个具有不同 PID 的进程:
iex(5)> Process.whereis(Todo.Cache)
#PID<0.164.0>
您可以像使用旧流程一样使用新流程:
iex(6)> bobs_list = Todo.Cache.server_process("Bob's list")
Starting to-do server for Bob's list.
#PID<0.167.0>
这个简短的实验证明了一些基本的容错能力。在崩溃之后,系统自我修复并恢复了完整的服务。
名称允许流程发现
重要的是要解释为什么将待办事项缓存注册为本地名称。您应该始终记住,要与进程进行通信,您需要拥有其 PID。在第 7 章中,您使用了一种简单的方法,创建了一个进程,然后传递其 PID。这在您进入监控者领域之前是可以的。
问题在于,受监督的进程可以被重启。请记住,重启归结为用一个新进程替代旧进程——新进程有一个不同的 PID。这意味着对崩溃进程的 PID 的任何引用都变得无效,标识了一个不存在的进程。
这就是注册名称重要的原因。它们提供了一种可靠的方式来查找进程并与之交互,无论可能的进程重启。
8.3.3 子规范
要管理子进程,监督者需要一些信息,例如以下问题的答案:
- 孩子应该如何开始?
- 如果孩子终止了该怎么办?
- 应该使用什么术语来唯一标识每个孩子?
这些信息统称为子规范。回想一下,当调用 Supervisor.start_link/2 时,您发送了一份子规范列表。在其基本形态中,规范是一个映射,包含几个字段来配置子项的属性。
例如,待办事项缓存的规范可能如下所示:
%{
id: Todo.Cache, ?
start: {Todo.Cache, :start_link, [nil]}, ?
}
? 子项的 ID
? 启动规范
:id 字段是一个任意术语,用于区分该子项与同一主管的其他子项。
:start 字段是形状为 {module, start_function, list_of_ arguments} 的三元组。在启动子进程时,通用监督代码将使用 apply(module, start_function, list_of_arguments) 来调用由此元组描述的函数。被调用的函数必须启动并链接该进程。
您可以省略规范中的一些其他字段,在这种情况下,将选择一些合理的默认值。我们将在第 9 章稍后讨论其中的一些。您还可以参考官方文档 https://hexdocs.pm/elixir/Supervisor.xhtml#module-child-specification 以获取更多详细信息。
无论如何,您可以将规格图直接传递给 Supervisor.start_link 。以下是一个示例:
Supervisor.start_link(
[
%{
id: Todo.Cache,
start: {Todo.Cache, :start_link, [nil]}
}
],
strategy: :one_for_one
)
这将指示主管调用 Todo.Cache.start_link(nil) 来启动子进程。请记住,您已将 Todo.Cache.start_link 更改为接受一个参数(该参数被忽略),因此您需要传递某个值(在此示例中为 nil )。
这种方法的一个问题是容易出错。如果缓存的实现发生变化,例如启动函数的签名,您需要记住在启动监控程序的代码中调整规范。
为了解决这个问题, Supervisor 允许您在子规范列表中传递一个元组 {module_name, arg} 。在这种情况下, Supervisor 将首先调用 module_name .child_spec(arg) 以获取实际的规范。此函数必须返回规范映射。然后,监督者根据返回的规范启动子进程。
Todo.Cache 模块已经定义了 child_spec/1 ,即使您没有自己编写。默认实现是由 use GenServer 注入的。因此,您也可以以以下方式启动监督者:
Supervisor.start_link(
[{Todo.Cache, nil}],
strategy: :one_for_one
)
因此, Supervisor 将调用 Todo.Cache.child_spec(nil) 并根据返回的规范启动子进程。验证注入的 child_spec/1 实现返回的内容很简单:
iex(1)> Todo.Cache.child_spec(nil)
%{id: Todo.Cache, start: {Todo.Cache, :start_link, [nil]}}
换句话说,生成的 child_spec/1 返回一个规范,该规范调用模块的 start_link/1 函数,并将参数传递给 child_spec/1 。这正是你让 Todo.Cache.start_link 接受一个参数的原因,尽管该参数被忽略:
defmodule Todo.Cache do
use GenServer ?
def start_link(_) do ?
...
end
...
end
生成默认的 child_spec/1
符合默认的 child_spec/1
通过这样做,您使 Todo.Cache 与生成的 child_spec/1 兼容,这意味着您可以将 Todo.Cache 包含在子项列表中,而无需进行任何额外的工作。
如果您不喜欢这种方法,您可以向 use GenServer 提供一些选项,以调整生成的 child_spec/1 的输出。有关更多详细信息,请参阅官方文档(https://hexdocs.pm/elixir/GenServer.xhtml#module-how-to-supervise)。如果您需要更多控制,您可以自己定义 child_spec/1 ,这将覆盖默认实现。
最后,如果您不关心传递给 child_spec/1 的参数,您可以在子规范列表中仅包含模块名称。在这种情况下, Supervisor 将向 child_spec/1 传递空列表 [] 。因此,您也可以像这样启动 Todo.Cache :
Supervisor.start_link(
[Todo.Cache],
strategy: :one_for_one
)
在进一步之前,让我们回顾一下监督者启动的工作原理。当你调用 Supervisor.start_link(child_specs, options) 时,以下情况发生:
- 新过程已启动,由 Supervisor 模块提供动力。
- 监督进程逐一遍历子规范列表,并启动每个子进程。
- 每个规范在需要时通过调用相应模块中的 child_spec/1 来解决。
- 监督者根据子规范的 :start 字段启动子进程。
8.3.4 包装监督者
到目前为止,您已经在 shell 中与 supervisor 进行了交互。但在实际应用中,您会希望在代码中使用 supervisor。就像使用 GenServer 一样,建议将 Supervisor 包装在一个模块中。
以下列表实现了您第一个监督者的模块。
清单 8.2 待办事项系统监督者 (supervised_todo_cache/lib/todo/system.ex)
defmodule Todo.System do
def start_link do
Supervisor.start_link(
[Todo.Cache],
strategy: :one_for_one
)
end
end
通过这个简单的添加,启动整个系统变得容易:
$ iex -S mix
iex(1)> Todo.System.start_link()
Starting to-do cache.
Starting database server.
Starting database worker.
Starting database worker.
Starting database worker.
名称 Todo.System 被选用来描述模块的目的。通过调用 Todo.System.start_link() ,您可以启动整个待办事项系统,包含所有必需的服务,如缓存和数据库。
8.3.5 使用回调模块
另一种启动监督者的方法是提供回调模块。这的工作方式类似于 GenServer 。您需要开发一个必须实现 init/1 函数的模块。该函数必须返回子规范的列表和其他监督者选项,例如其策略。
这里是您如何重写 Todo.System 以使用这种方法:
defmodule Todo.System do
use Supervisor ?
def start_link do
Supervisor.start_link(__MODULE__, nil) ?
end
def init(_) do ?
Supervisor.init([Todo.Cache], strategy: :one_for_one) ?
end ?
end
? 包含一些常见的模板内容
? 使用 Todo.System 作为回调模块启动监督者
? 实现所需的回调函数
与 GenServer 一样,您从 use Supervisor 开始,以便在您的模块中获得一些通用的模板代码。
关键部分发生在您调用 Supervisor.start_link/2 时。您现在传递的是回调模块,而不是子规范的列表。在这种情况下,监督进程将调用 init/1 函数以提供监督规范。传递给 init/1 的参数是您传递给 Supervisor .start_link/2 的第二个参数。最后,在 init/1 中,您借助 Supervisor .init/2 函数描述监督者,传递给它子项列表和监督者选项。
前面的代码是 Supervisor.start_ link([Todo.Cache], strategy: :one_for_one) 的更复杂的等效形式。显然,您需要更多的代码行才能获得相同的效果。好的一面是,这种方法给您更多的控制。例如,如果您需要在启动子进程之前进行一些额外的初始化,您可以在 init/1 中做到。此外,回调模块在热代码重载方面更灵活,允许您修改子进程列表而无需重新启动整个监视器。
在大多数情况下,直接传递子规格列表的简单方法就足够了。此外,正如您在前面的示例中看到的,如果将 Supervisor 的使用封装在一个专用模块中,切换不同的方法就很容易。因此,在本书中,您将仅使用简单方法,而不使用回调模块。
8.3.6 连接所有过程
在这一点上,您正在监督待办缓存过程,因此您获得了一些基本的容错能力。如果缓存过程崩溃,将启动一个新过程,系统可以恢复提供服务。
然而,您当前实现中存在一个问题。当主管重新启动待办事项缓存时,您将获得一个完全独立的进程层次结构,并且会有一组与之前的待办事项服务器毫无关系的新待办事项服务器。之前的待办事项服务器将成为未使用的垃圾,仍在运行并消耗内存和 CPU 资源。
让我们演示这个问题。首先,启动系统并请求一个待办事项服务器:
iex(1)> Todo.System.start_link()
iex(2)> Todo.Cache.server_process("Bob's list")
Starting to-do server for Bob's list.
#PID<0.159.0>
缓存的待办服务器在后续请求中未启动:
iex(3)> Todo.Cache.server_process("Bob's list")
#PID<0.159.0>
检查正在运行的进程数量:
iex(4)> length(Process.list())
71
现在,终止待办缓存:
iex(5)> Process.exit(Process.whereis(Todo.Cache), :kill)
Starting to-do cache.
最后,请求一个待办事项服务器用于鲍勃的列表:
iex(6)> Todo.Cache.server_process("Bob's list")
Starting to-do server for Bob's list.
#PID<0.165.0>
如您所见,在您重启待办事项缓存后,检索先前获取的服务器会创建一个新进程。这并不令人惊讶,因为您终止了先前的缓存进程,这也销毁了进程状态。
当一个进程终止时,它的状态被释放,新进程以全新的状态开始。如果你想保留状态,必须自己处理;我们将在第 9 章讨论这个问题。
在缓存过程重新启动后,您将拥有一个完全新的进程,它对之前缓存的内容没有任何概念。同时,您的旧缓存结构(待处理服务器)并没有被清理。您可以通过重新检查正在运行的进程数量来看到这一点:
iex(7)> length(Process.list())
72
您有一个额外的进程,即之前为 Bob 的待办事项列表启动的待办服务器。这显然不好。终止待办缓存会破坏其状态,因此您还应该关闭所有现有的待办服务器。这样,您可以确保正确的进程终止。
要做到这一点,您必须在进程之间建立链接。每个待办事项服务器必须与缓存相连。进一步来说,您还需要将数据库服务器与待办事项缓存连接,并将数据库工作者与数据库服务器连接。这将有效确保整个结构相互链接,如图 8.4 所示。
通过链接一组相互依赖的过程,您可以确保一个过程的崩溃也会导致其依赖项崩溃。无论哪个过程崩溃,链接都确保整个结构被终止。由于这将导致缓存过程的终止,监控者会注意到这一点,并会启动一个新的系统。
通过这种方法,您可以检测系统中任何部分的错误并从中恢复,而不会留下悬挂的进程。缺点是,您允许错误产生广泛的影响。单个数据库工作者或待办服务器中的错误将导致整个结构崩溃。这远非完美,您将在第 9 章中进行改进。
目前,让我们坚持这种简单的方法并实现所需的代码。在您当前的系统中,您有一个待办事项监督者,它启动并监督缓存。您必须确保缓存与所有其他工作进程直接或间接相连。
更改很简单。您只需将项目中所有流程的 start 切换为 start_link 。在相应的模块中,您当前有如下内容:
def start(...) do
GenServer.start(...)
end
此代码片段必须转换为以下内容:
def start_link(...) do
GenServer.start_link(...)
end
当然,每个 module.start 调用必须替换为 module .start_link 。这些更改是机械的,代码在这里没有呈现。完整的解决方案位于 todo_links 文件夹中。
让我们看看新系统是如何工作的:
iex(1)> Todo.System.start_link()
iex(2)> Todo.Cache.server_process("Bob's list")
Starting to-do server for Bob's list.
iex(3)> length(Process.list())
71
iex(4)> Process.exit(Process.whereis(Todo.Cache), :kill) ?
iex(5)> bobs_list = Todo.Cache.server_process("Bob's list")
Starting to-do server for Bob's list.
iex(6)> length(Process.list())
71 ?
终止整个过程结构
? 进程计数保持不变。
当您崩溃一个进程时,整个结构会被终止,并且一个新的进程会在其位置启动。链接确保相关的进程也被终止,从而保持系统的一致性。
8.3.7 重启频率
重要的是要记住,监督者不会无限期地重启子进程。监督者依赖于最大重启频率,该频率定义了在给定时间段内允许多少次重启。默认情况下,最大重启频率为 5 秒内 3 次重启。您可以通过将 :max_restarts 和 :max_seconds 选项传递给 Supervisor.start_link/2 来更改这些参数。如果超过此频率,监督者将放弃并终止自己及其所有子进程。
让我们在 shell 中验证这一点。首先,启动 supervisor:
iex(1)> Todo.System.start_link()
Starting the to-do cache.
现在,您需要频繁重启待办事项缓存进程:
iex(1)> for _ <- 1..4 do
Process.exit(Process.whereis(Todo.Cache), :kill)
Process.sleep(200)
end
在这里,您终止缓存进程并短暂休眠,允许主管重新启动该进程。这一过程重复四次,这意味着在最后一次迭代中,您将超过默认的最大重启频率(5 秒内三次重启)。
这里是输出:
Starting the to-do cache. ?
Starting database server. ?
... ?
** (EXIT from #PID<0.149.0>) :shutdown ?
? 重复三次
主管终止。
在超过最大重启频率后,监控程序放弃并终止,同时也关闭了子进程。
您可能会想知道这个机制的原因。当系统中的一个关键进程崩溃时,它的监控者会尝试通过启动一个新进程将其重新上线。如果这没有帮助,那么无限重启就没有意义。如果在给定的时间间隔内发生了太多次重启,很明显问题无法解决。在这种情况下,监控者能做的唯一明智的事情就是放弃并终止自己,这也会终止它的所有子进程。
该机制在所谓的监督树中发挥着重要作用,在这些树中,监督者和工作人员被组织在更深的层次结构中,这使您能够控制系统如何从错误中恢复。下一章将对此进行详细解释,您将在其中构建一个细粒度的监督树。
摘要
- 运行时错误有三种类型:抛出、错误和退出。
- 当运行时错误发生时,执行会向上移动到相应的 try 块。如果错误未被处理,进程将崩溃。
- 可以在另一个进程中检测到进程终止。为此,您可以使用链接或监视器。
- 链接是双向的——任一进程的崩溃都会传播到另一个进程。
- 默认情况下,当一个进程异常终止时,所有与之链接的进程也会终止。通过捕获退出,您可以对链接进程的崩溃做出反应并采取相应措施。
- 监督者是一个管理其他进程生命周期的进程。它可以启动、监督和重启崩溃的进程。
- Supervisor 模块用于启动监督者并与之协作。
- 一个监督者由子规范列表和监督策略定义。您可以将这些作为参数提供给 Supervisor.start_link/2 ,或者您可以实现一个回调模块。
- 上一篇:Linux系统僵尸进程详解
- 下一篇:APP检测:安卓系统四大组件介绍
相关推荐
- 4万多吨豪华游轮遇险 竟是因为这个原因……
-
(观察者网讯)4.7万吨豪华游轮搁浅,竟是因为油量太低?据观察者网此前报道,挪威游轮“维京天空”号上周六(23日)在挪威近海发生引擎故障搁浅。船上载有1300多人,其中28人受伤住院。经过数天的调...
- “菜鸟黑客”必用兵器之“渗透测试篇二”
-
"菜鸟黑客"必用兵器之"渗透测试篇二"上篇文章主要针对伙伴们对"渗透测试"应该如何学习?"渗透测试"的基本流程?本篇文章继续上次的分享,接着介绍一下黑客们常用的渗透测试工具有哪些?以及用实验环境让大家...
- 科幻春晚丨《震动羽翼说“Hello”》两万年星间飞行,探测器对地球的最终告白
-
作者|藤井太洋译者|祝力新【编者按】2021年科幻春晚的最后一篇小说,来自大家喜爱的日本科幻作家藤井太洋。小说将视角放在一颗太空探测器上,延续了他一贯的浪漫风格。...
- 麦子陪你做作业(二):KEGG通路数据库的正确打开姿势
-
作者:麦子KEGG是通路数据库中最庞大的,涵盖基因组网络信息,主要注释基因的功能和调控关系。当我们选到了合适的候选分子,单变量研究也已做完,接着研究机制的时便可使用到它。你需要了解你的分子目前已有哪些...
- 知存科技王绍迪:突破存储墙瓶颈,详解存算一体架构优势
-
智东西(公众号:zhidxcom)编辑|韦世玮智东西6月5日消息,近日,在落幕不久的GTIC2021嵌入式AI创新峰会上,知存科技CEO王绍迪博士以《存算一体AI芯片:AIoT设备的算力新选择》...
- 每日新闻播报(September 14)_每日新闻播报英文
-
AnOscarstatuestandscoveredwithplasticduringpreparationsleadinguptothe87thAcademyAward...
- 香港新巴城巴开放实时到站数据 供科技界研发使用
-
中新网3月22日电据香港《明报》报道,香港特区政府致力推动智慧城市,鼓励公私营机构开放数据,以便科技界研发使用。香港运输署21日与新巴及城巴(两巴)公司签署谅解备忘录,两巴将于2019年第3季度,开...
- 5款不容错过的APP: Red Bull Alert,Flipagram,WifiMapper
-
本周有不少非常出色的app推出,鸵鸟电台做了一个小合集。亮相本周榜单的有WifiMapper's安卓版的app,其中包含了RedBull的一款新型闹钟,还有一款可爱的怪物主题益智游戏。一起来看看我...
- Qt动画效果展示_qt显示图片
-
今天在这篇博文中,主要实践Qt动画,做一个实例来讲解Qt动画使用,其界面如下图所示(由于没有录制为gif动画图片,所以请各位下载查看效果):该程序使用应用程序单窗口,主窗口继承于QMainWindow...
- 如何从0到1设计实现一门自己的脚本语言
-
作者:dong...
- 三年级语文上册 仿写句子 需要的直接下载打印吧
-
描写秋天的好句好段1.秋天来了,山野变成了美丽的图画。苹果露出红红的脸庞,梨树挂起金黄的灯笼,高粱举起了燃烧的火把。大雁在天空一会儿写“人”字,一会儿写“一”字。2.花园里,菊花争奇斗艳,红的似火,粉...
- C++|那些一看就很简洁、优雅、经典的小代码段
-
目录0等概率随机洗牌:1大小写转换2字符串复制...
- 二年级上册语文必考句子仿写,家长打印,孩子照着练
-
二年级上册语文必考句子仿写,家长打印,孩子照着练。具体如下:...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- wireshark怎么抓包 (75)
- qt sleep (64)
- cs1.6指令代码大全 (55)
- factory-method (60)
- sqlite3_bind_blob (52)
- hibernate update (63)
- c++ base64 (70)
- nc 命令 (52)
- wm_close (51)
- epollin (51)
- sqlca.sqlcode (57)
- lua ipairs (60)
- tv_usec (64)
- 命令行进入文件夹 (53)
- postgresql array (57)
- statfs函数 (57)
- .project文件 (54)
- lua require (56)
- for_each (67)
- c#工厂模式 (57)
- wxsqlite3 (66)
- dmesg -c (58)
- fopen参数 (53)
- tar -zxvf -c (55)
- 速递查询 (52)