chapter9_4 非抢占式的多线程

  协同程序与常规的多线程不同之处:协同程序是非抢占式的。

当一个协同程序运行时,是无法从外部停止它的。只有当协同程序显式地调用yield时,它才会停止。

当不存在抢先时,编程会变得简单很多,无须为同步的bug抓狂。

在程序中所有的同步都是显式的,只需要确保一个协同程序在它的临界区域之外调用yield即可。

对于这样非抢占式的多线程来说,只要有一个线程调用了一个阻塞操作,整个程序在该操作完成前,都会停止下来。

下面用一个有趣的方法来解决这个问题:通过HTTP下载几个远程文件。

下面的例子测试下载lua源代码,其中会用到LuaSocket模块:

local socket = require "socket"

local host = "www.lua.org"
local file1 = "/ftp/lua-5.3.3.tar.gz "

local HTTP = "HTTP/1.0\r\nUser-Agent: Wget/1.12 (linux-gnu)\r\nAccept: */*\r\nHost: www.lua.org\r\nConnection: Keep-Alive\r\n\r\n"

local sock = assert(socket.connect(host1,80))
sock:send("GET " .. file1 ..  HTTP)

repeat
    local chunk,status,partial = sock:receive(4096)
    print("chuck:size:  ",string.len(chunk or partial),status or "ok")
until status == "closed"

sock:close()

在正常情况下receive函数会返回一个字符串。若发生错误,则返回nil,并且附加错误码及出错前读取到的内容(partial).

接下来下载几个文件,最笨的办法就是逐个下载,但是太慢。程序大部分时间花费在等待数据的接收上。

更明确地说,是将时间花在了receive阻塞调用上。

采用的解决办法是,当一个链接没有可用数据时,程序便可以从其他链接出读取数据。

很明显协同程序提供了一种简便的方式来构建这种并发下载。

为每个下载任务创建一个新的线程,只要一个线程无可用数据,它就把控制权转让给一个简单的调度程序。

而这个调度程序则会调用其他下载线程。

在以协同程序来重写程序,先将前面的下载代码重新写:

function receive(connection)
    local s,status,partial = connection:receive(2^10)
    return s or partial,status
end 

function download(host,file)
    local sock = assert(socket.connect(host,80))
    local count = 0            --记录接收到的字节数
    sock:send("GET " .. file ..  HTTP)
    repeat
        local chunk,status = receive(sock)
        count = count + #chunk
    until status == "closed"
    sock:close()
    print(file,count)
end
download(host,file1)

这是下载一个文件的函数封装,只需调用download就可以。单独下载一个文件需要18秒左右。

但是在并发的情况中,receive代码不能阻塞,因此在它没有可用数据时应该挂起:

function receive(connection)
    connection:settimeout(0)    --设置为非阻塞
    local s,status,partial = connection:receive(2^10)
    if status == "timeout" thencoroutine.yield(connection)
    end
    return s or partial,status
end 

settimeout的调用,使得对此链接的操作不会阻塞。

即使在超时的情况下,连接也是会返回已经读取到的内容,即记录在partial变量中。

以下代码用table threads为调度程序保存所有正在运行中的线程。

get函数保证每个下载任务都在一个独立的线程中执行。

调度程序本身主要就是一个循环,遍历所有的线程,逐个唤醒它们的执行。

当有线程完成时,就将该线程从列表中删除。

threads = {}             --保存活跃线程的表
function get(host,file)
    local co = coroutine.create(function() --创建协同程序
        download(host,file)
    end)
    table.insert(threads,co)      --插入列表
end 

function dispatch()
    local i = 1
    while true do
        if threads[i] == nil then        --没有线程了
            if threads[1] == nil then break end --表是空表吗
            i = 1                        --重新开始循环
        end
        local status,res = coroutine.resume(threads[i]) --唤醒改线程继续下载文件
        if not res then             --线程是否已经完成了任务
            table.remove(threads,i) --移除list中第i个线程
        else
            i = i + 1               --检查下一个线程
        end
    end
end

最后,主程序需要创建所有的线程,并调用调度程序。

local file1 = "/ftp/lua-5.3.3.tar.gz "
local file2 = "/ftp/lua-5.3.2.tar.gz "
local file3 = "/ftp/lua-5.3.1.tar.gz "
local file4 = "/ftp/lua-5.3.0.tar.gz "
local file5 = "/ftp/lua-5.2.4.tar.gz "
local file6 = "/ftp/lua-5.2.3.tar.gz "
local file7 = "/ftp/lua-5.2.2.tar.gz "
local file8 = "/ftp/lua-5.2.1.tar.gz "
local file9 = "/ftp/lua-5.2.0.tar.gz "
get(host,file1)
get(host,file2)
get(host,file3)
get(host,file4)
get(host,file5)
get(host,file6)
get(host,file7)
get(host,file8)
get(host,file9)

dispatch() --main loop

同时下载9个文件总共耗时36秒,比串行下载9个文件速度快很多。

但是发现CPU占用率跑到98%。

为了避免这样的情况,可以使用LuaSocket中的select函数(socket.select(recvt, sendt [, timeout]))。

在等待时陷入阻塞状态,若要在当前实现中应用这个函数,只需要修该调度即可:

function dispatch_new()
    local i = 1
    local timedout = {}                  --Recvt 集合
    while true do
        if threads[i] == nil then        --没有线程了
            if threads[1] == nil then break end --表是空表
            i = 1                        --重新开始循环
            timedout = {}                --遍历完所有线程,开始新一轮的遍历
        end
        local status,res = coroutine.resume(threads[i]) --唤醒该线程继续下载文件
        if not res then               --若完成了res就为nil,只有status一个返回值true。否则res为yield传入的参数connection。
            table.remove(threads,i)   --移除list中第i个线程
        else
            i = i + 1                 --检查下一个线程
            timedout[#timedout +1] = res
            if #timedout == #threads then  --所有线程都阻塞了吗?
                socket.select(timedout)    --如果线程有数据,就返回
            end
        end
    end
end
... ...
dispatch_new() --main loop

receive会将超时的连接通过yield传给resume的res。如果所有的连接都超时了,调度程序就用select来等待这些链接的状态发生变化。

最后运行改良版的程序后,9个文件下载总耗时24秒,cpu占用率不到5%。

时间: 2024-10-07 20:39:45

chapter9_4 非抢占式的多线程的相关文章

抢占式内核与非抢占式内核

2013-02-06 10:43 3269人阅读 评论(3) 收藏 举报  分类: [OS](4)  版权声明:本文为博主原创文章,未经博主允许不得转载. 目录(?)[+] UNIX采用抢占式内核,Linux采用非抢占式内核 内核抢占(可抢占式内核):即当进程位于内核空间时,有一个更高优先级的任务出现时,如果当前内核允许抢占,则可以将当前任务挂起,执行优先级更高的进程. 非抢占式内核:高优先级的进程不能中止正在内核中运行的低优先级的进程而抢占CPU运行.进程一旦处于核心态(例如用户进程执行系统调

抢占式内核与非抢占式内核中的自旋锁(spinlock)的区别

一.概括 (1)自旋锁适用于SMP系统,UP系统用spinlock是作死. (2)保护模式下禁止内核抢占的方法:1.执行终端服务例程时2.执行软中断和tasklet时3.设置本地CPU计数器preempt_count (3)自旋锁的忙等待的实际意义是:尝试获取自旋锁的另一个进程不断尝试获取被占用的自旋锁,中间只pause一下! (4)在抢占式内核的spin_lock宏中,第一次关抢占,目的是防止死锁(防止一个已经获取自旋锁而未释放的进程被抢占!!).而后又开抢占,目的是让已经释放自旋锁的进程可以

Java基础知识强化之多线程笔记07:同步、异步、阻塞式、非阻塞式 的联系与区别

1. 同步: 所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回.但是一旦调用返回,就必须先得到返回值了. 换句话话说,调用者主动等待这个"调用"的结果. 对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已. 2. 异步: 所谓异步,"调用"在发出之后,这个调用就直接返回了,所以没有返回结果. 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果.而是在"调用"发出后,"被调用者&q

Java基础:非阻塞式IO

转载请注明出处:jiq?钦's technical Blog 引言 JDK1.4中引入了NIO,即New IO,目的在于提高IO速度.特别注意JavaNIO不完全是非阻塞式IO(No-Blocking IO),因为其中部分通道(如FileChannel)只能运行在阻塞模式下,而其他的通道可以在阻塞式和非阻塞式之间进行选择. 尽管这样,我们还是习惯将Java NIO看作是非阻塞式IO,而前面介绍的面向流(字节/字符)的IO类库则是非阻塞的,详细来看,两者区别如下: IO NIO 面向流(Strea

Android基于AOP的非侵入式监控之——AspectJ实战

一引言 二什么是AspectJ 1 它只是一个代码编译器 2 它是用来做AOP编程的 3为什么要用AspectJ 三AspectJ原理与运用 1 基本原理 2 使用方式 21 纯注解方式 22 AspectJ语言 23 结合自定义注解使用 四AspectJ实战监听方法执行耗时打印并输出 五一些比较常见的问题 六推荐文章 一.引言 本博文的目的不是详细的介绍AspectJ的细节,而是最近项目用到了AspectJ,因此对其作了一些使用和重要概念上的总结. 相信很多做过Web的同学对AspectJ都不

Java NIO实现非阻塞式socket通信

博主知识水平有限,只能提供一个个人的狭隘的理解,如果有新人读到这儿,建议看一下其他教程或者API,如果不明白,再来看一下:如果有dalao读到这儿,希望能指出理解中的问题~谢谢 Java提供了用于网络通信的socket和serversocket包,然而实现方式是阻塞式的,同一时间点上只能进行一个连接,这会带来不好的体验.当然了,我们也可以通过不断创建线程的方式管理连接,但线程多了的话反而会降低效率.于是Java推出了非阻塞式IO--channel.并且channel提供关于网络通信的相关chan

非入侵式JavaScript

非入侵式JavaScript针对哪些情况: 1.并不是每个人的浏览器都支持JavaScript.需要让每个人都能看到全部内容,并且无须在浏览器中执行代码 就能使用该应用. 2.一些运行方式非常奇怪的浏览器.比如视觉受损的人会使用屏幕阅读器,一些手机用户无法使用含有 JavaScript的站点. 3.JavaScript在不同的平台上运行方式不同,IE是造成这一问题的罪魁祸首.需要根据不用的浏览器编写不同 的事件处理代码. 4.这些事件处理器都会引用全局命名空间中的函数.如果想把其他类库集成进来,

报错:非介入式客户端验证规则中的验证类型名称必须唯一。下列验证类型出现重复

当在ASP.NET MVC中,针对一个Model进行添加操作的时候,报如下错误: [InvalidOperationException: 非介入式客户端验证规则中的验证类型名称必须唯一.下列验证类型出现重复: range] 原因是第三方验证程序集和MVC固有验证发生名称的冲突. 解决办法:在Model中属性上注释掉第三方程序集提供的验证特性.

(单例设计模式中)懒汉式与饿汉式在多线程中的不同

/* 目的:分析一下单例设计模式中,懒汉式与饿汉式在多线程中的不同! 开发时我们一般选择饿汉式,因为它简单明了,多线程中不会出现安全问题! 而饿汉式需要我们自己处理程序中存在的安全隐患,但是饿汉式的程序技术含量更高! */ /* class SinglePerson implements Runnable{ private static SinglePerson ss = new SinglePerson("hjz", 22);//恶汉式 private int age; privat