自旋锁(spinlock)很好理解。对自旋锁加锁的操作,你可以认为是类似这样的:
while (抢锁(lock) == 没抢到) {
}
只要没有锁上,就不断重试。显然,如果别的线程长期持有该锁,那么你这个线程就一直在 while
while
while
地检查是否能够加锁,浪费 CPU 做无用功。
仔细想想,其实没有必要一直去尝试加锁,因为只要锁的持有状态没有改变,加锁操作就肯定是失败的。所以,抢锁失败后只要锁的持有状态一直没有改变,那就让出 CPU 给别的线程先执行好了。这就是互斥器(mutex)也就是题目里的互斥锁(不过个人觉得既然英语里本来就不带 lock,就不要称作锁了吧)。对互斥器加锁的操作你可以认为是类似这样的:
while (抢锁(lock) == 没抢到) {
本线程先去睡了请在这把锁的状态发生改变时再唤醒(lock);
}
操作系统负责线程调度,为了实现「锁的状态发生改变时再唤醒」就需要把锁也交给操作系统管理。所以互斥器的加锁操作通常都需要涉及到上下文切换,操作花销也就会比自旋锁要大。
以上两者的作用是加锁互斥,保证能够排它地访问被锁保护的资源。
不过并不是所有场景下我们都希望能够独占某个资源,很快你可能就会不得不写出这样的代码:
// 这是「生产者消费者问题」中的消费者的部分逻辑
// 等待队列非空,再从队列中取走元素进行处理
加锁(lock); // lock 保护对 queue 的操作
while (queue.isEmpty()) { // 队列为空时等待
解锁(lock);
// 这里让出锁,让生产者有机会往 queue 里安放数据
加锁(lock);
}
data = queue.pop(); // 至此肯定非空,所以能对资源进行操作
解锁(lock);
消费(data); // 在临界区外做其它处理
你看那个 while
,这不就是自己又搞了一个自旋锁么?区别在于这次你不是在 while
一个抽象资源是否可用,而是在 while
某个被锁保护的具体的条件是否达成。
有了前面自旋锁、互斥器的经验就不难想到:「只要条件没有发生改变,while
里就没有必要再去加锁、判断、条件不成立、解锁,完全可以让出 CPU 给别的线程」。不过由于「条件是否达成」属于业务逻辑,操作系统没法管理,需要让能够作出这一改变的代码来手动「通知」,比如上面的例子里就需要在生产者往 queue
里 push_back
后「通知」!queue.isEmpty()
成立。
也就是说,我们希望把上面例子中的 while
循环变成这样:
while (queue.isEmpty()) {
解锁后等待通知唤醒再加锁(用来收发通知的东西, lock);
}
生产者只需在往 queue
中 push_back
数据后这样,就可以完成协作:
触发通知(用来收发通知的东西);
// 一般有两种方式:
// 通知所有在等待的(notifyAll / broadcast)
// 通知一个在等待的(notifyOne / signal)
这就是条件变量(condition variable),也就是问题里的条件锁。它解决的问题不是「互斥」,而是「等待」。
至于读写锁(readers-writer lock),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。读写锁不需要特殊支持就可以直接用之前提到的几个东西实现,比如可以直接用两个 spinlock 或者两个 mutex 实现:
void 以读者身份加锁(rwlock) {
加锁(rwlock.保护当前读者数量的锁);
rwlock.当前读者数量 += 1;
if (rwlock.当前读者数量 == 1) {
加锁(rwlock.保护写操作的锁);
}
解锁(rwlock.保护当前读者数量的锁);
}
void 以读者身份解锁(rwlock) {
加锁(rwlock.保护当前读者数量的锁);
rwlock.当前读者数量 -= 1;
if (rwlock.当前读者数量 == 0) {
解锁(rwlock.保护写操作的锁);
}
解锁(rwlock.保护当前读者数量的锁);
}
void 以写者身份加锁(rwlock) {
加锁(rwlock.保护写操作的锁);
}
void 以写者身份解锁(rwlock) {
解锁(rwlock.保护写操作的锁);
}
如果整个场景中只有一个读者、一个写者,那么其实可以等价于直接使用互斥器。不过由于读写锁内部是至少需要用一把锁来保护当前读者数的,所以,如果你的临界区很小,读写锁相比一般的锁并不能带来很大的优势,甚至可能性能更低。
另一方面,读写锁要真正发挥效能,条件也比较麻烦。比如实际的读写锁通常不用例子里两把锁的实现,而是用一把锁、一个条件变量来实现,好处是可以缓解写者饥饿的情况(一旦有写者在等锁,后续读者都需要等写者离开后才能继续),但这样一来,如果读者的临界区没有明显小于写者的临界区,阻塞情况可能会变得比较不理想……
所以你可以认为读写锁是针对某种特定情景的「优化」。不是说不要用读写锁,而是读写锁往往没有看上去那么理想。个人建议是可以优先用 mutex,如果遇到瓶颈后可以选择替换为读写锁,看看能否带来性能提升。
以上。
前几天看 API 文档时候遇到了 std::error_code
这个东西,当时以为是 errno
的 Alias,后续查阅文档才发现并没有那么简单。
system_error
std::error_code
包含在 <system_error>
头文件中,相关的共有四个组件:
std::system_error
std::error_code
std::error_condition
std::error_category
std::system_error
是一个继承自 std::runtime_error
的异常类,用以表示与 OS 交互时得到的以错误码形式返回的错误。因此除了提供标准的 what()
函数外,它还额外暴露了一个返回 std::error_code
的 code()
函数。同时,因为必须提供错误码,它既不提供空构造、也不提供接受字符串的构造函数。
从名字上就可以看出来(同时也是当时的提案 N2241 中指出的),整个 <system_error>
头文件都是为实现这个异常而生的。STL 有意将 std::error_code
作为 std::system_error
的附属品,而非单独的错误处理机制。
std::error_code
STL 中早期类似的错误码实现需求来自(C++17 才正式进入标准的)文件系统 API,当时的实现认为错误分类应在 errno
和操作系统原生错误码上二选一。但由于 STL 中网络、各种奇形怪状的 Boost 库的陆续加入,大家急需一个可扩展的错误码表示方案,才演变成现在的 std::error_code
。
std::error_code
顾名思义表示错误码,是由一个 int
型的 value
和一个 std::error_category *
型的 category
组成的值类型类。
之所以在 value
外还需要保存一个 category
,是因为即使是同样的错误码,在不同的库或者场景下表示的意义可能不同。同样是 42,在一个库的错误码中可能表示「文件不存在」,在另一个库中可能就表示「DNS 解析失败」。如果你熟悉 Cocoa 的那一套,那么这就类似 NSError
的 domain
属性。
此外,虽然没有明说 value
在等于 0 时表示无错误,但整个系统就是建立在这样的假设上的,例如:
operator bool
是根据 value
是否非 0 来的
clear()
函数会将 value
设为 0
构造 std::error_code
std::error_code
的构造函数共有三种重载:
()
构造一个类似 clear()
后的对象
(value, category)
用参数填充对应的字段
(enum_value)
调用 make_error_code(enum_value)
工厂函数
前两种没啥好说的,第三种却值得推敲。
template<class ErrorCodeEnum>
error_code(ErrorCodeEnum e) noexcept;
此处的 ErrorCodeEnum
只是名字上说是枚举,但其实只要是用户定义类型就行(比如 enum class
/ enum
/ class
),所以理论上可以从异常直接构造 std::error_code
。
通常,如果构造函数只接受一个参数,那么我们推荐将它标记为 explicit
以免发生不必要的隐式转换。但这个函数不然,它要的就是让原始的 ErrorCodeEnum
可以隐式转换为 std::error_code
,从而实现这两者的直接比较。
catch (std::system_error const& e) {
if (e.code() == std::errc::invalid_argument) {
// blah blah blah...
}
}
当然,为了避免从任意类型的值都能搞个 std::error_code
出来,此重载只有当 std::is_error_code_enum<ErrorCodeEnum>::value
是 true
时才会有效。所以你自定义错误码枚举时,需要在 std
命名空间中特化此模版。
#include <iostream>
#include <system_error>
enum class YourErrorCode {
kSuccess = 0, // 别忘了 0 应该表示无错误
kNetworkError,
kBadRequest,
kServerError,
};
// 特化模版,启用对应的重载
namespace std {
template<>
struct is_error_code_enum<YourErrorCode>: true_type {};
}
// 提供工厂函数
// 工厂函数不必要写在 std 中
std::error_code make_error_code(YourErrorCode code)
{
return {
static_cast<int>(code),
std::generic_category(), // 这里暂时用自带的 category
};
}
int main()
{
std::error_code e = YourErrorCode::kBadRequest;
std::cout << e << '\n'; // 自带一个输出流的重载
}
当然,如果后续 C++ 中终于引入 Concept 的话,这个重载应该就没有这么复杂了吧。
std::error_category
前面说到 std::error_code
里保存的其实是指向 std::error_category
的指针,而非对象。这是因为这个类就是应该被当作单例来用的,实际也只能这么用才对,因为它的 operator=()
是通过直接比较 this
指针来实现的。
此外,std::error_category
还是个纯虚类,你必须实现的纯虚函数有:
name()
返回这类错误的名字
message(int)
为给出的「错误码」返回对应的文字描述
STL 自带 std::error_category
的几个子类,很好地示范了「如何正确使用纯虚类」:不暴露具体的子类,而是暴露工厂函数,只通过接口(纯虚类)访问子类。
// 得到的都是 std::error_category const&
auto const& gec = std::generic_category();
auto const& sec = std::system_category();
std::error_condition
2007 年,<system_error>
正式进入标准前夕,委员会对 N2241 提案给出了一些意见,其中包括一条
Obscure the distinction between system-specific errors and the general and portable notion of an error condition.
于是,为了区分「系统相关的错误」和「平台无关的错误」,std::error_condition
诞生了。
因此你可以看到,std::error_condition
是一个与 std::error_code
除了语义几乎没有差别的东西。从库作者的角度,你可以理解为封装底层细节时用 std::error_code
,而对外暴露接口时推荐使用 std::error_condition
。
一起玩
std::error_condition
与 std::error_code
虽然是两个独立的类,但它们可以通过 std::error_category
连接在一起。
这两个类的对象互相比较时,STL 提供了对应的 operator=()
等函数的重载,通过调用双方的 category().equivalent(other)
来比较。只要任何一方的 category 认为对方与自己等价,两者就会被判为相等。
// 卖个萌 :-P
enum class MyErrorCondition {
kChenggong,
kWangluoCuowu,
kQingqiuCuowu,
kFuwuqiCuowu,
};
class MyErrorCategory: public std::error_category
{
public:
// 还记得 std::error_cateory 是个单例么?
static MyErrorCategory const& instance() {
static MyErrorCategory instance;
return instance;
}
char const *name() const noexcept override {
return "MyErrorCategory";
}
std::string message(int code) const override {
return "Message"; // 偷个懒
}
bool equivalent(std::error_code const& code, int condition) const noexcept override {
// 理论上你用不着在这里处理 code.category() == this->instance 的情况
// 因为是个单例,所以某些情况下不得不用这么绕的办法来拿
auto const& yourErrorCodeCategory = std::error_code(YourErrorCode{}).category();
if (code.category() == yourErrorCodeCategory) {
switch (static_cast<MyErrorCondition>(condition)) {
case MyErrorCondition::kChenggong:
return code == YourErrorCode::kSuccess;
case MyErrorCondition::kWangluoCuowu:
return code == YourErrorCode::kNetworkError;
case MyErrorCondition::kQingqiuCuowu:
return code == YourErrorCode::kBadRequest;
case MyErrorCondition::kFuwuqiCuowu:
return code == YourErrorCode::kServerError;
}
}
return false;
}
};
// error_condition 同样需要特化模版启动重载
namespace std {
template<>
struct is_error_condition_enum<MyErrorCondition>: true_type {};
}
// error_condition 同样可以通过工厂函数构造
std::error_condition make_error_condition(MyErrorCondition code)
{
return {static_cast<int>(code), MyErrorCategory::instance()};
}
int main()
{
std::error_code code = YourErrorCode::kNetworkError;
std::error_condition condition = MyErrorCondition::kWangluoCuowu;
std::cout << (code == condition) << '\n';
}
看上去有点绕,但由于通常 std::error_code
是底层接口定死的,而你所做的只能是用 std::error_condition
来适配的情况来说,还是很合理的。
还是 STL 轻松,直接抛 std::system_error
给你。std::error_code
甩脸,要抽象你自己去用 std::error_condition
搞,哈哈。
顺带一提
上面已经提到此处的 std::system_error
异常与 std::error_code
的关系。事实上,至少在提出引入 std::error_code
提案的作者看来,STL 乃至 C++ 中推荐的错误汇报方法还是异常。由于存在与操作系统等底层 API 的交互,才引入了「从 API 返回的错误码构建异常」以及「将异常转换为错误码传给 API」这样的需求。
当然,为了方便统一范式,便于不支持异常的系统使用文件系统 API。C++17 才正式进入标准的文件系统 API 也添加了「默认抛异常,但额外传一个 std::error_code
就不抛异常」的机制。
以上。
当年 Google Reader 拜拜后,我也陆续尝试了不少国内国外的替代品。当时选择了相对不错的 feedly,但因为不像 GR 一样存在感强烈,常常忘掉它的存在。最近整理阅读渠道,又把 feedly 拾起来,就感觉到了 feedly 的不足。
首先便是基本功能收费,无法理解为啥作为一个 RSS 阅读器,feedly 连在自己的所有 feed 中搜索都是需要高级会员才能解锁的功能。
其次是 feedly 近几个月来隐藏已读文章的功能都是失效的。无法看到清爽的文章列表界面,这对于我这种强迫症来说简直要命。
另外,feedly 取消了几乎是 RSS 阅读器必备的星标功能,转而加入了「稍后阅读」按钮。虽然功能都只是一个旗标,可以将已标记的文章单独列出,但「星标」的语义可以由用户自己赋予;而「稍后阅读」却已经框死了语义,将其另作他用总让人有一种负罪感。于是我感觉很不舒服。
当然最令人发指的是 Android 客户端自带的「发送到 Pocket」功能,居然是需要你提供 Pocket 的用户名和密码的。既然网页版都实现了基于 OAuth 的 Pocket 授权,为啥客户端版本的反而要用这么「不安全」的方法,无法理解……曲线救国又需要「菜单 / 分享 / Add to Pocket」 这么啰嗦的步骤,所以干脆再见吧。
于是最近又尝试了一圈市面上仅存(诶)的几款 RSS 阅读器,客户端能够支持隐藏已读文章、支持星标(废话)、抓取速度好、又有比较好的 Pocket 支持的,大概就是 Inoreader 了。功能上,Inoreader 几乎是 Google Reader 传承而来的,也有丰富的快捷键支持。缺点是界面相比 feedly 感觉要不那么「现代」一点 :)
目前只用了 Inoreader 一小段时间,暂时没有特别的惊喜和苦恼。有一个有趣的用法是它可以将 Twitter / Google+ 当作 RSS 订阅,如果有合适的目标账号,应该还是很有用的。
以上。
几年前初次接触 Xcode,就觉得它的默认的代码模板很不好:
- 文件开头都有个注释,包含创建人及创建时间。但实际上这些信息都应该是版本控制系统管理的范畴,不应该写在代码中。
- 中文系统下,根据区域设置的不同,开头的注释里,文件创建时间可能包含汉字「年」。一个汉字夹杂在周围的英文注释里非常不伦不类。
当时毕竟还是 Too Young,刚接触 OS X,照着奇怪的教程跑去 /Applications/Xcode.app
里乱改一气,虽然达到了目的,但总觉得有些怪味道,就不了了之了。
最近终于有机会重新折腾一下 Xcode,就又查了一下相关的内容。不得不说,Xcode 这方面文档真是太不友好了 =,=
自定义文件模板
/Applications/Xcode.app
里是应用自己的原生内容,不应该擅自修改。要修改文件模板,应该把对应的位于 /Applications/Xcode.app/Contents/Developer/Library/Xcode/Templates
的 .xctemplate
模板文件夹复制到 ~/Library/Developer/Xcode/Templates
改个名字后,再加以修改。
# 创建文件夹结构(默认没有 Templates 及往后的文件夹)
mkdir -p ~/Library/Developer/Xcode/Templates/File\ Templates/Source
# 复制需要修改的 .xctemplate 文件夹
# 这里是在 Source 分类下,将 Swift File 模板复制为 Empty Swift File 模板
cp -R /Applications/Xcode.app/Contents/Developer/Library/Xcode/Templates/File\ Templates/Source/Swift\ File.xctemplate/ ~/Library/Developer/Xcode/Templates/Source/Empty\ Swift\ File.xctemplate/
这样,修改 Empty Swift File.xctemplate/___FILEBASENAME___.swift
就可以把讨厌的注释去掉了。
这里没法修改原有模板,也没法覆盖。即便使用与 Xcode 自带模板相同的名称,也只会在新建文件的模板选择对话框里出现两个同名的模板,非常蛋疼。
细枝末节
由于相关文档不足,我又有点强迫症,于是尝试了一些东西,比如上面说的「使用与 Xcode 默认模板相同的名字没法覆盖默认模板」。
上面路径中的 ~/Library/Developer/Xcode/Templates
为用户自定义模板的根目录,这个目录下的 .xctemplate
文件夹会被 Xcode 加载。Xcode 模板中的「分类」与此处 .xctemplate
的父目录有关。
Templates/Foo.xctemplate
:Templates 分类
Templates/Source/Foo.xctemplate
:Source 分类
Templates/File Templates/Source/Foo.xctemplate
:Source 分类
虽然最后两种在效果上没什么区别,但还是推荐使用最后一种,与 Xcode.app
中的目录结构对应起来,会比较方便管理。
另外,使用向导填写模板参数的高级用法需要查找官方 Reference,不过粗略找了一下也没有找到官方说明,很是沮丧 :( 不过反正目前用不到 :)
新项目中第一次开始作死使用 Python 3,在测试中遇到一件奇怪的事情。
系统中有一个邮件发送模块,直接在命令行中手动跑 Worker 的话邮件可以成功发送,而一旦用 supervisor 运行则无法发送邮件。日志中显示在邮件发送库中抛出了如下异常:
UnicodeEncodeError: 'utf-8' codec can't encode character '\udce6' in position 0: surrogates not allowed
Surrogate
所以,日志里这个 surrogate 是个啥?
Surrogate 是 Unicode 中位于 BMP 外的一组不会有对应字符的码位,Python 3 中使用这些码位来「代表」无法编码的字节。
如果你明白「锟斤拷」是怎么来的,那么就一定能理解 Python 的 replace
编解码错误处理机制:无法解码的字节会被替换成 U+FFFD
,无法编码的码位会被替换成 ?
。这样做有一个明显的缺点就是不可逆。Python 3 中新增的 surrogateescape
则是一种可逆的错误处理机制,利用 Surrogate 码位保存无法解码的字节,编码时则将其还原为对应的原始字节。
例子中的报错正是因为 0xE6
无法使用 ascii
解码,而被解为了 U+DCE6
。
'\udce6'.encode('ascii', 'surrogateescape')
>>> b'\xe6'
b'\xe6'.decode('ascii', 'surrogateescape')
>>> '\udce6'
知道了原因,用这种方法解码异常信息中的完整字符串就找到了罪魁祸首:邮件发送者的配置。
Python 3 的环境变量与编码
邮件发送者是使用 显示名称 <邮箱地址>
的格式在环境变量中设置的,显示名称部分包含了汉字。
Python 3 中,使用 os.getenv
或者 os.environ
获得到的环境变量的值都是 str
类型。在类 Unix 系统中,解码过程是使用 sys.getfilesystemencoding()
和 'surrogateescape'
错误处理来实现的。而 sys.getfilesystemencoding()
的取值与 nl_langinfo(COODESET)
相关,即 LC_CTYPE
环境变量。
所以,导致这部分环境变量包含 Surrogate 的原因就是 LC_TYPE
指定的编码不正确。
当环境变量 LC_CTYPE
、LC_ALL
、LANG
均没有显式定义时,各个操作系统有各自的 LC_CTYPE
的缺省值(比如 C
)。这种情况下,通过 sys.getfilesystemencoding()
通常会得到 ascii
,环境变量中包含非 ASCII 字符就会得到上面的 Surrogate。
真正的问题
到了这里,问题根源和解决方法基本上已经水落石出了。
前面说手动跑 Worker 可以但是 supervisor 跑 Worker 就不行,这是因为被 supervisor 管理的子进程并不会单独开 Shell 而是继承 supervisord
的 Shell,所以基本的环境变量也有不同。
解决方法便是在 supervisor 配置中修改 environment,加入 LC_CTYPE
或者 LC_ALL
或者 LANG
环境变量的配置。
附,LANG
、LC_ALL
、LC_TYPE
什么的之间到底是个啥关系:查找 LC_X
的值的过程是:LC_ALL
有值就用 LC_ALL
的值;否则 LC_X
有值就用 LC_X
的值;否则 LANG
有值就用 LANG
的值;否则就用操作系统的默认值。
以上。
参考文档
- «
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- ...
- 18
- »