Skip to content

Instantly share code, notes, and snippets.

@ahappyforest
Last active December 27, 2015 13:18
Show Gist options
  • Save ahappyforest/7331725 to your computer and use it in GitHub Desktop.
Save ahappyforest/7331725 to your computer and use it in GitHub Desktop.
/***********************************************************************
* Copyright (C) 2013 by wsn studio ***
***********************************************************************/
/**
* @file example.h
* @author Liu Peng
* @date 2013-11-06
* @brief 这是一个小例子, 目的是教你如何快速使用doxygen
*
* 注意doxygen使用反斜杠@\或者@@符号来定义一个标识
* @see http://www.stack.nl/~dimitri/doxygen/docblocks.html
* @see http://www.stack.nl/~dimitri/doxygen/commands.html
* @see https://gist.github.com/van9ogh/7331725
*/
#ifndef _EXAMPLE_H
#define _EXAMPLE_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/**< 这是一个定义enum的例子*/
/**
* @brief enum是个好东西!
*
* enum里面有三个变量, 我们会拿这三个变量表示物体的三种状态
*/
typedef enum STATUS_enum {
STATUS_OPEN, /**< 表示开关已经打开 */
STATUS_CLOSE, /**< 表示已经关闭运行 */
STATUS_RUNNING /**< 表示正在运行 */
} STATUSENUM;
/**
* @brief 结构体是个好东西
*
* 这个结构体用来盛放一些系统运行的变量
*/
typedef struct Example_s {
int a; /**< 变量1 */
int b; /**< 变量2 */
double c; /**< 变量3 */
} Example_t;
/**
* @brief 两个数相加
*
* 两个数相加, 我们需要两个整数变量, 然后将它们相加, 例如:
* @code{.c}
* int c;
* c = add(param1, param2);
* printf("1 + 1 = %d\n", c);
* @endcode
* @param a 第一个整数
* @param b 第二个整数
* @return 返回两个数相加后的整数
*/
int add(int a, int b);
#endif
@ahappyforest
Copy link
Author

马力兄

你好!

能与你讨论一些问题, 感觉很不错~

  1. 虽然这是一个单链表结构, 但是它不是只是对头节点进行get/put操作, queue.c实现的时候其实也是有两个指针, 一个是last[priority]指针, 一个entry[priority]指针, 入队操作last[priority]指针, 出队操作entry[priority]指针。 队列我觉得在数据结构上应该使用head和end指针, 这样才能满足FIFO的特性。
  2. 这个队列只使用了一个信号量, 之前使用这个库的时候, review这段代码确实有这个疑问。 其实由于queue.c使用了buf pool, 才导致实现起来有点复杂。 我们先撇开问题, 理清一下思路:

queue.c的实现有一个buf pool, 每次put一个新的node都是从pool中取出一个未使用的node, 然后将其插入到单链表中, 于是put操作,当pool为空取不出node的时候, 会发生pthread_cond_wait。

每次get一个新的node都是从单链表中取出一个node, 然后返回node中的key值, 于是get操作, 当单链表为空的时候, 会发生pthread_cond_wait

pool为空和单链表为空这是具有正交性的互斥命题, 因此我认为读线程并不会唤醒读线程, 写线程不会唤醒写线程。我觉得生产消费者模型, 应该一个信号量就够了。

  1. 一直以来, 我对pthread用户层面的使用本来就有疑惑, 为什么在pthread_cond_wait之前需要pthread_wait_lock, 而且在多个线程的情况下, 表现出来的现象是程序会都获取lock, 都会运行到pthread_cond_wait挂起。
    另外, 在临界区之内, 是不能使用挂起的, 比如使用了sleep函数, 线程就死了。关于这个内在的原理, 想听听马力兄的解释了 ;-) 并不是很明白, 是不是可以通过信号也能唤醒。
  2. 哈哈,这不能算作一个接口, 我也忍不住想要吐槽。作者倒是没有把它暴露出去, 用的static, 可能只是想把这一部分功能封装一下。
  3. 算是review过, 原作者当时的key类型是int, 后被我改成void*, 另外我把数据结构的设计放到了.c文件中, .h中暴露的接口被我修改过了。实现基本上也看过了, 虽然感觉作者写的不是很clean, 但作为Alex的一个测试, 想尽快提交一个版本。
    关于fork一个不知名的开源项目代码来构建自己的项目, 既然马力兄问到了, 当时没有在开源版本里面说明, 是我的责任,我将在README中注明,我看到马力你port tash的做法是将作者加入到注释列表中, 我觉得是个好主意。 其实私下说, 我个人对自己实现的一些开源项目并不是很满意, 都是把它当作idlebox了, 我想应该删除绝大多数的project, 以后写的东西, 一旦开源必属精品。 最近在忙毕设的事情, queue.c打算重写。当时感觉queue是一个被很多人写过的东西, 而作者构建的是一个优先队列, 还加入了buf pool, 以及信号量机制, 测试下来没有什么问题就先使用了。

另外, 对于最后一个问题, 学习开源项目源代码我感觉更偏向于工程实践, 在阅读开源项目的过程中我们会接触到大量的编码技巧, 作者的构架方案, 非常多的细节, 这样对自己构建软件非常有帮助, 但是以这样一种方式必然存在偶然性, 也和本身自己阅读的开源代码质量有很大关系。而学习计算机科学, 更注重的是原理性的学习, 从另外一个层次发现事物的本质, 比如马力兄之前写道, 关于模块化的通信机制, 这些东西很可能在阅读开源代码的过程中帮助不是很大, 我们需要从计算机科学的角度系统的学习。

说到模块化通信机制, 最近我发现Actor模式是软件构架很好的模式, Alex在写luactor, 我有空会去看云风的skynet, 都是采用的这种构架。同时, 我自己也在临摹云风的skynet, 在写cactor, 你可能又要说到里面的队列了, 好吧, 这一次我明确注明了。https://github.com/van9ogh/cactor

大学几年, 我也阅读过一些开源项目, 确实, 阅读开源项目, 对代码风格, 数据结构和API的设计更加有章可循, 这种感觉就像临摹, 越是大师的作品, 经过越长时间的推敲, 越是值得去临摹。但是我自己也明显感觉到, 自己在计算机科学理论方面的很多不足, 也在积极的通过阅读相关书籍补充。 另外, 就像马力兄指出queue.c的各种问题一样, 在代码被开源的情况下, 各种问题都会被暴露出来, 逼着自己做的更好, 我也在积极向这个方向努力。

;-)

祝好
刘鹏同学

@begeekmyfriend
Copy link

pool为空和单链表为空这是具有正交性的互斥命题, 因此我认为读线程并不会唤醒读线程, 写线程不会唤醒写线程。我觉得生产消费者模型, 应该一个信号量就够了。

——是否沉睡/唤醒是pthread_cond_t的内部机制,所以只同信号量有关,只要多个读写线程拥有同一个信号量,那么它们在pthread内部被唤醒的几率是相同的,与外部线程是读是写,以及外部资源是否有效没有关系。至于信号量在互斥锁内这种做法本身有待商榷,我看到的某些版本是解锁后wait唤醒后再上锁。坏情况是,如果读进程只会唤醒另一个读进程,那么它发现没啥可读的从而又进入沉睡,而写线程很有可能长时间得不到唤醒。你可以看看教材中都是用两个信号量的(除非只有一个读线程和一个写线程)。

至于entry和last,设计得相当隐晦啊,连类型都不对称,双向链表的首尾指针,以及环形缓冲的读写下标,都要比这简洁优雅,可见原作者写得相当随意。差评。

@begeekmyfriend
Copy link

还要吐槽的是queue_get_length其实是不可重入的函数,因为q->length存在竞态,而return并非原子操作,很可能包含内存寻址,加载寄存器ax,pop以及iret等指令,期间只要另外一个线程改写了就欧了。保守的做法是赋值给局部变量,加锁,然后返回该局部变量,或者直接调用atomic_get之类的特殊系统服务函数。

@ahappyforest
Copy link
Author

我可能有点理解马力兄你的意思了。

你说的是生产消费者模型, 保证的是读写交替, 因此在读之前要wait, 读完后要signal, 写则是相反的过程。因此会出现读线程会唤醒读线程, 写线程会唤醒写线程的情况。

在queue.c的实现中, 这并不是一个生产消费者模型(至少在时序上不是), 因为写线程一直是可以写的, 没有读线程也可以写。 而且队列在不为空的情况下, 根本就不会wait, 只有当出现两种情况下才会使用pthread_cond_t。

1. queue.c的实现有一个buf pool, 每次put一个新的node都是从pool中取出一个未使用的node, 然后将其插入到单链表中, 于是put操作,当pool为空取不出node的时候, 会发生pthread_cond_wait。

2. 每次get一个新的node都是从单链表中取出一个node, 然后返回node中的key值, 于是get操作, 当单链表为空的时候, 会发生pthread_cond_wait

生产消费者模型的实现, 有使用信号量的(semaphore), 也有使用条件变量的(pthread_cond_t), 个人感觉信号量更合适, 而条件变量本身只是用来触发某种条件的, 它是0,1模型, 在多个读写线程的情况下, 只会把问题搞复杂。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment