记一次关于 C++ 多线程写文件操作的错误修复

Created: 2018-01-08
Categories: Dev

Tags: C++, Multithreading

前段时间酷 Q HTTP API 插件有用户报错说,连续请求两次 API 发送同一张图(通过 URL),会发生第二张图发不出去的问题。想了一下,问题很显然,因为插件在处理第一个请求的时候,开始下载图片,以 URL 的 md5 作为文件名,然后第二个请求到的时候(多线程处理),图还没有下完,但是同名文件已经存在了,插件以为是缓存,于是试图直接发送下到一半的文件,自然是发不出去了。

最简单的修复方法是让插件先把图片下载到一个临时文件,下载完成后再改名。但是这样的话,两个请求都会造成文件下载,但实际上这是不必要的,因为它们实际发的是同一张图。那么自然要想到让第二个请求发现已经有请求在发送同一张图的时候,等待其完成,然后直接发送已缓存的图。

一开始想到用一个 std::map<std::string, std::mutex> 来按文件名保存互斥锁,每个线程首先算出文件名,然后尝试获取文件名对应的锁。这个方法看起来虽然好像可以,但实际上仔细一想发现了问题,std::map 本身就不是线程安全的数据结构,两个线程很可能在创建和删除锁的时候发生冲突,而如果再给 map 加个锁,逻辑就开始有些混乱了,有发生死锁的风险。

于是去 Stack Overflow 搜了一圈,发现了 std::condition_variable 这么个东西,通过 waitnotify_onenotify_all 方法来实现让线程等待和唤醒等待中的线程,基本上满足了我的需求。

最终修复代码如下:

static unordered_set<string> files_in_process;
static mutex files_in_process_mutex;
static condition_variable cv;

if (!filename.empty() && make_file != nullptr) {
    unique_lock<mutex> lk(files_in_process_mutex);
    // wait until there is no other thread processing the same file
    cv.wait(lk, [=] {
        return files_in_process.find(filename) == files_in_process.cend();
    });
    files_in_process.insert(filename);
    lk.unlock();

    // we are now sure that only our current thread is processing the file
    if (make_file()) {
        // succeeded
        segment.data["file"] = filename;
    }

    // ok, we can let other threads play
    lk.lock();
    files_in_process.erase(filename);
    lk.unlock();
    cv.notify_all();
}

使用一个 std::unordered_set<std::string> 保存了当前正在处理的请求对应的文件名,同时对应有一个互斥锁。

第一部分首先获取互斥锁,然后调用 std::condition_variablewait 方法,这个方法会首先判断传入的谓词(第二个参数)是否满足(在拥有锁的情况下调用),如果不满足,释放锁,然后等待(阻塞当前线程)。在第一个 API 调用时,显然并没有其它线程正在处理同名文件,所以它会往下执行,把当前文件名放到集合,然后释放锁(这是很重要的,在实际下载文件开始之前需要释放锁,否则下载别的文件的线程也得等这个线程执行完才能执行)。

第二部分就是下载文件操作了,这里之所以不用拥有锁,是因为我们在第一部分已经确定没有其它线程正在操作这个文件,而一旦文件名插入到集合中(拥有锁的时候插入的),别的下载同名文件的线程就会在 wait 方法里阻塞了。因此第二部分我们可以确定每个文件只有一个线程在下载。

第三部分就是下载完成后从文件名集合中删除当前文件名了,在拥有锁的情况下删除,然后立即释放锁,接着调用 notify_all,还记得其它需要发送同名文件的线程都在 wait 中等待吗,调用 notify_all 之后它们会被唤醒,然后依次获取锁、判断谓词是否满足,当第二个发送同一图片的线程执行到第二部分的时候,它会发现图片已经缓存了,于是直接发送缓存了的图片。

至此完美解决了一开始的问题。

使用 PyInstaller 将 Python 程序打包成无依赖的可执行文件

Created: 2017-06-30
Categories: Dev

Tags: Python, PyInstaller

本文以 Windows 为例,其它系统上应该坑会更少一点。

安装

首先一点,截至目前(17 年 6 月),PyInstaller 还不兼容 Python 3.6,根据官方的说明,目前支持的版本是 2.7 和 3.3 到 3.5。当你看到这篇文章时,可能已经支持更新版本了,建议查看官方 repo 的 README:pyinstaller/pyinstaller

用 pip 安装:

pip install pyinstaller

在 Windows 上,pip 会同时安装 pypiwin32 包,这是 PyInstaller 在 Windows 上的一个依赖。

这里就有了第一个坑,根据 Requirements

It requires either the PyWin32 or pypiwin32 Python extension for Windows.

理论上应该默认情况下安装了 pypiwin32 就可以运行的,但其实并不行,直接用的话会报错 ImportError: DLL load failed: The specified module could not be found.

根据 #1840 这个 issue,再安装个 PyWin32 就可以了。

前往 Python for Windows Extensions,点进 pywin32 目录里面的最新 build,找到对应当前 Python 版本的 exe,下载安装。

使用

比如我们现在有一个脚本文件 main.py,要将它打包成可执行文件,直接运行:

pyinstaller -F main.py

这将会在 main.py 所在目录下生成一些其它目录,最终的可执行文件就在 dist 目录中。这条命令中 -F 表示生成单个可执行文件,如果不加 -F,则默认生成一个目录,其中除了可执行文件,还包括其它依赖文件。

总体来说用起来是非常简单的,不过在打包使用了 requests 包的程序时,出现了 ImportError: No module named 'queue' 的报错,发现 这里 也有遇到了同样的问题。

这个问题只需要在打包时加入 --hidden-import 参数即可:

pyinstaller -F --hidden-import queue main.py

更多用法请参考 Using PyInstaller

从 DLL 文件生成 LIB 文件

Created: 2017-05-08
Categories: Dev, Detailed

Tags: 动态链接库, Windows, 酷Q

标题可能有一定误导嫌疑,首先这里的 lib 文件不是指静态编译的 lib 文件,而是和 dll 配套使用的用来通过链接的 lib 文件;另外也不是从 dll 生成,而是直接生成,只是需要先通过 dll 来查看有哪些导出函数。

故事背景

在做酷 Q 插件开发的时候,发现官方的 C++ SDK 很久没有更新了,找了一圈发现其实它的函数实现都在安装目录的 CQP.dll,我要做的就是能让 C++ 代码去调用里面的函数,根据官方 SDK 的结构,光使用头文件来声明是没有用的,还需要一个 .lib 文件,研究了一圈发现其实 SDK 所谓的 .lib 文件并不是真正的函数实现,而只是用来通过编译的一个东西,它内部实际上是对 dll 中的函数的一些描述。于是就有了后面的折腾。

方法和坑……

需要说明的是,这里主要说的是针对酷 Q 的 dll 的情况,似乎编译 dll 的时候,函数的修饰符会影响很多结果,我也不太清楚酷 Q 具体是怎么修饰的导出函数,所以,这里说的方法可能不具有普适性。

我本来以为那个 .lib 文件是把 dll 转换成静态编译的,但似乎这其实是做不到的(或者我没找到办法),然后发现只是一个对 dll 里函数的描述,于是找到了生成 lib 文件的办法。

获取 DLL 文件的导出函数列表

首先需要知道代码里面会调用那些函数的函数名称和参数列表,这也就需要知道 dll 里面有哪些函数。打开 VS 附带的一个「Visual Studio Developer Command Prompt」(可以在开始菜单搜索到),然后进入 dll 文件所在目录,运行 dumpbin.exe /exports Some.dll > dump 即可把导出函数等一堆信息 dump 到 dump 文件中,类似下面这样:

Microsoft (R) COFF/PE Dumper Version 14.10.25019.0
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file .\CQP.dll

File Type: DLL

  Section contains the following exports for CQP.dll

    00000000 characteristics
    590B3029 time date stamp Thu May  4 21:44:09 2017
        0.00 version
           1 ordinal base
          36 number of functions
          36 number of names

    ordinal hint RVA      name

          1    0 0006F0D2 CQ_addLog
          2    1 0006F1D2 CQ_getAppDirectory
          3    2 0006F12C CQ_getCookies
          4    3 0006F160 CQ_getCsrfToken

......

从这里就可以知道 dll 的导出函数有哪些,不过只有函数名,我这里遇到的情况,还需要知道参数列表(准确地说是参数列表所占用的字节数)。

……

READ MORE

把博客迁移到 VeriPress 了

Created: 2017-03-29
Categories: Misc

Tags: Blog, VeriPress

好几天前就写好了 VeriPress 的初始版本(说初始版本是因为有一些功能在写之前就打算放在第二版来实现,不过目前看来,这个就当个 flag 好了……),一直没空把自己博客迁移过来,刚刚抽空把 VPS 全整理了一下,然后终于迁移了博客。

不得不说我之前真的蠢,竟然直接拿 Flask 的 run() 来跑,这次是用 uWSGI 和 nginx 跑的 Flask 的 app 对象,并开启了 HTTP/2 支持。

不过由于新的 VeriPress 和原来的 BlogA 在给评论框提供文章的唯一标识不一样了,原来的 Disqus 评论在新的上也就看不到了,虽然 Disqus 官方有一个迁移工具,但我想本来也没有几个评论,也就不折腾了吧,况且 Disqus 被墙了,感觉其实不是一个好的选择,就先这样吧。

再说一下我现在跑 VeriPress 的方法,首先在 VPS 用 virtualenv 来安装,VeriPress 实例放在主目录,然后通过 uWSGI 指定 virtualenv 的路径和工作目录,文章和页面在本地电脑上保存,写新文章之后用 sftp 同步到 VPS。这样整个流程基本上用起来还算舒服,缺点就是需要手动同步文章等数据,但现在实在懒得配置 GitHub 的 webhook 来自动更新了。

其实还是非常想后面有空给 VeriPress 加上数据库存储支持的,同时加上后台管理页面,这样也就不用费心同步文件了。

另外就是博客主题,由于不是很有空,就先用着之前写的默认主题了,后面有机会要重新写个好一点的主题。

嗯,来数数这篇文章里一共立了多少 flag。

情人节要到了,手把手教你写个 QQ 机器人给女朋友玩

Created: 2017-02-12
Categories: Dev, Detailed

Tags: 酷Q, QQ Bot, Mojo-Webqq, HTTP API

其实 QQ 机器人写起来很简单的,对于本教程中使用的 QQ 机器人框架,你需要一点点 C/C++ 基础,或者了解其它任何语言的 HTTP 库(开启 HTTP 服务端、发起 HTTP 请求两种)的使用。

QQ 机器人框架简介

也就是对 QQ 协议分析之后封装出来的一个完整的 QQ 客户端及开发框架,通过对这个客户端的事件的处理和接口的调用,即可实现诸如自动回复、主动通知等功能。

目前网上各种机器人框架很多,本教程主要以 酷 QMojo-Webqq 为例。

这两个框架差别还是蛮大的:酷 Q 是闭源的,使用 Android QQ 协议,似乎是易语言写的,只能在 Windows 上跑(最新版本也支持在 Wine 里面跑),且必须在图形界面操作,官方提供易语言和 C++ 的 SDK;Mojo-Webqq 是开源的,使用 SmartQQ 协议,用 Perl 写的,全平台基本都能跑,比较方便用脚本来自动化,官方提供 Perl 的接口以及 HTTP 接口。

另外,酷 Q 由于是 Android QQ 协议,要比 SmartQQ 稳定很多,且可以接收图片、语音等。

这两者的差别还有很多,可以分别去看它们的文档。

下面分别讲如何使用这两个客户端框架来编写简易的 QQ 机器人。

……

READ MORE