最近终于找到本有兴趣的书啦,就是这本《C++ 并发编程实战》。我会找一些自己觉得没想到、好玩、或者有自己想法的地方整理下。
顺带吐个槽:In Action 系列的封面人物真心诡异,还是 O'reilly 萌萌哒的奇怪可爱小动物看着顺心。
join()
的异常安全
std::thread
要求在析构函数被调用时,该对象必须没有与之相关联的线程,否系统分分钟自尽给你看(调用 std::terminate()
)。
一般情况下,我们主要需要注意的地方是析构前对 join()
或者 detach()
的调用。如果要分离线程,线程启动后就可以立即调用 detach()
,基本不会带来什么问题。而 join()
则没法立即调用,这样就会带来可能会被忽视的异常安全问题:线程启动后,如果某处抛出异常,可能把本应执行的 join()
跳过,导致程序自行退出。
void foo()
{
std::thread t(thread_func);
do_something(); // 如果抛出异常,就可能把下面的 join() 跳过
t.join();
}
把 do_something();
用 try
/ catch
包裹起来,并在 catch
里调 t.join()
最后把异常重新 throw
出去的方案固然可行,但对于 C++ 而言,更好的方案是 RAII:
class thread_guard
{
public:
explicit thread_guard(std::thread& t)
: thread_(t)
{
}
~thread_guard()
{
if (thread_.joinable()) { // join() 前对象必须满足 joinable
thread_.join();
}
}
thread_guard(thread_guard const&) = delete;
thread_guard& operator=(thread_guard const&) = delete;
private:
std::thread& thread_;
};
void foo()
{
std::thread t(thread_func);
thread_guard g(t);
do_something();
}
当然你也可以考虑把 std::thread
移动进去而不是传引用,书里管那种东西叫 scoped_thread
,调用时不必为 std::thread
对象单独开一个名字。
线程传参二三事
给线程函数传参是通过 std::thread
的构造函数完成的,其原理与 std::bind
类似。所有参数先在当前线程被复制 / 移动到对象内部,然后在新线程中完成对线程函数的调用。
这两步的划分会导致会被一些人说「坑」(虽然实际上是他们自己没注意)。
比如从「被复制」到「实际调用线程函数」之间可能并不连续,那么被复制的参数届时是否还有效就可能成为问题,例如:
void thread_func(std::string const& s);
void foo(int bar)
{
char buffer[1024];
sprintf(buffer, "%d", bar);
std::thread t(thread_func, buffer);
t.detach();
}
前面说过构造函数仅负责原样复制 / 移动参数,所以 char *
到 std::string
的转换是在新线程中的第二步时做的,于是例子中 t
对象构造函数暂存的其实是指向栈上的 buffer
的指针。而由于 detach()
,foo
返回后可能新线程还未正式启动。如果 thread_func
真正被调用时 foo
已经返回,那么这个指针参数就无效了。
一个解法是把代码改成 std::thread t(thread_func, std::string(buffer));
这样构造函数就会把实参 std::string
移动进去(即便在 std::string
无法移动只能复制的平行宇宙,这样做也是安全的)。
再过十几天,就是世界反法西斯战争胜利纪念日了。今天,ShadowSocks 在 GitHub 的相关仓库被勒令删除代码,停止官方维护,V2EX 和知乎上相关的内容也都被删了,可见这是一个同样值得纪念的日子。
说来,如果不是它,我估计现在还没开始用 Python 呢。要知道学习一门语言,学以致用的需求是很重要的。光头脑发热「我要学 Python」,看个语法,写点 Hello World 之类的,过几天估计就忘了。所幸当时 ShadowSocks 横空出世,给我的感觉是「居然寥寥数行(至少发布之初是这样)就把事情办妥了」「只要查查文档,理解起来应该很快吧」。于是为了理解代码,各种对照着翻文档,看语法,当时还写了一篇博客记录了下笔记。后来的一段时间,写 Python 不知道该用什么风格,也拿 ShadowSocks 的代码来模仿。
话说回来,现在这么多人在用 ShadowSocks,被封其实也是情理之中的事情。如果还能找到 V2EX 上初版 ShadowSocks 的帖子的话,应该可以看到作者当时的想法大致可以总结为:
- 公布一个自用的梯子,供大家参考
- 大家用不同的协议,更不容易被封
我觉得后者应该才是精华,可惜实际却很少有人在意。最初只提供傻傻的 Table 加密的一个原因,或许是「它只是一个用来抛砖引玉的 Demo」。或许理想的做法是每个人 Fork 并创造自己的加密手段甚至协议,在小范围内使用。然而更多的人(包括我)选择了直接使用,继而推动 ShadowSocks 本身的复杂化,变成了一个可以让人考虑让它失败的单点。
有点语无伦次。Anyway,谢谢 @clowwindy,辛苦了。
前几周周末百无聊赖,决定利用周末开始一个开发 Android 微博客户端的漫长路程。主要是为了熟悉下 Android Studio 和「更 Android 的网络通信方式」。这周末由于公司项目的事情,没什么空搞这货,只给时间线加了一个下拉刷新的功能。
SwipeRefreshLayout
光是下拉刷新的话,其实用不着第三方库,安卓自带就有一个工具类 SwipeRefreshLayout
,全名唤作 android.support.v4.widget.SwipeRefreshLayout
,可以用作一个可滚动的视图(比如 ScrollView
、ListView
)的容器,从而为这个视图提供下拉刷新功能。而且理论上根据系统的不同会有不同的表现形式,比如早期的就是 ActionBar 下边缘的横线动画,Material Design 后则是下拉的小圆形动画。
首先在 Gradle 中需要加入对 Support Library v4 的引用:
compile 'com.android.support:support-v4:21.0.0'
当然如果你的工程中已经间接引用了的话也可以不写,比如 v7 就依赖于 v4。
顾名思义,SwipeRefreshLayout
是一种 Layout,可以直接在 XML 文件里用,比如:
<android.support.v4.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/swipe_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/listview_timeline"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</android.support.v4.widget.SwipeRefreshLayout>
此时,如果把 listview_timeline
拉过头,就会触发 SwipeRefreshLayout
的 onRefresh
事件,并在界面上显示加载动画。
通过为 SwipeRefreshLayout
设置 setOnRefreshListener
,即可监听刷新事件,重新加载数据。加载完成后,调用 setRefreshing(false)
即可终止刷新动画。
当然写完你就会发现这个刷新图标默认是纯黑色箭头,和我们在 Google+ 以及最近更新的 Chrome 里看到的画风不一样啊。实际上用 setColorSchemeResources
方法就可以为这个动画设置一系列颜色了。
Otto 的跨线程调用
因为我的客户端是在 Service
中请求时间线的,即在 Activity 中触发下拉刷新后,是直接开个新的 Service
去做网络请求。如果要跨组件传递「刷新完成」这样的消息,作为一个新手的我感觉会非常复杂。于是就想起来用 Otto。
Otto 是 Square 公司开源的一个事件总线库(Event Bus)。顺带说一句 Square 公司(不是 Enix 的那个 Square ;))贡献了很多优质精巧的 Android 开源库,比如 Picasso 和 Retrofit。
用起来大致就是先全局定一个 Bus
对象,比如写一个单例 BusProvider
;再为每个事件定义一个类,比如 TimelineUpdatedEvent
(空类即可);最后就可以在事件生成处——比如 Service
里——写:
BusProvider.getInstance().post(new TimelineUpdatedEvent());
而在事件接收处——比如 Activity
里——写:
@Override
public void onPause() {
super.onPause();
BusProvider.getInstance().unregister(this);
}
@Override
public void onResume() {
super.onResume();
BusProvider.getInstance().register(this);
}
@Subscribe
public void onTimelineUpdated(TimelineUpdatedEvent event) {
// 时间线已更新,停止刷新动画
}
然而光这样做却有一个问题:Otto 为了保持简洁,要求所有 post
发生在主线程中,而在 Service
中调用显然不在主线程,从而会导致抛出异常。所幸我们可以用 Android 的 Handler
和 Looper
在 Otto 默认的 Bus
周围再封装一层:
public class MainThreadBus extends Bus {
private final Handler handler = new Handler(Looper.getMainLooper());
@Override
public void post(final Object event) {
if (Looper.myLooper() == Looper.getMainLooper()) {
super.post(event);
} else {
handler.post(new Runnable() {
@Override
public void run() {
MainThreadBus.super.post(event);
}
});
}
}
}
思路很简单,post
前先检查自己是否在主线程,如果不是,让关联到主线程的 Handler
去调用原来的 post
。
今天看到 C 语言委员会的提案 N1875,顿时有种「卧槽」的感觉,因为它的标题是:Adding classes to C。这要是通过了,那可真就变成名副其实的 C with Classes 了呀~
纵观提案全文,主要从 C++ 中吸收「类」的概念和用法,但是没有虚函数之类的东西。如果算上单独的「访问限制符」、「单一继承」提案,一个 C 语言的类,很可能类似于:
class Car: public Vehicle
{
public: // 这是单独的另一个提案引入的
// 构造函数
initCar() {
initVehicle(); // 需要显式调用父类构造函数
countWheels_ = 4;
}
// 构造函数
initCar(int speedMax, int countWheels) {
initVehicle(speedMax); // 需要显式调用父类构造函数
countWheels_ = countWheels;
}
// 析构函数
deleteCar() {
deleteVehicle(); // 需要显式调用父类析构函数
}
int getWheelsCount() const { // 也有 const 哟
return this->countWheels_; // 也有 this 指针
}
private:
int countWheels_;
};
使用时,构造写法有点奇怪,也没说构造失败会怎样,析构也需要手动显式调用:
Car car; // 相当于 initCar()
Car tank.initCar(80, 16);
tank.deleteCar(); // 析构函数需要显式调用
car.deleteCar();
从目前的样子看,这样的「类」更类似于语法糖。当然,这只是个提案而已,会不会最终被批准,还得拭目以待。
以上。
SSH 用起来有一个不方便的地方,就是断线就要重连,不用个 screen / tmux 啥的浑身不舒服。这种现象在台式机上还好,因为网络稳定,但到了笔记本 / 手机之类的设备上,从休眠中恢复、网络断开啥的并不鲜见。mosh 的全称是 mobile shell,顾名思义,正是专为解决这些「移动」客户端的烦恼而设计的 (b ̄(I) ̄)b
安装非常简单,各大平台都有二进制包可以直接使用,比如:
pacman -S mosh # Arch Linux
brew install mosh # Mac OS X
官网上列有一大堆其它平台的安装方法和源码编译指南:https://mosh.mit.edu/#getting
安装好的 mosh 分三部分:
mosh
mosh-server
mosh-client
其中,我们直接使用的是 mosh
。用法和 ssh
一样,比如连接服务器:
mosh user@host
mosh 会先通过 SSH 连接到服务器,然后启动服务器上的 mosh-server
,启动成功后 SSH 被断开,一切就交给本地的 mosh-client
来和远程的 mosh-server
用 UDP 在 60000-61000 端口通信了。
如果要透过 mosh
给 ssh
传其它参数,那么就需要麻烦一些了,比如:
# 指定 SSH 端口
# mosh 自己的 -p 参数用来是指定 mosh-server 和 mosh-client 通信的 UDP 端口的
mosh --ssh="ssh -p 1024" user@host
使用过程中,如果网络延迟比较大,你会发现 mosh 不像 SSH 一样需要「盲打」,而是直接打出带下划线的字符。这就是 mosh 提供的「预测」模块的作用:在延迟比较大的时候,根据本地输入提前输出预期的结果,等收到远程服务器的实际结果后替换之。
网络意外断开时,窗口顶部会提示断开的时间和可用的转义序列,等网络恢复、顶部的提示消失,一切就会跟啥事都没发生过一样……
- «
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- ...
- 18
- »