Lua协同程序深度解析:从基础到多线程编程实战
引言
协同程序是Lua语言中一项强大而独特的并发编程特性,它允许在单个线程内实现多个执行流的协作式切换。与传统的多线程编程不同,协同程序采用非抢先式调度,这意味着程序可以显式控制执行权的转移,从而避免了竞态条件和锁机制的复杂性。协同程序在Lua中被称为coroutine,它提供了一种轻量级的并发模型,特别适合处理I/O密集型任务、状态机实现、迭代器构造以及数据流处理等场景。本章将全面探讨协同程序的核心概念、实现原理和实际应用,结合详实的理论分析和丰富的代码示例,帮助读者深入理解并掌握这一重要特性。我们将从协同程序的基础操作开始,逐步深入到管道与过滤器模式、迭代器实现,最终探讨非抢先式多线程编程。所有代码示例均遵循Allman风格和驼峰命名法,确保在Visual Studio 2022和VS Code环境中能够正确运行,力求用通俗易懂的语言解释复杂概念,为读者提供实用的编程指导。
9.1 协同程序的基本原理与操作
协同程序在Lua中是一种可以暂停和恢复执行的函数,它允许程序在多个任务之间进行协作式切换,而无需依赖操作系统级别的线程。每个协同程序都有自己的执行上下文,包括局部变量、程序计数器等状态信息,这些状态在挂起时会被保存,在恢复时会被还原。与函数调用不同,协同程序的执行权转移是对称的:任何一个协同程序都可以挂起自己并将控制权交给另一个协同程序,之后可以从挂起点恢复执行。
在Lua中,协同程序相关的函数主要位于coroutine表内,包括创建、启动、挂起、恢复和状态查询等操作。理解这些基本操作是掌握协同程序的关键。首先,我们来看协同程序的创建和启动。协同程序通过coroutine.create函数创建,该函数接收一个函数作为参数,并返回一个协同程序对象。协同程序对象可以通过coroutine.resume函数启动或恢复执行,而协同程序内部则可以使用coroutine.yield函数挂起自己的执行。
下面我们通过一个简单的例子来演示协同程序的基本操作流程:
-- 定义一个简单的协同程序函数
function simpleCoroutineFunction()
print("协同程序开始执行")
local step = 1
-- 第一次挂起
print("步骤 " .. step .. ": 即将挂起")
local fromMain = coroutine.yield("第一次挂起返回的值")
print("从主程序接收的值: " .. tostring(fromMain))
step = step + 1
-- 第二次挂起
print("步骤 " .. step .. ": 即将再次挂起")
coroutine.yield("第二次挂起返回的值")
step = step + 1
print("步骤 " .. step .. ": 协同程序执行结束")
return "协同程序最终返回值"
end
-- 创建协同程序
local co = coroutine.create(simpleCoroutineFunction)
print("协同程序创建完成,状态: " .. coroutine.status(co))
-- 第一次恢复执行
print("\n第一次恢复协同程序:")
local success, value1 = coroutine.resume(co)
print("恢复成功: " .. tostring(success))
print("返回的值: " .. tostring(value1))
print("当前状态: " .. coroutine.status(co))
-- 第二次恢复执行
print("\n第二次恢复协同程序:")
local success, value2 = coroutine.resume(co, "从主程序传递的值")
print("恢复成功: " .. tostring(success))
print("返回的值: " .. tostring(value2))
print("当前状态: " .. coroutine.status(co))
-- 第三次恢复执行
print("\n第三次恢复协同程序:")
local success, value3 = coroutine.resume(co)
print("恢复成功: " .. tostring(success))
print("返回的值: " .. tostring(value3))
print("当前状态: " .. coroutine.status(co))
运行上述代码,我们可以看到协同程序的完整生命周期。首先,通过coroutine.create创建协同程序对象,此时协同程序处于"suspended"状态。第一次调用coroutine.resume启动协同程序,它会执行到第一个coroutine.yield处挂起,并返回yield的参数。此时协同程序状态变为"suspended"。第二次调用coroutine.resume时,我们传递了一个参数给协同程序,这个参数会成为coroutine.yield的返回值,协同程序继续执行到下一个yield。第三次恢复时,协同程序执行完毕,状态变为"dead"。
协同程序的状态管理是其核心特性之一。Lua提供了coroutine.status函数来查询协同程序的当前状态,可能的状态包括:
- “suspended”:协同程序已创建但未启动,或已挂起
- “running”:协同程序正在执行
- “normal”:协同程序是活动的但未运行(正在运行其他协同程序)
- “dead”:协同程序已经执行完毕或发生错误
另一个有用的函数是coroutine.wrap,它创建一个协同程序并返回一个函数,调用这个函数就相当于调用coroutine.resume,但不需要显式传递协同程序对象。下面是用coroutine.wrap重写的示例:
-- 使用coroutine.wrap创建协同程序
local wrappedCoroutine = coroutine.wrap(simpleCoroutineFunction)
print("使用wrap创建的协同程序:")
local value1 = wrappedCoroutine()
print("第一次调用返回值: " .. tostring(value1))
local value2 = wrappedCoroutine("从主程序传递的值")
print("第二次调用返回值: " .. tostring(value2))
local value3 = wrappedCoroutine()
print("第三次调用返回值: " .. tostring(value3))
coroutine.wrap的优点是语法更简洁,但它不能检查协同程序状态,且在协同程序死亡后再次调用会引发错误。
协同程序的核心优势在于它能够保持执行状态。下面的例子展示了协同程序如何用于实现一个简单的状态机:
-- 使用协同程序实现状态机
function createStateMachine()
local states = {
"初始化",
"处理中",
"完成"
}
local currentStateIndex = 0
return coroutine.create(function()
while currentStateIndex < #states do
currentStateIndex = currentStateIndex + 1
local nextState = coroutine.yield(states[currentStateIndex])
if nextState == "重置" then
currentStateIndex = 0
print("状态机已重置")
end
end
return "状态机执行完毕"
end)
end
-- 使用状态机
local stateMachine = createStateMachine()
print("状态机演示:")
for i = 1, 4 do
local success, state = coroutine.resume(stateMachine)
if success then
print("状态 " .. i .. ": " .. tostring(state))
else
print("状态机结束: " .. tostring(state))
end
-- 在第三次时发送重置信号
if i == 3 then
print("发送重置信号")
local success, state = coroutine.resume(stateMachine, "重置")
if success then
print("重置后状态: " .. tostring(state))
end
end
end
这个例子展示了协同程序如何维护内部状态(currentStateIndex),并根据外部输入(通过yield返回值)改变行为。这种模式非常适合游戏开发、解析器、工作流引擎等场景。
协同程序与普通函数的一个关键区别是它可以在执行过程中多次挂起和恢复,这使得它能够处理需要保持中间状态的长时任务。下面的例子模拟了一个分批处理数据的场景:
-- 分批处理数据的协同程序
function createBatchProcessor(dataArray, batchSize)
local index = 0
return coroutine.create(function()
local totalBatches = math.ceil(#dataArray / batchSize)
for batch = 1, totalBatches do
local batchData = {}
local startIndex = (batch - 1) * batchSize + 1
local endIndex = math.min(batch * batchSize, #dataArray)
for i = startIndex, endIndex do
table.insert(batchData, dataArray[i])
end
local shouldContinue = coroutine.yield(batch, batchData)
if shouldContinue == false then
print("处理提前终止")
return "提前终止"
end
end
return "处理完成"
end)
end
-- 测试数据
local testData = {}
for i = 1, 25 do
testData[i] = "数据项_" .. i
end
-- 使用批处理器
local processor = createBatchProcessor(testData, 5)
print("批量处理演示:")
local batchCount = 1
while coroutine.status(processor) == "suspended" do
local success, batchNum, batchData = coroutine.resume(processor)
if success then
print("批次 " .. batchNum .. ": 包含 " .. #batchData .. " 项数据")
-- 模拟处理数据
for i, item in ipairs(batchData) do
print(" 处理: " .. item)
end
-- 在第三批后提前终止
if batchNum == 3 then
print("第三批处理完成后提前终止")
local success, result = coroutine.resume(processor, false)
if success then
print("最终结果: " .. result)
end
break
end
batchCount = batchCount + 1
else
print("处理完成: " .. batchNum)
break
end
end
这个例子展示了协同程序如何用于实现复杂的数据处理流水线,其中处理过程可以被挂起、恢复,甚至提前终止。
理解协同程序的基础操作是掌握更高级应用的前提。在下一节中,我们将探讨如何利用协同程序实现管道和过滤器模式,这是数据流处理中的常见架构。
9.2 管道与过滤器模式的协同程序实现
管道与过滤器是一种经典的软件架构模式,它将数据处理过程分解为一系列独立的处理步骤(过滤器),这些步骤通过管道连接,数据从管道的一端流入,经过各个过滤器的处理,最终从另一端流出。在Lua中,协同程序是实现管道与过滤器模式的理想工具,因为每个过滤器可以作为一个协同程序,在需要时挂起等待数据,处理完数据后又可以将结果传递给下一个过滤器。
在管道与过滤器模式中,每个过滤器都是一个独立的处理单元,它从输入管道读取数据,进行处理,然后将结果写入输出管道。管道则负责在过滤器之间传递数据。使用协同程序实现时,每个过滤器可以运行在自己的执行上下文中,通过yield和resume进行数据传递和流程控制。
首先,我们来看一个简单的管道与过滤器实现。这个例子包含三个过滤器:一个生产者生成数据,一个处理器转换数据,一个消费者消费数据。
-- 生产者过滤器:生成数字序列
function createProducer(filterCount)
return coroutine.create(function()
for i = 1, filterCount do
coroutine.yield("数据_" .. i) -- 生产数据并挂起
end
return "生产完成"
end)
end
-- 处理器过滤器:转换数据
function createProcessor(inputCoroutine)
return coroutine.create(function()
while true do
-- 从生产者获取数据
local success, data = coroutine.resume(inputCoroutine)
if not success or data == "生产完成" then
coroutine.yield("处理完成")
break
end
-- 处理数据:添加前缀
local processedData = "[已处理] " .. data
coroutine.yield(processedData) -- 将处理后的数据传递给下一个过滤器
end
end)
end
-- 消费者过滤器:消费数据
function createConsumer(processorCoroutine)
return coroutine.create(function()
local consumedCount = 0
while true do
-- 从处理器获取数据
local success, data = coroutine.resume(processorCoroutine)
if not success or data == "处理完成" then
print("消费完成,总计消费: " .. consumedCount .. " 项数据")
break
end
-- 消费数据
consumedCount = consumedCount + 1
print("消费者接收: " .. data)
coroutine.yield("已消费") -- 确认消费
end
return consumedCount
end)
end
-- 构建管道
print("简单管道与过滤器演示:")
local producer = createProducer(5)
local processor = createProcessor(producer)
local consumer = createConsumer(processor)
-- 启动管道
local success, result = coroutine.resume(consumer)
if success then
print("管道执行结果: " .. tostring(result))
end
在这个例子中,数据流向是线性的:生产者→处理器→消费者。每个过滤器都是一个协同程序,通过yield传递数据,通过resume接收数据。这种设计使得每个过滤器可以独立开发、测试和复用。
然而,上述实现有一个问题:过滤器的耦合度较高,每个过滤器都需要知道前一个过滤器的存在。为了创建更灵活的管道系统,我们可以引入管道连接器的概念。下面是一个改进的实现:
-- 通用的过滤器接口
function createFilter(processFunction)
return coroutine.create(function(inputPipe)
while true do
-- 从输入管道获取数据
local success, data = coroutine.resume(inputPipe)
if not success or data == nil then
coroutine.yield(nil) -- 传递结束信号
break
end
-- 处理数据
local result = processFunction(data)
-- 将处理后的数据传递给下一个过滤器
coroutine.yield(result)
end
end)
end
-- 创建生产者
function createProducerFilter(itemCount)
return coroutine.create(function()
for i = 1, itemCount do
coroutine.yield({id = i, value = "原始数据_" .. i})
end
coroutine.yield(nil) -- 发送结束信号
end)
end
-- 创建数据转换过滤器
function createTransformFilter()
local processFunction = function(data)
data.value = "转换后的_" .. data.value
data.timestamp = os.time()
return data
end
return createFilter(processFunction)
end
-- 创建数据验证过滤器
function createValidationFilter()
local processFunction = function(data)
if data.id % 2 == 0 then
data.valid = true
data.validationNote = "偶数ID通过验证"
else
data.valid = false
data.validationNote = "奇数ID未通过验证"
end
return data
end
return createFilter(processFunction)
end
-- 创建消费者过滤器
function createConsumerFilter()
local totalConsumed = 0
local validCount = 0
local processFunction = function(data)
totalConsumed = totalConsumed + 1
if data.valid then
validCount = validCount + 1
print(string.format("消费有效数据: ID=%d, 值=%s, 时间=%d",
data.id, data.value, data.timestamp))
else
print(string.format("跳过无效数据: ID=%d, 原因=%s",
data.id, data.validationNote))
end
return {total = totalConsumed, valid = validCount}
end
return createFilter(processFunction)
end
-- 管道连接函数
function connectPipes(pipe1, pipe2)
return coroutine.create(function()
while true do
-- 从第一个管道获取数据
local success, data = coroutine.resume(pipe1)
if not success then
coroutine.yield(nil)
break
end
-- 如果第一个管道结束,传递结束信号
if data == nil then
coroutine.resume(pipe2, nil)
coroutine.yield(nil)
break
end
-- 将数据传递给第二个管道
local success2, result = coroutine.resume(pipe2, data)
if success2 then
coroutine.yield(result)
end
end
end)
end
-- 构建和执行管道
print("\n改进的管道系统演示:")
-- 创建各个过滤器
local producer = createProducerFilter(6)
local transformer = createTransformFilter()
local validator = createValidationFilter()
local consumer = createConsumerFilter()
-- 连接管道
local pipe1 = connectPipes(producer, transformer)
local pipe2 = connectPipes(pipe1, validator)
local finalPipe = connectPipes(pipe2, consumer)
-- 执行管道
local finalResult = nil
while true do
local success, data = coroutine.resume(finalPipe)
if not success or data == nil then
break
end
finalResult = data
end
if finalResult then
print(string.format("\n处理统计: 总计处理%d项,有效数据%d项",
finalResult.total, finalResult.valid))
end
这个改进的实现引入了几个重要概念:
- 通用过滤器接口:所有过滤器都遵循相同的模式,提高了代码的一致性。
- 管道连接器:专门的函数负责连接两个过滤器,降低了过滤器之间的耦合度。
- 数据封装:数据以表的形式传递,可以携带多个字段。
- 结束信号:使用nil作为管道结束的信号。
管道与过滤器模式的一个高级应用是创建可组合的数据处理流水线。下面的例子展示了如何动态构建和修改管道:
-- 动态管道构建器
function createPipelineBuilder()
local filters = {}
return {
addFilter = function(self, filterCreator)
table.insert(filters, filterCreator)
return self -- 支持链式调用
end,
build = function(self, initialData)
-- 创建初始生产者
local currentPipe = coroutine.create(function()
for _, item in ipairs(initialData) do
coroutine.yield(item)
end
coroutine.yield(nil)
end)
-- 依次连接所有过滤器
for _, filterCreator in ipairs(filters) do
local filter = filterCreator()
local nextPipe = coroutine.create(function(input)
while true do
local success, data = coroutine.resume(input)
if not success or data == nil then
coroutine.yield(nil)
break
end
local success2, result = coroutine.resume(filter, data)
if success2 then
coroutine.yield(result)
end
end
end)
currentPipe = nextPipe
end
return currentPipe
end
}
end
-- 示例过滤器创建函数
function createUpperCaseFilter()
return createFilter(function(data)
if type(data) == "string" then
return data:upper()
end
return data
end)
end
function createExclamationFilter()
return createFilter(function(data)
return data .. "!"
end)
end
function createRepeaterFilter(count)
return createFilter(function(data)
local result = ""
for i = 1, count do
result = result .. data
if i < count then
result = result .. "-"
end
end
return result
end)
end
-- 使用动态管道构建器
print("\n动态管道构建演示:")
local builder = createPipelineBuilder()
local pipeline = builder
:addFilter(createUpperCaseFilter)
:addFilter(createExclamationFilter)
:addFilter(function() return createRepeaterFilter(3) end)
:build({"hello", "world", "lua"})
-- 执行管道
while true do
local success, data = coroutine.resume(pipeline)
if not success or data == nil then
break
end
print("输出: " .. data)
end
这个动态管道构建器展示了如何通过组合简单的过滤器来创建复杂的数据处理流程。每个过滤器都是独立的,可以轻松添加、移除或替换,这符合软件工程的开闭原则。
管道与过滤器模式在实际应用中有许多用途,包括日志处理、数据转换、ETL(提取、转换、加载)流程等。使用协同程序实现这种模式的优势在于:
- 代码清晰:每个过滤器只关注自己的处理逻辑。
- 资源高效:不需要为每个过滤器创建线程,减少了上下文切换开销。
- 可控性强:可以精确控制数据流和错误处理。
在下一节中,我们将探讨如何利用协同程序实现迭代器,这是协同程序的另一个重要应用场景。
9.3 基于协同程序的迭代器实现
迭代器是遍历集合元素的工具,在Lua中,我们通常使用闭包或无状态迭代器来实现。然而,协同程序提供了一种更直观、更强大的迭代器实现方式。使用协同程序实现的迭代器可以维护复杂的遍历状态,支持暂停和恢复,并且代码结构更加清晰。这种迭代器特别适合遍历复杂数据结构或生成复杂序列。
基于协同程序的迭代器核心思想是将遍历逻辑封装在一个协同程序中,每次迭代时恢复协同程序的执行,获取下一个元素,然后挂起等待下一次迭代。这种方式使得迭代器可以保持完整的遍历状态,包括局部变量、程序计数器等。
首先,我们来看一个简单的例子:使用协同程序实现数组迭代器。
-- 使用协同程序实现数组迭代器
function createCoroutineArrayIterator(array)
return coroutine.create(function()
for i, value in ipairs(array) do
coroutine.yield(i, value) -- 挂起并返回索引和值
end
end)
end
-- 使用协同程序迭代器遍历数组
function iterateArrayWithCoroutine(array)
local iterator = createCoroutineArrayIterator(array)
return function()
local success, index, value = coroutine.resume(iterator)
if success and index ~= nil then
return index, value
else
return nil
end
end
end
-- 测试数组迭代器
print("协同程序数组迭代器演示:")
local testArray = {"苹果", "香蕉", "橙子", "葡萄"}
-- 方法1:直接使用协同程序
local coIterator = createCoroutineArrayIterator(testArray)
while true do
local success, index, value = coroutine.resume(coIterator)
if not success or index == nil then
break
end
print(string.format("方法1 - 索引[%d]: %s", index, value))
end
-- 方法2:使用迭代器函数
for index, value in iterateArrayWithCoroutine(testArray) do
print(string.format("方法2 - 索引[%d]: %s", index, value))
end
这个例子展示了两种使用协同程序迭代器的方式:直接操作协同程序对象,或者将其包装成迭代器函数。第二种方式更符合Lua的迭代器惯例,可以与泛型for循环无缝集成。
协同程序迭代器的真正优势在于处理复杂遍历逻辑。下面的例子实现了一个二叉树遍历器:
-- 二叉树节点定义
local function createTreeNode(value, left, right)
return {
value = value,
left = left,
right = right
}
end
-- 创建示例二叉树
-- A
-- / \
-- B C
-- / \ / \
-- D E F G
local tree = createTreeNode("A",
createTreeNode("B",
createTreeNode("D"),
createTreeNode("E")
),
createTreeNode("C",
createTreeNode("F"),
createTreeNode("G")
)
)
-- 使用协同程序实现二叉树中序遍历
function createInorderTreeIterator(root)
return coroutine.create(function()
local function traverse(node)
if node then
traverse(node.left)
coroutine.yield(node.value)
traverse(node.right)
end
end
traverse(root)
end)
end
-- 使用协同程序实现二叉树前序遍历
function createPreorderTreeIterator(root)
return coroutine.create(function()
local function traverse(node)
if node then
coroutine.yield(node.value)
traverse(node.left)
traverse(node.right)
end
end
traverse(root)
end)
end
-- 使用协同程序实现二叉树层序遍历
function createLevelOrderTreeIterator(root)
return coroutine.create(function()
if not root then
return
end
local queue = {root}
while #queue > 0 do
local node = table.remove(queue, 1)
coroutine.yield(node.value)
if node.left then
table.insert(queue, node.left)
end
if node.right then
table.insert(queue, node.right)
end
end
end)
end
-- 测试二叉树迭代器
print("\n二叉树协同程序迭代器演示:")
print("中序遍历:")
local inorderIterator = createInorderTreeIterator(tree)
while true do
local success, value = coroutine.resume(inorderIterator)
if not success or value == nil then
break
end
print(" " .. value)
end
print("前序遍历:")
local preorderIterator = createPreorderTreeIterator(tree)
while true do
local success, value = coroutine.resume(preorderIterator)
if not success or value == nil then
break
end
print(" " .. value)
end
print("层序遍历:")
local levelOrderIterator = createLevelOrderTreeIterator(tree)
while true do
local success, value = coroutine.resume(levelOrderIterator)
if not success or value == nil then
break
end
print(" " .. value)
end
这个例子展示了协同程序如何简化复杂数据结构的遍历。对于递归遍历(如中序和前序遍历),协同程序可以自然地处理递归调用栈,代码比传统的迭代器实现更加清晰。对于层序遍历,协同程序可以维护队列状态,而无需在迭代器函数外部管理。
协同程序迭代器另一个强大的应用是生成无限序列或复杂序列。下面的例子实现了一个素数生成器:
-- 使用协同程序实现素数生成器
function createPrimeNumberGenerator()
return coroutine.create(function()
-- 生成所有素数
local primes = {}
local function isPrime(num)
if num < 2 then
return false
end
for _, prime in ipairs(primes) do
if prime * prime > num then
break
end
if num % prime == 0 then
return false
end
end
return true
end
local num = 2
while true do
if isPrime(num) then
table.insert(primes, num)
coroutine.yield(num)
end
num = num + 1
end
end)
end
-- 使用协同程序实现斐波那契数列生成器
function createFibonacciGenerator()
return coroutine.create(function()
local a, b = 0, 1
coroutine.yield(a) -- 斐波那契数列的第一个数
coroutine.yield(b) -- 斐波那契数列的第二个数
while true do
a, b = b, a + b
coroutine.yield(b)
end
end)
end
-- 测试序列生成器
print("\n序列生成器演示:")
print("前10个素数:")
local primeGenerator = createPrimeNumberGenerator()
for i = 1, 10 do
local success, prime = coroutine.resume(primeGenerator)
if success then
print(" 素数 " .. i .. ": " .. prime)
end
end
print("前10个斐波那契数:")
local fibonacciGenerator = createFibonacciGenerator()
for i = 1, 10 do
local success, fib = coroutine.resume(fibonacciGenerator)
if success then
print(" 斐波那契数 " .. i .. ": " .. fib)
end
end
这些生成器可以无限生成序列,但通过协同程序的挂起机制,我们可以按需获取元素,避免不必要的计算。这对于处理大数据集或无限流特别有用。
协同程序迭代器还可以与管道模式结合,创建强大的数据处理链。下面的例子展示了如何将迭代器、过滤器和消费者组合在一起:
-- 组合迭代器、过滤器和消费者
print("\n迭代器管道演示:")
-- 创建数字序列迭代器
function createNumberSequenceIterator(startVal, endVal)
return coroutine.create(function()
for i = startVal, endVal do
coroutine.yield(i)
end
end)
end
-- 创建过滤器:只保留偶数
function createEvenFilter(inputIterator)
return coroutine.create(function()
while true do
local success, value = coroutine.resume(inputIterator)
if not success or value == nil then
break
end
if value % 2 == 0 then
coroutine.yield(value)
end
end
end)
end
-- 创建过滤器:平方转换
function createSquareFilter(inputIterator)
return coroutine.create(function()
while true do
local success, value = coroutine.resume(inputIterator)
if not success or value == nil then
break
end
coroutine.yield(value * value)
end
end)
end
-- 创建消费者:收集结果并计算总和
function createSumConsumer(inputIterator)
return coroutine.create(function()
local sum = 0
local count = 0
while true do
local success, value = coroutine.resume(inputIterator)
if not success or value == nil then
break
end
sum = sum + value
count = count + 1
print(" 处理值: " .. value .. ", 当前总和: " .. sum)
end
return sum, count
end)
end
-- 构建处理管道
local numberIterator = createNumberSequenceIterator(1, 10)
local evenFilter = createEvenFilter(numberIterator)
local squareFilter = createSquareFilter(evenFilter)
local sumConsumer = createSumConsumer(squareFilter)
-- 执行管道
local success, totalSum, itemCount = coroutine.resume(sumConsumer)
if success then
print(string.format("处理完成: 总计处理%d项,总和=%d", itemCount, totalSum))
end
-- 验证结果:1-10中的偶数为2,4,6,8,10,它们的平方和为4+16+36+64+100=220
print("验证: 2² + 4² + 6² + 8² + 10² = 4 + 16 + 36 + 64 + 100 = 220")
这个例子展示了如何将迭代器作为数据源,通过多个过滤器进行数据处理,最终由消费者汇总结果。每个组件都是独立的协同程序,通过yield和resume传递数据。这种架构非常灵活,可以轻松添加、移除或修改处理步骤。
基于协同程序的迭代器相比传统迭代器有以下优势:
- 状态管理简单:协同程序自动维护遍历状态,无需手动管理。
- 代码结构清晰:遍历逻辑可以写成顺序代码,而不是分散在多个函数中。
- 支持复杂控制流:可以轻松实现递归遍历、条件分支等复杂逻辑。
- 可组合性强:可以轻松将多个迭代器组合成处理管道。
在下一节中,我们将探讨协同程序在非抢先式多线程编程中的应用,这是协同程序最强大的特性之一。
9.4 非抢先式多线程编程实践
非抢先式多线程是一种并发编程模型,其中多个执行流(线程)共享同一个CPU核心,并且执行权的转移由线程自身控制,而不是由操作系统强制调度。在Lua中,协同程序是实现非抢先式多线程的理想工具。每个协同程序可以视为一个轻量级线程,它们协作共享CPU时间,通过显式的yield调用来让出执行权。
这种模型的优势在于避免了传统多线程编程中的竞态条件、死锁和锁机制复杂性,因为一次只有一个协同程序在执行。然而,这也意味着程序员必须小心设计协同程序之间的协作,避免某个协同程序长时间占用CPU。
非抢先式多线程特别适合I/O密集型应用,如网络服务器、游戏逻辑、用户界面等,其中大部分时间都在等待外部事件。下面我们通过几个例子来展示非抢先式多线程的实现和应用。
首先,我们实现一个简单的多任务调度器:
-- 简单的协同程序调度器
function createSimpleScheduler()
local tasks = {} -- 任务队列
local taskIdCounter = 0
local function addTask(taskFunction, ...)
taskIdCounter = taskIdCounter + 1
local task = {
id = taskIdCounter,
coroutine = coroutine.create(taskFunction),
args = {...}
}
table.insert(tasks, task)
return taskIdCounter
end
local function removeTask(taskId)
for i, task in ipairs(tasks) do
if task.id == taskId then
table.remove(tasks, i)
return true
end
end
return false
end
local function run()
while #tasks > 0 do
-- 循环调度所有任务
for i = #tasks, 1, -1 do -- 反向遍历以便安全删除
local task = tasks[i]
if coroutine.status(task.coroutine) == "dead" then
-- 任务已完成,从队列移除
table.remove(tasks, i)
else
-- 恢复任务执行
local success, shouldYield = coroutine.resume(task.coroutine, unpack(task.args))
if not success then
-- 任务出错
local errorMsg = shouldYield or "未知错误"
print(string.format("任务 %d 出错: %s", task.id, errorMsg))
table.remove(tasks, i)
elseif shouldYield == "SLEEP" then
-- 任务请求睡眠,暂时移除
table.remove(tasks, i)
-- 可以在这里实现睡眠逻辑,但在这个简单调度器中我们只是移除
print(string.format("任务 %d 进入睡眠", task.id))
end
end
end
-- 简单延迟,模拟时间片
-- 在实际应用中,这里可能是检查I/O事件或实际等待
os.execute("sleep 0.01") -- 注意:这个调用依赖于操作系统
end
print("所有任务执行完成")
end
return {
addTask = addTask,
removeTask = removeTask,
run = run
}
end
-- 示例任务函数
function sampleTask1(taskName)
for i = 1, 5 do
print(string.format("%s: 执行步骤 %d", taskName, i))
coroutine.yield() -- 让出执行权
end
print(string.format("%s: 完成", taskName))
end
function sampleTask2(taskName)
for i = 1, 3 do
print(string.format("%s: 处理项目 %d", taskName, i))
coroutine.yield() -- 让出执行权
if i == 2 then
print(string.format("%s: 模拟I/O等待", taskName))
coroutine.yield("SLEEP") -- 模拟I/O等待
end
end
print(string.format("%s: 完成", taskName))
end
-- 测试简单调度器
print("简单协同程序调度器演示:")
local scheduler = createSimpleScheduler()
scheduler.addTask(sampleTask1, "任务A")
scheduler.addTask(sampleTask2, "任务B")
scheduler.run()
这个简单调度器演示了非抢先式多线程的基本概念:多个任务交替执行,每个任务在适当的时候通过yield让出CPU。调度器循环遍历所有任务,恢复它们的执行,直到所有任务完成。
然而,上面的调度器还不够实用,因为它没有处理任务间的通信、事件等待等复杂场景。下面我们实现一个更完整的协作式多线程系统:
-- 高级协作式多线程系统
function createAdvancedThreadSystem()
local threads = {} -- 所有线程
local eventQueue = {} -- 事件队列
local threadIdCounter = 0
local running = true
-- 创建新线程
local function createThread(threadFunction, ...)
threadIdCounter = threadIdCounter + 1
local thread = {
id = threadIdCounter,
coroutine = coroutine.create(threadFunction),
status = "ready", -- ready, waiting, finished
waitEvent = nil, -- 等待的事件类型
data = {...} -- 线程数据
}
table.insert(threads, thread)
return threadIdCounter
end
-- 发送事件
local function sendEvent(eventType, eventData, targetThreadId)
local event = {
type = eventType,
data = eventData,
target = targetThreadId,
time = os.time()
}
table.insert(eventQueue, event)
-- 唤醒等待此事件的线程
for _, thread in ipairs(threads) do
if thread.status == "waiting" and
thread.waitEvent == eventType and
(targetThreadId == nil or thread.id == targetThreadId) then
thread.status = "ready"
thread.waitEvent = nil
end
end
end
-- 线程等待事件
local function waitForEvent(eventType)
local thread = nil
for _, t in ipairs(threads) do
if t.coroutine == coroutine.running() then
thread = t
break
end
end
if thread then
thread.status = "waiting"
thread.waitEvent = eventType
return coroutine.yield()
end
return nil
end
-- 线程睡眠
local function sleepThread(seconds)
local wakeupTime = os.time() + seconds
local thread = nil
for _, t in ipairs(threads) do
if t.coroutine == coroutine.running() then
thread = t
break
end
end
if thread then
thread.status = "waiting"
thread.waitEvent = "WAKEUP_" .. tostring(wakeupTime)
return coroutine.yield()
end
end
-- 调度器主循环
local function runScheduler()
while running do
local activeThreads = 0
-- 处理事件队列
local currentTime = os.time()
for i = #eventQueue, 1, -1 do
local event = eventQueue[i]
-- 检查超时事件(如睡眠唤醒)
if event.type:find("^WAKEUP_") then
local wakeupTime = tonumber(event.type:sub(8))
if currentTime >= wakeupTime then
sendEvent("WAKEUP", nil, event.target)
table.remove(eventQueue, i)
end
end
end
-- 执行所有就绪线程
for _, thread in ipairs(threads) do
if thread.status == "ready" then
activeThreads = activeThreads + 1
-- 恢复线程执行
local success, result = coroutine.resume(thread.coroutine, unpack(thread.data))
if not success then
-- 线程出错
thread.status = "finished"
print(string.format("线程 %d 出错: %s", thread.id, result or "未知错误"))
elseif coroutine.status(thread.coroutine) == "dead" then
-- 线程完成
thread.status = "finished"
print(string.format("线程 %d 完成", thread.id))
end
elseif thread.status == "waiting" then
activeThreads = activeThreads + 1 -- 等待中的线程也算活动线程
end
end
-- 移除已完成线程
for i = #threads, 1, -1 do
if threads[i].status == "finished" then
table.remove(threads, i)
end
end
-- 如果没有活动线程,退出调度器
if activeThreads == 0 then
running = false
print("调度器: 没有活动线程,退出")
break
end
-- 简单延迟,避免忙等待
os.execute("sleep 0.01")
end
end
-- 停止调度器
local function stopScheduler()
running = false
end
return {
createThread = createThread,
sendEvent = sendEvent,
waitForEvent = waitForEvent,
sleep = sleepThread,
run = runScheduler,
stop = stopScheduler
}
end
-- 示例线程函数
function workerThread(threadName, workCount)
print(string.format("[%s] 线程启动", threadName))
for i = 1, workCount do
print(string.format("[%s] 执行工作 %d/%d", threadName, i, workCount))
-- 模拟工作
os.execute("sleep 0.05")
-- 每完成2个工作,发送进度事件
if i % 2 == 0 then
local threadSystem = coroutine.yield() -- 获取线程系统引用
threadSystem.sendEvent("PROGRESS",
{thread = threadName, progress = i/workCount * 100})
end
-- 随机睡眠
if math.random() < 0.3 then
print(string.format("[%s] 进入睡眠", threadName))
local threadSystem = coroutine.yield() -- 获取线程系统引用
threadSystem.sleep(1) -- 睡眠1秒
print(string.format("[%s] 唤醒", threadName))
end
end
print(string.format("[%s] 线程完成", threadName))
end
function monitorThread()
print("[监控器] 监控线程启动")
local completed = 0
while completed < 2 do -- 等待2个工作线程完成
print("[监控器] 等待事件...")
local threadSystem = coroutine.yield() -- 获取线程系统引用
-- 等待任意事件
local event = threadSystem.waitForEvent(nil)
if event then
if event.type == "PROGRESS" then
print(string.format("[监控器] 收到进度: %s - %.1f%%",
event.data.thread, event.data.progress))
end
end
-- 简单延迟
os.execute("sleep 0.1")
end
print("[监控器] 监控线程完成")
end
-- 测试高级线程系统
print("\n高级协作式多线程系统演示:")
local threadSystem = createAdvancedThreadSystem()
-- 修改workerThread以接收线程系统作为参数
local function wrappedWorkerThread(threadSystemRef, threadName, workCount)
return workerThread(threadName, workCount)
end
-- 创建线程
threadSystem.createThread(wrappedWorkerThread, threadSystem, "工人A", 6)
threadSystem.createThread(wrappedWorkerThread, threadSystem, "工人B", 4)
threadSystem.createThread(monitorThread, threadSystem)
-- 运行调度器
print("启动调度器...")
threadSystem.run()
这个高级线程系统展示了非抢先式多线程的更多特性:
- 事件驱动:线程可以等待和发送事件。
- 睡眠机制:线程可以睡眠指定时间。
- 线程监控:专门的监控线程可以观察系统状态。
- 错误处理:系统可以捕获和处理线程错误。
非抢先式多线程在实际应用中有许多用途。下面我们实现一个简单的网络服务器模拟:
-- 模拟非抢先式多线程网络服务器
function createNetworkServerSimulator()
local connections = {}
local connectionIdCounter = 0
-- 模拟连接处理线程
function connectionHandler(connectionId, clientAddress)
print(string.format("[连接 %d] 处理来自 %s 的连接",
connectionId, clientAddress))
-- 模拟接收数据
for i = 1, 3 do
print(string.format("[连接 %d] 等待数据包 %d...", connectionId, i))
-- 模拟网络延迟
os.execute("sleep 0.5")
-- 模拟接收数据
local data = string.format("数据包%d_来自%s", i, clientAddress)
print(string.format("[连接 %d] 收到: %s", connectionId, data))
-- 模拟处理数据
os.execute("sleep 0.2")
local response = string.format("响应%d_连接%d", i, connectionId)
print(string.format("[连接 %d] 发送: %s", connectionId, response))
-- 让出执行权,模拟非阻塞I/O
coroutine.yield()
end
print(string.format("[连接 %d] 连接关闭", connectionId))
return connectionId
end
-- 模拟接受连接线程
function acceptConnections()
print("[服务器] 开始接受连接")
for i = 1, 3 do -- 模拟3个客户端连接
print(string.format("[服务器] 等待新连接..."))
-- 模拟连接到达延迟
os.execute("sleep 1")
connectionIdCounter = connectionIdCounter + 1
local clientAddress = string.format("192.168.1.%d", 100 + i)
print(string.format("[服务器] 接受新连接: ID=%d, 地址=%s",
connectionIdCounter, clientAddress))
-- 为新连接创建处理线程
local handler = coroutine.create(connectionHandler)
connections[connectionIdCounter] = {
coroutine = handler,
address = clientAddress,
status = "active"
}
-- 启动连接处理
coroutine.resume(handler, connectionIdCounter, clientAddress)
-- 让出执行权,以便处理其他连接
coroutine.yield()
end
print("[服务器] 停止接受新连接")
end
-- 连接调度器
function connectionScheduler()
print("[调度器] 启动连接调度器")
-- 启动连接接受线程
local acceptor = coroutine.create(acceptConnections)
local activeConnections = true
while activeConnections do
activeConnections = false
-- 恢复连接接受线程
if coroutine.status(acceptor) ~= "dead" then
activeConnections = true
coroutine.resume(acceptor)
end
-- 恢复所有活动连接的处理线程
for connId, conn in pairs(connections) do
if conn.status == "active" then
activeConnections = true
if coroutine.status(conn.coroutine) == "dead" then
conn.status = "closed"
print(string.format("[调度器] 连接 %d 处理完成", connId))
else
coroutine.resume(conn.coroutine)
end
end
end
-- 简单延迟
os.execute("sleep 0.1")
end
print("[调度器] 所有连接处理完成")
end
return {
start = connectionScheduler
}
end
-- 测试网络服务器模拟
print("\n非抢先式多线程网络服务器模拟:")
local server = createNetworkServerSimulator()
server.start()
这个网络服务器模拟展示了非抢先式多线程如何用于处理并发连接。每个连接在一个独立的协同程序中处理,但所有连接共享同一个CPU核心,通过yield调用来协作。这种模型避免了传统多线程中的锁竞争,简化了并发编程。
非抢先式多线程的优缺点总结:
优点:
- 简化并发编程:无需锁机制,避免竞态条件。
- 资源高效:线程是轻量级的,创建和切换开销小。
- 确定性行为:执行顺序可控,便于调试和测试。
- 避免上下文切换开销:所有线程在同一个操作系统线程中运行。
缺点:
- 不支持真正并行:无法利用多核CPU。
- 编程复杂性:需要小心设计协作点,避免线程饿死。
- 阻塞操作会阻塞所有线程:如果一个线程执行了阻塞操作,整个程序都会阻塞。
在实践中,非抢先式多线程通常与事件循环结合使用,如Lua的LuaSocket库或OpenResty中的ngx_lua模块。这些系统使用非阻塞I/O,当需要等待I/O时,协同程序让出执行权,事件循环在I/O就绪时恢复相应的协同程序。
总结
本章深入探讨了Lua协同程序的各个方面,从基础操作到高级应用。我们首先介绍了协同程序的基本原理,包括创建、启动、挂起和恢复等操作,并通过实例展示了协同程序的状态管理能力。接着,我们探讨了如何使用协同程序实现管道与过滤器模式,构建灵活的数据处理流水线。然后,我们展示了基于协同程序的迭代器实现,这种实现方式简化了复杂数据结构的遍历和无限序列的生成。最后,我们深入研究了非抢先式多线程编程,实现了任务调度器和模拟网络服务器,展示了协同程序在并发编程中的强大能力。
协同程序是Lua语言中一项独特而强大的特性,它提供了一种轻量级的并发模型,特别适合处理I/O密集型任务、状态机实现和复杂控制流。通过协同程序,开发者可以编写出清晰、高效且易于维护的并发代码,而无需面对传统多线程编程中的复杂性。无论是简单的数据转换管道,还是复杂的协作式多线程系统,协同程序都能提供优雅的解决方案。
掌握协同程序需要实践和经验。建议读者从简单例子开始,逐步尝试更复杂的应用场景,如游戏开发中的状态管理、网络编程中的并发处理,或数据处理中的流水线构建。通过不断实践,你将能充分发挥协同程序的潜力,编写出高质量的Lua代码。
475

被折叠的 条评论
为什么被折叠?



