偶然翻出了高中时的解题报告

解题报告:http://info.doraemonext.com/blog/oi/problem.pdf

贪心问题总结:http://info.doraemonext.com/blog/oi/greedy.pdf

pb_ds 库总结:http://info.doraemonext.com/blog/oi/pb_ds.pdf

还有曾经和 绝恋love枫 一起编写过的长达 333 页的 HZOI 题目整理集:http://pan.baidu.com/s/1o8L6B8m

那时候的昵称还是 CodeWaySky,时间真是好快啊 (>﹏<)

附带一张曾经的封面:

hzoiers

wechat-python-sdk 从 Python2 升级 Python3 中的坑

算是一篇流水账,记录一下今天的升级 wechat-python-sdk 从 Python2 升级到 Python3 的历程。

大致内容

Python 3 相对于 Python 2 更新了很多地方,这里简要列出今天用到的内容:

  • print 函数,Python 2 是语句, print 'hello',Python 3 中是函数,必须以正常函数调用的方式 print('hello')
  • Python 2 中的字符串分为 strunicode 类型,Python 3 中的字符串分为 strbytes 类型,这也是今天遇到的坑最多的地方,一会儿着重说一下
  • 一系列内置函数及方法都返回迭代器对象,而不是 list 或者 tuple,比如 filter/map/dict.items 等等
  • 类库发生了一系列的变化,今天遇到的是 StringIO.StringIO 变化为 io.StringIOfile 被删除

升级过程

upload_media 函数

这个函数的作用是上传多媒体文件,在 Python 2 的情况下需要传入一个 file object 或者一个 StringIO.StringIO object。

Python 3 的情况下略有不同,StringIO.StringIO 被移到了 io.StringIO 下,同时多出来一个 io.BytesIO,用于二进制内容,很显然,对于 Python 3 这里需要使用 io.BytesIO 而不是 io.StringIO

另外是 file 类型在 Python 3 中被取消,取而代之的也是 io 库中的对应 class,根据打开文件模式的不同会被生成不同的 class,这里我使用了以下语句用作识别与 Python 2 中对应的 file 类型:

if isinstance(media_file, io.IOBase) and hasattr(media_file, 'name'):

不管 file 如何被打开,都会继承自 io.IOBase class,然后判断有没有 name 属性(因为这个函数需要调用该方法),从而与 Python 2 中的 isinstance(media_file, file) 相对应。

io.BytesIO 没什么好说的,直接用就可以了。

加解密 EncodingAESKey 的问题

这个主要就是 Python 2 的 unicode/str 和 Python 3 的 str/unicode 来回折腾的事了。

  • Python 2 的 str 对应于 Python 3 的 bytes
  • Python 2 的 unicode 对应于 Python 3 的 str

上面只是主要关系的对应,实际上还是有很大不同的。Python 3 全面采用了 unicode 编码,但是加解密的时候还是需要使用 bytes 的,所以涉及到了中间的各种转化,幸好测试覆盖率不错,按照测试跑出来的错误慢慢调试就好了。

迭代器的返回问题

发现在测试中跑出来一个错误,是 generate_jsapi_signature 中的,相关代码如下:

    data = {
        'jsapi_ticket': jsapi_ticket,
        'noncestr': noncestr,
        'timestamp': timestamp,
        'url': url,
    }
    keys = data.keys()

因为 Python 3 中,使用 dict 方法 dict.keys(), dict.items() 以及 dict.values() 都不再像 Python 2 中返回元组而是迭代器了,所以需要多包装一层,改为:keys = list(data.keys()) 即可。

同样的问题也出现在 setup.py 中的安装依赖条件上,最后修改为如下语句即可:

install_requires=list(map(lambda x: x.replace('==', '>=') and x.rstrip('\n'), open("requirements.txt").readlines())),

最后

用了整整一个下午的时间把 wechat-python-sdk 从 0.6.1 升级到了 0.6.2,不过这只是跑通了所有测试而已,更多的 Bug 需要进一步的开发者反馈了 = =

log4go 和 logrus 的对比与分析

这两天的任务是调研 logrus 和 log4go 的区别,阅读它们的源码,然后进行进一步封装,简要写一下它们之间的对比分析。

  1. logrus 是同步写入日志,log4go 是异步写入。

    • logrus 中,通过 sync.Mutex 的方式每次对 Logger 进行加锁,获得锁后写入单条日志,然后释放锁。
    • log4go 中,每个 Logger 拥有一个默认值为 3200 长度的缓存 channel(可以缓存 3200 个 LogRecord 指针对象,每个 LogRecord 对应一条日志),用于异步写入日志时的缓冲。
  2. logrus 不支持对日志进行 rotate,log4go 支持 rotate。

    • logrus 在 README 中明确说明如果需要对日志进行 rotate 操作,应由系统中的 logrotate(8) 命令进行,logrus 本身并不支持该操作。不过可以将 log4go 中的自动 rotate 代码移植到 logrus 中。
  3. logrus 不支持配置文件,log4go 支持。

    有两种方式可以解决该问题。

    • 第一种是使用 logrus 作者提供的 logrus_mate 工具,不过该工具 Star 很少,且读取配置文件方式只支持 json 形式的文件格式。
    • 另一种是将 log4go 的 XML 解析代码移植到 logrus 中,工作量还好,可以不用变更已有代码。
  4. logrus 支持 Hook,log4go 不支持。

    • logrus 的 Hook 可以在写入日志到硬盘之前(也是利用 Formatter 格式化日志格式之前)依次调用所有已注册 Hook,可以很方便的异步发送日志到 其他服务器或者其他日志接口。
  5. logrus 支持 Field 形式的写入日志方式。

    • 这个是 logrus 作者认为很有特色的地方,按照他的意思,写入日志不应该用

      log.Error("Failed to send event %s to topic %s with key %d")

      这种形式,而是把特定的 Field 都提取出来,将写入日志代码变成这个样子:

      log.WithFields(log.Fields{"event": event, "topic": topic, "key": key}).Error("Failed to send event")

      这里的 log.WithFields 会返回一个 *Entry 对象,该对象可以重用,从而可以在写入日志时省略公共 Field 部分。

  6. 这一条简要概括一下 logrus 和 log4go 在写入日志时的流程。

    • log4go 的 Logger 是一个 map[string]*Filter 对象,也就是说,可以包含多个 Filter,然后每个 Filter 包含 LevelLogWriter 对象,每次写入会循环所有的 Filter,然后判断 Level,大于等于该 Filter Level 即通过 LogWriter 进行写入,LogWriter 接收 LogRecord 对象作为参数,log4go 默认支持的有 File, Sock, Term 三种方式作为 LogWriter,当然还可以自己继续写其他方式的 LogWriter

      log4go 的 Filter 方式很适合对一条日志的多次报警,比如对 Filter Level 大于等于 ERROR 的全部发邮件,然后大于等于 INFO 的全部写入磁盘等等。

    • logrus 没有 Filter 这种形式,采用的是通过一个 Entry 对象保存 Logger, Fields 等,然后 Logger 再去保存 Writer, Hooks, Formatter 对象。Entry 对象可以持续重用,每次写入会调用 Logger 中的 Writer 进行写入,执行顺序是 range Hooks -> Formatter -> Writer,这其中每个操作都会对 Logger 中的 mutex 进行加锁。所以通过同一个 Entry 进行日志写入的话不能实现并发(它们在执行时共享同一个 Logger)。

愿自己永远保持一颗向上的心

又到了每天的睡前时间,听着网易云音乐的轻音乐专辑,轻柔的音乐在耳边回荡,思绪也变得宁静起来。关掉了所有代码,打开Blog,准备写一点心情随笔。

3月18来到北京,21入职小米,到现在也有了两周多的时间。从现在开始,一只脚已经踏入了社会的大门,儿时的懵懂无知,校园内的青涩年华,似乎已渐行渐远。未来,一切都是未知,从此,不再有父母和老师的谆谆教导,一切,都要靠自己去感悟、体会、奋斗。

又想起了高二时的竞赛前夕,焦躁而又无助的自己,每天在被窝里自己默默的哭,可能只是因为白天一道本应该做出的题没有AC,也可能是因为学了一个下午的知识点却一点也不得要领。后来的结果不出意料,名落孙山。但因为这段经历,锤炼了自己的心态,在那之后,到现在,没有任何一次因为压力而失落,没有任何一次因为压力而气馁,也许这就是挫折教育的真谛吧。

小时候,碰疼了可以朝父母哭,可以扑进他们的怀中安然入睡,知道不管发生什么,父母就是那遮风挡雨的港湾。

慢慢的,发现自己也变成了那港湾,为爱的人,为已年老的他们,撑起那把他们曾经撑起的那把保护伞。

现在的自己,刚刚开始北漂的历程。晚上,抬头看着周边林立的高楼大厦、万家灯火,不知何处为家。想起了小米的那句宣传标语,永远相信美好的事情即将发生。未知,才有拼搏的动力,才有收获的惊喜,不是吗?

愿自己永远保持一颗向上的心。万物之中,希望最美,最美之物,永不凋零。

DSC00778

C 语言中的函数式宏定义及相关技巧

宏定义可以像函数调用一样在代码中使用,称为函数式宏定义。

#define MAX(a, b) ((a)>(b)?(a):(b))
k = MAX(i&0x0f, j&0x0f)

在预处理之后,C 编译器会将其展开成下面的语句:

k =  ((i&0x0f)>(j&0x0f)?(i&0x0f):(j&0x0f))

非常方便而且效率极高,没有真正的函数调用所需的开销,没有分配和释放栈帧、传参、传返回值等等。但是使用时有很多的注意事项:

  1. 函数式宏定义的参数是没有类型的,不做参数类型检查,所以传参需要小心
  2. 定义宏时要小心优先级问题。比如上面的定义换成 #define MAX(a, b) (a>b?a:b),省去了括号,那么展开之后就变成了 k = (i&0x0f>j&0x0f?i&0x0f:j&0x0f),优先级就错了。所以定义的时候要善用括号来保证优先级
  3. 不要在函数式宏定义中使用可能产生副作用的语句,比如 ++,--,例如 MAX(++a, ++b),展开之后就变成了 k = ((++a)>(++b)?(++a):(++b)),整个的含义就错了

复杂的函数式宏定义技巧

然后是一个有意思的问题,一些比较复杂的函数式宏定义一般会这么写:

#define device_init_wakeup(dev,val) \
        do { \
                device_can_wakeup(dev) = !!(val); \
                device_set_wakeup_enable(dev,val); \
        } while(0)

(上述代码取自 include/linux/pm.h

那么为什么会用 do { ... } while(0) 把代码括起来呢?

#define device_init_wakeup(dev,val) \
                device_can_wakeup(dev) = !!(val); \
                device_set_wakeup_enable(dev,val);

if (n > 0)
    device_init_wakeup(d, v);

上面的代码在宏展开后,第二条语句就不属于 if 条件了。

那么如果用 { ... } 把语句块括起来呢?

#define device_init_wakeup(dev,val) \
                { device_can_wakeup(dev) = !!(val); \
                device_set_wakeup_enable(dev,val); }

if (n > 0)
    device_init_wakeup(d, v);
else
    continue;

注意一下上面代码的 device_init_wakeup(d, v);;。如果不写这个分号,语法和语义都是正确的,但是看起来不像函数调用。如果写这个分号,if 语句就被这个分号结束掉了,else 没有办法和 if 进行配对。

所以使用 do { ... } while(0) 可以很好的解决这个问题。调用起来像函数,而且语法语义均正确。

[ 以上内容为 https://akaedu.github.io/book/ch21s02.html 的读书笔记 ]

结构体内存对齐问题

两个法则:

  • 大小为 size 的字段,他的结构内偏移 offset 需符合 offset mod size == 0
  • 整个结构的大小必须是其中最大字段大小的整数倍

示例:

struct Test {
    int a;     // 4
    long b;    // 8
    char c;    // 1
    short d;   // 2
};

整个 struct Test 的实例大小为 24。下面来逐步分析一下:

  1. a 本身占 4 个字节,无需调整;
  2. b 本身占 8 个字节,根据法则 1,它的偏移位置 offset 需要满足 offset mod 8 == 0,所以 offset 为 8,即 a 的后面需要填充额外 4 个字节的 padding;
  3. c 本身占 1 个字节,16 mod 1 == 0,无需调整;
  4. d 本身占 2 个字节,根据法则 1,offset 需要为 18 才满足 offset mod 2 == 0,所以 c 的后面需要额外填充 1 个字节的 padding;
  5. 根据法则 2,整个结构体中最大的字段大小为 8,所以必须为 8 的整数倍,上面所有的变量共占用 20 个字节,所以最接近的大小为 24 字节。

所以最后实例的大小为 24。

指针与 const 限定符详解

这里分析一下 C 语言中当 const 关键字遇到各种指针时的对应意义,以及我自己记忆它们的方法,希望能对你有所帮助。

因为当多级指针和 const 混合在一起会显得异常复杂,比如 const int *const **const *p; 之类的(虽然现实中应该没人用这么多级的指针)。但不管多复杂的组合,判断方法都很简单:

  • 如果起始是 const int 这种顺序,在大脑里面将它倒过来,变成 int const,方便后面分析;
  • 从右向左依次扫描每一个 const,对每一个 const 提取该 const 之后的所有字符,然后将提取后的字符串中所有的 const 清除;
    • 举个例子,比如 int const *const **p;,碰到右边第一个 const 之后,提取右面的所有字符,即 **p,清除后仍为 **p
    • 然后碰到第二个 const,提取右面的所有字符,即 *const **p,清除所有 const 后,为 ***p
  • 提取得到的指针描述(如 **p)即为不可改写;
  • 重复上述步骤,直到扫描完所有的 const
  • 剩下的所有层级的指针描述均可改写;

下面我们再来举几个例子。

一级指针

这是最简单的情况:

const int *a;
int const *a;

const 后面提取出的指针描述为 *a,所以 *a 不可改写。剩下的指针描述 a 则允许被改写。


int *const a;

const 后面提取出的指针描述为 a,所以 a 不可改写。剩下的指针描述 *a 则允许被改写。


int const *const a;

右边第一个 const 后面提取出的指针描述为 a,所以 a 不可改写。

右边第二个 const 后面提取出的指针描述为 *a,所以 *a 仍然不可被改写。

二级指针

二级指针略显复杂,让我们继续来分析。

const int **a;

const 后面提取出的指针描述为 **a,所以 **a 不可改写。剩下的指针描述 *aa 均可被改写。


int *const *a;

const 后面提取出的指针描述为 *a,所以 *a 不可改写。剩下的指针描述 **aa 均可被改写。


int **const a;

const 后面提取出的指针描述为 a,所以 a 不可改写。剩下的指针描述 **a*a 均可被改写。


int *const *const a;

右边第一个 const 后面提取出的指针描述为 a,所以 a 不可改写。

右边第二个 const 后面提取出的指针描述为 *a,所以 *a 不可改写。

剩下的指针描述 **a 可以被改写。


大概就是这样,如果上述结论有错误欢迎留言。

C 语言的存储空间布局

以下为从低地址到高地址的顺序描述。

  • 正文段。CPU执行的机器指令部分。通常正文段是可共享的,只需要一个副本。另外一般也是只读的,防止意外修改指令。正文段也会包含字符串常量。

  • 初始化数据段。通常称为 数据段。包含了程序中需要明确赋初值的变量。

    • 例如,任何函数之外的声明 int maxcount = 99; 将会使此变量以其初值存放在初始化数据段中。
  • 未初始化数据段。通常称为 bss段(block started by symbol)。在程序开始执行之前,内核将此段中的数据初始化为 0 或空指针。

    • 例如,函数外的声明 long sum[1000]; 将会使此变量存放在非初始化数据段中。
  • 堆。通常在堆中进行动态存储分配。

  • 栈。自动变量以及每次函数调用时所需保存的信息都存放在此段中。

  • 命令行参数区。存放命令行参数和环境变量的值。

注意堆由低地址向高地址生长,栈由高地址向低地址生长。

需要存放在磁盘程序文件中的段只有正文段和初始化数据段,未初始化数据段的内容并不存放在磁盘程序文件中。

Linux 系统编程 - 线程专题总结

线程标识

#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);

// 返回值:相等则返回非 0 数值,否则返回 0

因为线程 ID 是用 pthread_t 数据类型来表示的,实现的时候可以用一个结构来代表 pthread_t 数据类型,所以可移植的操作系统实现不能把它作为整数处理。

可移植的写法是使用 pthread_equal 对两个线程 ID 进行比较。


#include <pthread.h>
pthread_t pthread_self(void);

// 返回值:调用线程的线程 ID

线程可以通过 pthread_self 函数获得自身的线程 ID。

线程创建

#include <pthread.h>
int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr,
                   void *(*start_rtn)(void *), void *restrict arg);

// 返回值:成功返回 0,否则返回错误编号                   

pthread_create 在调用失败时通常会返回错误码,而不是设置 errno(每个线程都提供 errno 的副本,但只是为了与使用 errno 的现有函数兼容)。

在线程中,从函数中返回错误码更为清晰整洁,不需要依赖那些随着函数执行不断变化的全局状态,这样可以把错误的范围限制在引起出错的函数中。

线程终止

如果进程中的任意线程调用了 exit_Exit 或者 _exit,那么整个进程就会被终止。如果信号的默认动作是终止进程,那么发送到任意线程的信号也会终止整个进程。

单个线程可以通过 3 种方式退出,而不会终止整个进程:

  • 线程可以简单地从启动例程中返回,返回值是线程的退出码
  • 线程可以被同一进程中的其他线程取消
  • 线程调用 pthread_exit

#include <pthread.h>
void pthread_exit(void *rval_ptr);

rval_ptr 参数是一个无类型指针,与传给启动例程的单个参数类似。进程中的其他线程也可以通过调用 pthread_join 函数访问到这个指针。

注意 rval_ptr 可以传递任意复杂信息结构的地址,但是该地址必须在调用者完成调用以后仍然有效。例如,在调用线程的栈上分配了该结构,但其他线程在使用这个结构的时候内存内容可能已经改变了,这种情况下需要使用全局结构或者 malloc 函数分配结构。


#include <pthread.h>
int pthread_join(pthread_t thread, void **rval_ptr);

// 返回值:成功返回 0,否则返回错误编号

当线程调用 pthread_join 后,该调用线程将一直阻塞,直到指定的线程调用 pthread_exit、从启动例程中返回或者被取消。如果线程简单地从它的启动例程返回,rval_ptr 就包含返回码。如果线程被取消,由 rval_ptr 指定的内存单元就设置为 PTHREAD_CANCELED


#include <pthread.h>
int pthread_cancel(pthread_t tid);

// 返回值:成功返回 0,否则返回错误编号

线程可以通过调用 pthread_cancel 函数来请求取消同一进程中的其他线程

默认情况下,pthread_cancel 函数会使得由 tid 标识的线程的行为表现如同调用了参数为 PTHREAD_CANCELEDpthread_exit 函数,但目标线程可以选择忽略取消或者控制如何被取消。

所以pthread_cancel并不等待线程终止,它仅仅提出请求


线程清理处理程序

线程可以安排它退出时需要调用的函数,称为线程清理处理程序

一个线程可以建立多个清理处理程序。处理程序记录在中,也就是说,它们的执行顺序与它们注册时相反

#include <pthread.h>
void pthread_cleanup_push(void (*rtn)(void *), void *arg);
void pthread_cleanup_pop(int execute);

当线程执行以下动作时,清理函数 rtn 是由 pthread_cleanup_push 函数调度的,调用时只有一个参数 arg

  • 调用 pthread_exit
  • 响应取消请求时f
  • 用非零 execute 参数调用 pthread_cleanup_pop

如果 execute 参数设置为 0,清理函数将不被调用。不管发生上述哪种情况,pthread_cleanup_pop 都将删除上次 pthread_cleanup_push 调用建立的清理处理程序。

注意这些函数有一个限制,由于它们可以实现为宏,宏定义中可以包含字符 {},所以必须在线程相同的作用域中以配对的形式使用

线程同步 - 互斥量

可以使用 pthread 的互斥接口来保护数据,确保同一时间只有一个线程访问数据。

互斥变量是用 pthread_mutex_t 数据类型表示的。在使用互斥变量以前,必须首先对它进行初始化,初始化分两种:

  • 设置为常量 PTHREAD_MUTEX_INITIALIZER(适用于静态分配的互斥量)
  • 通过调用 pthread_mutex_init 函数进行初始化

如果动态分配了互斥量,则在释放内存前需要调用 pthread_mutex_destroy

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
                       const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);

// 所有函数的返回值:成功返回 0,否则返回错误编号。

如果使用默认的属性初始化互斥量,则只需要把 attr 设为 NULL 即可。


#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

// 所有函数的返回值:成功返回 0,否则返回错误编号。
  • 对互斥量进行加锁,需要调用 pthread_mutex_lock。如果互斥量已经上锁,那么调用线程将会阻塞直到互斥量被解锁
  • 对互斥量进行解锁,需要调用 pthread_mutex_unlock
  • 如果线程不希望被阻塞,那么使用 pthread_mutex_trylock 尝试对互斥量进行加锁
    • 如果调用时互斥量处于未锁住状态,那么 pthread_mutex_trylock 将锁住互斥量,然后返回 0
    • 如果调用时互斥量处于锁住状态,那么 pthread_mutex_trylock 将会失败,不能锁住互斥量,也是直接返回,返回值为 EBUSY

#include <pthread.h>
#include <time.h>
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
                            const struct timespec *restrict tsptr);

// 返回值:成功返回 0,否则返回错误编号。                            

当线程试图获取一个已加锁的互斥量时,pthread_mutex_timedlock 互斥量原语允许绑定线程阻塞时间。

pthread_mutex_timedlock 函数与 pthread_mutex_lock 是基本等价的,但是在达到超时时间值时,pthread_mutex_timedlock 不会对互斥量进行枷锁,而是返回错误码 ETIMEDOUT

线程同步 - 读写锁

读写锁与互斥量类似,不过读写锁允许更高的并行性。

互斥量只有两种状态:锁住状态和不加锁状态,且一次只有一个线程可以对其加锁。

读写锁可以有三种状态:读模式加锁状态,写模式加锁状态,不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁

读写锁非常适合对数据结构读的次数远大于写的情况:

  • 当在写模式下时,它所保护的数据结构可以被安全的修改,因为一次只有一个线程可以在写模式下拥有这个锁
  • 当在读模式下时,只要线程先获取了读模式下的读写锁,该锁所保护的数据结构就可以被多个获得读模式锁的线程读取

PS: 当读写锁处于读模式锁住的状态,而这时有一个线程试图以写模式获取锁时,读写锁通常会阻塞随后的读模式锁请求。这样可以避免读模式锁长期占用,而等待的写模式锁请求一直得不到满足。

读写锁是用 pthread_rwlock_t 数据类型表示的。在使用读写锁以前,必须首先对它进行初始化,初始化分两种:

  • 设置为常量 PTHREAD_RWLOCK_INITIALIZER(适用于静态分配的读写锁)
  • 通过调用 pthread_rwlock_init 函数进行初始化

    #include <pthread.h>
    int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
                            const pthread_rwlockattr_t *restrict attr);
    int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
    
    // 两个函数的返回值:成功返回 0,否则返回错误编号
    

如果使用默认的属性初始化读写锁,则只需要把 attr 设为 NULL 即可。


#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

// 所有函数的返回值:成功返回 0,否则返回错误编号
  • 在读模式下锁定读写锁,调用 pthread_rwlock_rdlock 函数
  • 在写模式下锁定读写锁,调用 pthread_rwlock_wrlock 函数
  • 不管是什么方式锁住的读写锁,均可以调用 pthread_rwlock_unlock 进行解锁

下面还有读写锁原语的条件版本:

#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

// 两个函数的返回值:成功返回 0,否则返回错误编号

可以获取锁时,这两个函数返回 0,否则它们返回错误号 EBUSY

[未完待续...]

C 语言 restrict 关键字作用详解

restrict 是 C99 中新增的关键字,仅用于限定指针。

使用该关键字修饰的指针会明确的告诉编译器:所有修改该指针所指向内容的操作全都是基于该指针的。换句话说,不存在除该指针外修改该指针指向内容的方式。

使用该关键字可以让编译器进行更好的代码优化,下面用具体示例来分析一下。

#include <stdio.h>
#include <stdlib.h>

int calc(int *ptr1, int *ptr2)
{
    *ptr1 = 10;
    *ptr2 = 20;
    return *ptr1;
}

int main(void)
{
    int *a = malloc(sizeof(int));
    int *b = malloc(sizeof(int));
    printf("%d\n", calc(a, b));
    return 0;
}

注意上面代码中的 calc 函数,如果不仔细看的话会想当然的认为该函数会一直返回 10,然后认为编译器优化代码的时候会直接将其优化为常量。但结果并不是这个样子,下面是我们使用

gcc -S test.c -O2

生成优化后的汇编代码,可以看到,calc 对应的汇编代码如下:

calc:
.LFB39:
    .cfi_startproc
    movl    $10, (%rdi)
    movl    $20, (%rsi)
    movl    (%rdi), %eax
    ret
    .cfi_endproc

显然编译器并没有“聪明的”将返回值变为常量。为什么呢?

这个问题的关键在于,编译器并不知道传入的两个指针形参 ptr1ptr2 是不是指向了相同的内存地址。如果 ptr1 == ptr2,那么会发生什么?没错,返回值就变成了 20。

在绝大多数情况下,我们调用函数并不会传入两个地址相同的指针去让它们重复操作,如果我们可以保证这一点,那么我们就可以使用 restrict 关键字修饰指针形参,告诉编译器,所有修改该指针所指向内容的操作全都是基于该指针的,不会有别的指针来添乱

接下来我们的 calc 函数的定义就变成了下面这个样子:

int calc(int *restrict ptr1, int *restrict ptr2)
{
    *ptr1 = 10;
    *ptr2 = 20;
    return *ptr1;
}

然后再使用

gcc -S test.c -std=c99 -O2

生成优化后的汇编代码(注意添加 -std=c99),可以看到,calc 新的对应的汇编代码如下:

calc:
.LFB23:
    .cfi_startproc
    movl    $10, (%rdi)
    movl    $20, (%rsi)
    movl    $10, %eax
    ret
    .cfi_endproc

现在编译器聪明的将返回语句优化为常量,进一步提升了效率。

PS: 如果函数形参中的指针被 restrict 修饰,然后你又作死的传入了相同地址的指针且在函数中进行修改,那么编译器可能会对代码做出错误的优化。比如上面的 calc 函数指针形参经过 restrict 修饰后,我还是调用 calc(a, a) 这种的话,将仍然会返回值 10,而不是正确答案 20。