V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
gantleman
V2EX  ›  程序员

拯救多线程混乱的 pelagia

  •  
  •   gantleman ·
    gantleman · 2020-07-13 21:28:40 +08:00 · 12045 次点击
    这是一个创建于 1597 天前的主题,其中的信息可能已经有所发展或是发生改变。

    全文地址: gameinstitute.qq.com/community/detail/133448

    导致多线程混乱的依赖和死锁问题。

    在介绍各种多线程开发工具前我先要介绍下多线程下最大的二个问题。

    第一个问题就是任务执行的依赖关系。在不同线程间执行的任务代码会有先后次序的需求。但因为多线程的特性会导致这些代码被同时执行。那么需要一种方法来控制多线程间代码的依赖关系。违反这种依赖关系可能会导致数据的丢失。执行过程的严重错误。甚至崩溃,死机。

    第二个问题就是死锁。 在多线程执行过程中,多个线程会访问同一个数据。如果其中一个线程锁住了一个数据,然后请求另一个数据的锁。而其他线程刚好锁住了他请求中的数据,并也刚好请求他拥有的数据。就会导致两个线程互相等待系统卡死的问题。

    为什么多线程下这两个问题是核心问题? 因为这两个问题的出现与多线程软件规模成正比,也就是说随着多线程软件规模的增加,这两个问题出现的概率也在增加。他们像达克摩斯之剑一样悬挂在任何多线程项目的上空。

    pelagia

    是开源项目其地址在 github.com/surparallel/pelagia

    pelagia 是由 surparallel open source 推出的开源项目,是唯一同时解决依赖和死锁的多线程工具。 在前面我简单的介绍了多线程工具的现状。我们知道依赖和死锁是多线程开发中必须要解决的两个问题。并不是简单的因为依赖和死锁会有概率的出现在多线程项目中。而是因为在多线程项目中存在指数规模效应的问题。

    所谓指数规模效应是指假设一个项目有两个线程,如果你新添加一个线程,那么两个线程间发生依赖和死锁,就有 3 的 2 次方即 9 种可能。如果有 99 个线程,你同样是新添加一个线程,那么发生依赖和死锁的可能就变成了 100 的 2 次方种可能。也就是说可能发生依赖和死锁的状况,随着开发任务的增加成指数级别的增加。

    这样就导致每开发一个新的线程任务就要和其余的线程任务做仔细的检查。检查是否可能出现依赖和死锁。而这种检查的规模也是程指数增加的。这样就导致我们开发的多线程项目很快就会达到团队所能承受的极限。

    因为指数规模效应的存在就必然会有祖传代码拍死老师傅。为了避免老师傅们被祖传代码拍死。pelagia 结合消息通信和数据分区两个技术,同时解决依赖和死锁问题,彻底拯救了老师傅。

    pelagia 目前处于准完成阶段。大部分功能已经开发完毕。包括消息通信的统计系统,支持多线程的日志系统,多线程数据分区的存储系统。其硬盘储存系统性能略快于 redis 。

    39 条回复    2020-08-10 14:37:51 +08:00
    VDimos
        1
    VDimos  
       2020-07-19 11:31:34 +08:00 via Android
    这个论坛用 c 的多线程,看起来不是很多呀
    mightofcode
        2
    mightofcode  
       2020-07-20 10:56:48 +08:00
    哦豁
    jones2000
        3
    jones2000  
       2020-07-22 01:00:19 +08:00
    死锁了, 直接用 windbg 调试,导入对应的 pdb,是可以看到锁变量信息和对应上锁的代码函数调用。 不过这个是 windows 的调试方法。 其他平台就不清楚。
    gantleman
        4
    gantleman  
    OP
       2020-07-22 01:54:59 +08:00
    @jones2000 死锁是工程不可控的表现。本质问题是工程的不可控,不可控就意味,今天不会出问题不代表永远不会出问题。一个人的项目不出问题不代表团队也不出问题。靠人力和调试可以解决这个问题,前提是可以调试的环境。多线程软件运行在汽车上,飞机上,火箭上。出现死锁就会造成巨大的财产损失。也不会有机会去调试。所以多线程的广泛应用就需要彻底消灭死锁的问题。
    gantleman
        5
    gantleman  
    OP
       2020-07-24 10:04:49 +08:00
    @VDimos 目前支撑使用 c,c++,lua 。嵌入 js 也在开发中。
    ihciah
        6
    ihciah  
       2020-07-29 18:01:02 +08:00   ❤️ 2
    看了很多 pelagia 的宣传,但是都感觉有点空啊?能讲讲实现原理或者细节吗?
    Yut
        7
    Yut  
       2020-07-29 22:16:54 +08:00
    不是,死锁完全就是代码设计问题,靠这种打补丁修有什么用嘛。
    送老哥一句话
    “Let it crash”
    sgissb1
        8
    sgissb1  
       2020-07-29 22:37:23 +08:00
    死锁的出现,一般有三种:
    1,疏忽大意
    2,根本不懂多线程是什么
    3,不懂最基本的代码设计和模块排列,就装架构师,模仿设计模式搞事情,或者创造新的设计模式。

    一般第一种也很多,但对于线程操作熟练工和代码模块设计和排列的熟练工来说,稍微好一点的代码习惯这个问题就会少很多。

    现在不管 c 还是 c++程序员,第二种和第三种居多。因为 c/c++和 java 类开发工具的线程调度模型稍有区别,主要是没有类似 gc 等会打断线程的开发语言特性的软 /硬中断。

    所以其线程相当接近硬件调度模型和操作系统调度模型的情况。做非嵌入式的,操作系统和硬件都稍微了解一些,在平时养成好一点的代码习惯,加上不要没事瞎装架构师,算法工程师,操练个 2-3 年的同时渐渐从上到下(指底层调度)的做个了解,基本没有太大问题。

    工程问题在于取舍,而取舍在于经验,正确的经验积累在于正确正确的基础知识和踏实。可惜现在程序员和韭菜一样,一茬一茬的割,加上“我的代码拯救世界”的心态流行。本来很不算太难的事情,其实很难。
    ysmood
        9
    ysmood  
       2020-07-29 22:49:54 +08:00   ❤️ 2
    一千星的项目一个 PR 都没有,全部 issue 都是作者自己创建的。我更希望能分享下推广的经验?
    gantleman
        10
    gantleman  
    OP
       2020-07-31 04:01:02 +08:00
    @sgissb1 作为一个工具,需要在给定条件下能获得准确的结果。如果汽车在 80%的时间正常,20%的时间可能原地爆炸。在工程上又给不出爆炸的原因,那么汽车这个工具就是无法使用的工具。对于多线程来说排除死锁非常困难,导致多线程无法被广泛使用。首先就要解决需要人为因素介入的问题。在给定条件下保证多线程可以 100%正常运行。排除人为因素的干扰也是工具存在的意义之一。
    gantleman
        11
    gantleman  
    OP
       2020-07-31 04:25:14 +08:00
    @ysmood 解决多线程问题是非常冷门的领域,在全世界关心这方面技术的人都非常少。激进的营销策略只是为了找到有共同兴趣的朋友一起分享开源技术。我做的工作哪怕最终只对很少的人有帮助,对我也是非常有意义的。做营销肯定要大嗓门喊出去,做事就要有做事的样子和觉悟。如果有冒犯之处请您多包含。
    mind3x
        12
    mind3x  
       2020-07-31 06:31:37 +08:00
    看了一下官方示例 https://github.com/surparallel/pelagia/blob/master/src/psimple.c
    只能说,这不像是写了十几年 C 啊:


    static int TaskRouting(char* value, short valueLen) {
    void* pEvent;
    memcpy(&pEvent, value, valueLen);

    这 memcpy 心够大的……
    gantleman
        13
    gantleman  
    OP
       2020-07-31 08:24:50 +08:00
    @mind3x 请教问题具体指什么?
    gantleman
        14
    gantleman  
    OP
       2020-07-31 09:17:50 +08:00
    @mind3x 你是第二个和我提 memcpy 不能这样用。但都没说为什么不能这样用,或者这样用会导致什么问题,或者标准的用法应该是什么。每个都能能力不足的地方,谢谢您指正我的问题。
    tcfenix
        15
    tcfenix  
       2020-07-31 10:14:50 +08:00   ❤️ 1
    @gantleman
    不是很熟悉 C,所以想请教一下,memcpy 之前不需要 malloc 一下么?还是在 C 里面 void* pEvent;就会直接分配内存空间?
    gantleman
        16
    gantleman  
    OP
       2020-07-31 10:25:21 +08:00
    @tcfenix 因为传进来的值就是指针,不用再分配堆空间了,直接复制到指针变量里就可以了。
    sgissb1
        17
    sgissb1  
       2020-07-31 12:08:50 +08:00
    @gantleman 意思说,如果你不是程序员,去造汽车,你也能保证 100%不原地爆炸?

    你整个回复,就是在非给定条件下,汽车原地爆炸。在给定条件下,你线程 100%正常。不理解你想说什么
    gantleman
        18
    gantleman  
    OP
       2020-07-31 12:24:39 +08:00
    @sgissb1 我的表达能力很差,写代码久了确实不太会和人相处。发明工具的目的是帮助程序员更好的使用多线程。降低多线程开发的使用门槛。
    lwlizhe
        19
    lwlizhe  
       2020-07-31 14:20:24 +08:00
    这项目为啥 star 那么多,fork 那么少……

    我这还一批 fork 了不点 star 的……
    gantleman
        20
    gantleman  
    OP
       2020-07-31 17:59:59 +08:00
    @lwlizhe 对自己的项目有信心,当然就要花钱请公司帮忙推广呀!现在是金钱社会啦。美国总统拉选票都要做广告。你需要 star 的话我把广告公司的联系方式给你呀!国内各大公司都是他们的老客户。广告的钱还是要花的不能省。
    felixlong
        21
    felixlong  
       2020-07-31 22:55:05 +08:00
    @gantleman 不知所云。有这时间打两把王者荣耀不好吗?
    mind3x
        22
    mind3x  
       2020-08-01 03:29:00 +08:00   ❤️ 1
    @gantleman C 程序员需要特别小心内存越界。你的 pEvent 只在栈上分配了 4 字节(32 位)或 8 字节(64 位)的空间,然而你的 memcpy 对传进来的 valueLen 不加任何检查,调用者轻易就可以用自己构造的数据覆盖栈上不属于 pEvent 的空间。这是个很基础的问题,随手搜一下有很多文章,例如 https://www.jianshu.com/p/47d484b9227e
    gantleman
        23
    gantleman  
    OP
       2020-08-01 04:35:07 +08:00
    @mind3x 谢谢您的提醒。如你所说防御编程只针对调用者或第三方。这个示例中调用者和开发者都是用户本人。并且此处的输入的长度和输出的长度都被明显指出没有隐式调用。对内存越界的恐慌和焦虑常见于有一定经验的开发者。过度的防御编程并不能解决这个问题。代码的整洁和语法的准确,对调用链层级的有效控制会更有效的解决内存溢出问题。再次感谢您对 pelagia 的关注。
    mind3x
        24
    mind3x  
       2020-08-01 05:31:47 +08:00   ❤️ 7
    @gantleman
    我回复你上一条是以为你真的想了解自己的代码糟糕在哪里,结果看来并不是。

    我翻译一下我上面的回答和你的答复:
    我:这是个基本的常识。基,本,的,常,识。
    你:我自己的代码,爱怎么写就怎么写。

    那我也不用那么客气了。我 1995 年开始写 C,从底层一路写上来,你还没听说过线程的时候我就在写线程调度器了。没见过写成这样还这么理直气壮的,“过度的防御编程并不能解决这个问题”。早拉黑得了,浪费自己生命。
    gantleman
        25
    gantleman  
    OP
       2020-08-01 05:50:11 +08:00
    @mind3x 过度防御编程会带来两个问题。第一,c 和 c++的操作分为内存和计算两种,所有内存操作极端情况下都需要防御。那么就意味着需要额外增加 1/3 的代码来做防御。第二,防御编程的结果是提醒开发者,你的编程出错了。无论在开发阶段还是用户运行阶段这都会导致调用栈信息的丢失。以调用栈信息的丢失换来没有意义的日志输出得不偿失。所以过度防御危害更大。不要那么大火气,有些事情说清楚其实很简单。
    tairan2006
        26
    tairan2006  
       2020-08-02 19:09:14 +08:00   ❤️ 1
    @gantleman 你说的这个问题是可以解决的,合约化编程加注释,或者 debug 模式下运行,release 的时候不跑都行。但是口头约定是不行的,除非这代码你就没打算让别人提交 patch…
    gantleman
        27
    gantleman  
    OP
       2020-08-02 21:41:32 +08:00
    @tairan2006 我想合约化编程或着人与人的约定是不能解决这个问题的。保证代码的强壮的方法是要深思熟虑,内部讨论,反复 review,交叉 review,白盒测试,黑盒测试,压力测试。没有任何开源软件会仅凭人与人的约定(口头,注释或其他)就允许提交 path 。既然选择了开源的方式,我欢迎有能力保障开源社区健康发展的人,在深入了解 pelagia 项目之后加入进来。
    随着项目的发展,我对提交代码越来越谨慎。前天我在考虑使用 pelagia 处理广告集合竟价中存在的功能缺陷。通常这种情况下我会先列个功能提纲,尽可能把想到的功能都加上去。而通常两三天后我就会发现有些功能是多余的。反复思考后发现只有补充一到两个接口就可以了。有些极端情况,例如处理“相位技术”就需要创建大量的缓存镜像。可能会导致内存急速膨胀。我犹豫了半年多要不要加入进来。因为在单片机或工业机上对内存要求会比较苛刻。通常情况下不会需要大量存储换效率的情况。
    no1xsyzy
        28
    no1xsyzy  
       2020-08-05 14:30:43 +08:00
    @gantleman #23 俺还在想你这 valueLen 不管是多少都会导致破坏调用栈,结果传的是编译期常量 sizeof(void*)
    这还要传个参,这叫脱裤子放屁
    这还不是搜索 TaskRouting 调用方查出来,搜索到 functionPoint 就找不下去了,除了 pjob.c 之外没有任何地方包含了 functionPoint,而该文件内也只定义了它。
    不是很懂怎么办到的,用数字偏移量?用数字偏移量不写注释写半天没什么意义的注释干什么?
    看了下,RoutingFun 必须传一个 char* 一个 short,看来对于 C 传任意数量参数调用的技巧不是很熟。

    项目如其名,佩拉吉奥斯‧塞普汀三世 —— 疯狂且自大
    no1xsyzy
        29
    no1xsyzy  
       2020-08-05 14:31:23 +08:00
    @gantleman #27 @gantleman #23 自相矛盾。
    gantleman
        30
    gantleman  
    OP
       2020-08-05 17:19:22 +08:00
    @no1xsyzy 推荐你看下另一帖子 www.v2ex.com/t/695286#reply20 。我这人确实有些狂妄,因为我一直在并行技术的最前沿疯狂尝试。解决一些国内几年都解决不了的技术问题并分享给大家。开发过程中每个人使用语言的习惯都不相同。可以贴出来不好懂的代码我来慢慢讲。
    no1xsyzy
        31
    no1xsyzy  
       2020-08-05 19:11:28 +08:00
    @gantleman #30 我的调查路线是这样的,#12 说了 psimple.c@TaskRouting
    就 Search in repository 搜索 TaskRouting,唯一一处使用是 psimple.c#L67,传递给 plg_JobCreateFunPtr
    继续搜索,其实现在 pjob.c#L205,而传递来的函数指针被赋给 pEventPorcess->functionPoint
    然后搜索 functionPoint 只找到 struct 定义里找到。也就是说这东西就这么被吃了,再也没有被用上……
    xsen
        32
    xsen  
       2020-08-07 11:10:56 +08:00   ❤️ 1
    #1/#2 两个问题,都是架构设计的问题,还有就是管理的问题
    gantleman
        33
    gantleman  
    OP
       2020-08-07 12:02:55 +08:00
    @xsen 是的,软件工程的特点就在不断进化。对于纸带机所有开发和编译都是设计管理问题。我们知道后来这些都被编译器和操作系统取代了。软件工程的自动化,平台化,排斥人工的特点是诞生之日起就深入骨髓的。
    gantleman
        34
    gantleman  
    OP
       2020-08-08 23:01:56 +08:00
    @no1xsyzy 很抱歉最近很忙,在准备用 pelagia 冲刺 3D 同屏 1 万人的目标。functionPoint 会在 3 个地方被使用。分别是 job 的 885 行,919 行,954 行,和用户相关的是 919 行这段。

    这段表示表示如果是 C 函数指针类型直接执行调用执行,如果失败将回滚数据库事物。
    if (pEventPorcess->scriptType == ST_PTR) {

    if (0 == pEventPorcess->functionPoint(pOrderPacket->value, plg_sdsLen(pOrderPacket->value))) {
    job_Rollback(pJobHandle);
    }
    }
    no1xsyzy
        35
    no1xsyzy  
       2020-08-09 00:16:19 +08:00   ❤️ 1
    @gantleman #34 哦是我没仔细看 Github Repo 内搜索的前提,同一文件只会显示前两处匹配……
    不过在尝试写一个双链表 insert 写得一塌糊涂后明白自己写 C 确实太少,尤其指针的表记都得想半天。还是算了。
    在我经历过的学习路线图上 C 只是 ASM 的伪代码(
    swulling
        36
    swulling  
       2020-08-10 09:04:52 +08:00
    @gantleman
    > 我想合约化编程或着人与人的约定是不能解决这个问题的。保证代码的强壮的方法是要深思熟虑,内部讨论,反复 review,交叉 review,白盒测试,黑盒测试,压力测试。没有任何开源软件会仅凭人与人的约定(口头,注释或其他)就允许提交 path 。

    良好且严格的代码规范,合理的注释和文档。
    这两者的重要性在某些人眼里比 Review 、Test 要低,其实并不然。

    此外,从你的代码风格和上面的某些回复合理猜测主要在做个人编程或者小团队开发,否则不会养成这些习惯。
    gantleman
        37
    gantleman  
    OP
       2020-08-10 10:41:08 +08:00
    @swulling 在讨论任何事情的时候都要设置一个合理的前提,pelagia 就是一个小团队项目。为了最大可能兼容单片机和任何其他种类的客户端所以不会引入网络功能。预计最终代码不会超过 5 万行。随着项目的稳定也必然会添加防御相关的代码。工程开发有其过程,核心功能都不完备的情况就添加各种防御,哪么核心功能发生变化后防御代码怎么办?这些防御的代码文或档在大团队下对团队协作是必须。我带团队也天天强调文档和防御的重要性。强调文档的重要并不意味真的就比核心功能还重要。核心功能都没有的话,这个帖子就不存在了。以上内容都是我猜测的,因为你没有说出为什么文档比核心功能更重要的原因。希望能坦诚告诉我。至于开发习惯,是不是因为我一直写个人项目或则小团队项目,没有大公司成熟代码的习惯。还是因为 pelagia 本身是开源项目而没有把大公司病带入这个项目。或则大公司的代码是不是因为文档就更加强壮。这个命题真的很大,涉及到多个软件管理的领域。当然我也有兴趣和你聊聊开发管理的问题。小团队,大团队,公司角度的管理都不同。侧重点也各有不同。但无论什么时候软件按时,按质,按要求做出来。对公司,对团队,对团队领导才是最重要。无论小项目,大项目,团队项目。都要认真对待做出成绩。我能力也有不足的地方,对于细枝末节把空的并不好。也欢迎有兴趣,志同道合的开发者加入进来。
    danc
        38
    danc  
       2020-08-10 13:58:17 +08:00   ❤️ 1
    rust 了解一下?
    gantleman
        39
    gantleman  
    OP
       2020-08-10 14:37:51 +08:00
    @danc 你提出了一个有趣的问题,我没有理解错的话是想问 rust 和 go 在并行上和 pelagia 有什么区别。简单说 go 和 rust 会死锁,pelagia 不会。深入的说 go 和 rust 通过放弃对线程间共享数据的管理。来减少工程的复杂程度。也许开发者仅仅不想处理这么复杂的问题,或则没想好怎么处理这么复杂的问题。去掉共享数据的管理在技术水平上就相当于自降一级。假设有一天软件工程师在做开发的时候,不需要考虑语言错误问题,存储错误问题,网络错误问题。那不表示这些问题天生不存在。哪仅仅是因为所使用系统语言或工具将这些问题处理掉了。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3253 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 36ms · UTC 12:14 · PVG 20:14 · LAX 04:14 · JFK 07:14
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.