Bean Li 2021-05-10T04:52:59+00:00 beanli.coder@gmail.com 上图不说话:关闭服务器节能模式 2021-03-07T10:29:00+00:00 Bean Li http://bean-li.github.io/powerleader-efficiency-mode

powerleader_2

powerleader_3

powerleader_4

powerleader_5

powerleader_6

powerleader_7

]]>
使用SystemTap给I/O设备注入延迟 2021-03-07T10:29:00+00:00 Bean Li http://bean-li.github.io/SystemTap-Inject-Latency 前言

当我们的IO密集型的应用怀疑设备的IO抖动,比如说某个磁盘的性能衰退,导致性能问题。这种故障一般需要有故障的硬盘才能测试。但是如何使用正常的硬盘模拟出块设备有较大的延迟呢?

阿里的褚霸有一篇文章《巧用Systemtap注入延迟模拟IO设备抖动》,使用SystemTap来模拟故障的设备。

global inject, ka_cnt

probe procfs("cnt").read {
  $value = sprintf("%d\n", ka_cnt);
}
probe procfs("inject").write {
  inject= $value;
  printf("inject count %d, ka %s", ka_cnt, inject);
}

probe vfs.read.return,
      vfs.write.return {
  if ($return &&
      devname == @1 &&
      inject == "on\n")
  {
    ka_cnt++;
    udelay($2);
  }
}

probe begin{
  println("ik module begin:)");
}

$ stap -V
Systemtap translator/driver (version 2.1/0.152, commit release-2.0-385-gab733d5)
Copyright (C) 2005-2013 Red Hat, Inc. and others
This is free software; see the source for copying conditions.
enabled features: LIBSQLITE3 NSS BOOST_SHARED_PTR TR1_UNORDERED_MAP NLS

$ sudo stap -p4 -DMAXSKIPPED=9999 -m ik -g inject_ka.stp sda6 300
ik.ko

但是霸爷的中心在vfs层注入延迟,如果我们关心块设备层,希望能够模拟出来块设备层的较高延迟,那么怎么做呢?

不啰嗦,直接进入主题。

在块设备层注入延迟

global cnt = 0 ;
probe module("sd_mod").function("sd_init_command") !,
      kernel.function("sd_init_command")
{
    device = kernel_string(@choose_defined($cmd, $SCpnt)->request->rq_disk->disk_name)
    if(device == @1)
    {
        mdelay($2);
        if(cnt % 100 == 0)
        { 
             printf("%s inject delay %4d times %7d\n", device,$2, cnt)
        }
        cnt++ ;
    }
    #printf("device %s sd_init_command\n", device);
}

probe begin{
  println("inject_scsi_delay module begin");
}

上述文件命名为inject_ka.stp,那么通过如下指令,可以给sdb设备注入10ms的延迟:

stap -p4 -DMAXSKIPPED=99999999 -m ik -g inject_ka.stp sdb 10
staprun ik.ko

或者执行:

stap -g -DMAXSKIPPED=99999999 inject_ka.stp sdb 10

我们可以通过iostat查看效果:

image-20210305224928219

参考文献

https://sourceware.org/systemtap/examples/io/iostat-scsi.stp

https://sourceware.org/systemtap/examples/io/iostat-scsi.txt

]]>
从osdmap中获取crush相关的信息 2021-02-27T10:29:00+00:00 Bean Li http://bean-li.github.io/osdmap-crush 前言

开发功能过程中,有遇到crushmap被改坏的情况:

2021-02-26 16:58:13.362710 mon.lmuoc mon.0 10.10.10.119:6789/0 2658547 : cluster [DBG] osdmap e1033: 10 total, 10 up, 10 in
2021-02-26 16:58:14.418809 mon.lmuoc mon.0 10.10.10.119:6789/0 2658853 : cluster [DBG] osdmap e1034: 10 total, 10 up, 10 in
2021-02-26 16:58:06.359967 mgr.lmuoc client.4097 10.10.10.119:0/854553116 16170 : cluster [DBG] pgmap v6705: 7168 pgs: 7168 active+clean; 87.2GiB data, 180GiB used, 36.0TiB / 36.2TiB avail; 12.3KiB/s rd, 505B/s wr, 15op/s; 10.5MiB/s, 2objects/s recovering
2021-02-26 16:58:08.388416 mgr.lmuoc client.4097 10.10.10.119:0/854553116 16174 : cluster [DBG] pgmap v6706: 7168 pgs: 7168 active+clean; 87.2GiB data, 180GiB used, 36.0TiB / 36.2TiB avail; 16.7KiB/s rd, 589B/s wr, 21op/s; 4.60MiB/s, 1objects/s recovering
2021-02-26 16:58:10.411606 mgr.lmuoc client.4097 10.10.10.119:0/854553116 16175 : cluster [DBG] pgmap v6707: 7168 pgs: 7168 active+clean; 87.2GiB data, 180GiB used, 36.0TiB / 36.2TiB avail; 15.6KiB/s rd, 589B/s wr, 18op/s; 4.60MiB/s, 1objects/s recovering
2021-02-26 16:58:12.440488 mgr.lmuoc client.4097 10.10.10.119:0/854553116 16179 : cluster [DBG] pgmap v6708: 7168 pgs: 7168 active+clean; 87.2GiB data, 180GiB used, 36.0TiB / 36.2TiB avail; 9.45KiB/s rd, 336B/s wr, 12op/s; 4.60MiB/s, 1objects/s recovering
2021-02-26 16:58:14.454427 mgr.lmuoc client.4097 10.10.10.119:0/854553116 16181 : cluster [DBG] pgmap v6711: 7168 pgs: 17 peering, 7151 active+clean; 87.2GiB data, 180GiB used, 36.0TiB / 36.2TiB avail; 19.7KiB/s rd, 505B/s wr, 24op/s
2021-02-26 16:58:15.399887 mon.lmuoc mon.0 10.10.10.119:6789/0 2659056 : cluster [WRN] Health check failed: Reduced data availability: 2 pgs inactive, 17 pgs peering (PG_AVAILABILITY)
2021-02-26 16:58:21.342312 mon.lmuoc mon.0 10.10.10.119:6789/0 2660571 : cluster [INF] Health check cleared: PG_AVAILABILITY (was: Reduced data availability: 3 pgs inactive, 18 pgs peering)
2021-02-26 16:58:21.342357 mon.lmuoc mon.0 10.10.10.119:6789/0 2660572 : cluster [INF] Cluster is now healthy
2021-02-26 16:58:23.065699 mon.lmuoc mon.0 10.10.10.119:6789/0 2660963 : cluster [DBG] osdmap e1035: 10 total, 10 up, 10 in
2021-02-26 16:58:24.139954 mon.lmuoc mon.0 10.10.10.119:6789/0 2661276 : cluster [DBG] osdmap e1036: 10 total, 10 up, 10 in

在修改crush过程中,只有4个版本的osdmap变化,如何从osdmap中提取出crush,以及判断crush是从那个版本开始被改坏的呢?

获取指定版本的osdmap

ceph osd getmap 1034 -o osdmap.1034

从osdmap获得crush

osdmaptool osdmap.1034 --export-crush crush.1034
crushtool -d crush.1034 -o crush.1034.txt

获得crush之后,可以比较两个版本之间,crush的变化:

image-20210228101105633

我们发现,我们关心的metadata的权重,在1035这个osdmap epoch被篡改了。

从osdmap获得某个Pool的PG分布

osdmaptool osdmap.1034  --test-map-pgs-dump --pool 10

其输出大约为:

。。。
10.3f6	[6,3]	6
10.3f7	[0,8]	0
10.3f8	[5,4]	5
10.3f9	[6,0]	6
10.3fa	[9,0]	9
10.3fb	[6,4]	6
10.3fc	[6,0]	6
10.3fd	[6,2]	6
10.3fe	[5,0]	5
10.3ff	[5,3]	5
#osd	count	first	primary	c wt	wt
osd.0	212	102	102	3.60291	1
osd.1	181	83	83	3.60291	1
osd.2	203	97	97	3.60291	1
osd.3	233	112	112	3.60291	1
osd.4	195	103	103	3.60291	1
osd.5	202	97	97	3.63974	1
osd.6	224	124	124	3.63974	1
osd.7	201	99	99	3.63974	1
osd.8	212	99	99	3.63974	1
osd.9	185	108	108	3.63974	1
 in 10
 avg 204 stddev 15.3428 (0.0752096x) (expected 13.5765 0.0665512x))
 min osd.1 181
 max osd.3 233
size 0	0
size 1	0
size 2	1024
size 3	0

我们可以看到PG在OSD上的分布是否均匀,以及各个PG各自分布在那个OSD上。

]]>
kdump 安装 2021-02-21T10:29:00+00:00 Bean Li http://bean-li.github.io/kdump 前言

有时候需要分配内核crash,我们需要kdump工具,本文介绍Ubuntu 下的相关工具的安装。

kdump功能主要分为3部分:

1)kdump使能阶段:安装kdump,然后重启系统生效,系统会预留一段内存作为第二内核和日志存储用;kdump启动会使用kexec命令加载第二内核完毕,等待触发panic启动;

2)系统panic,进入第二内核,内核将出错信息保存在/proc/vmcore提供给用户;

3)日志转储完毕,重启系统,/var/crash分析日志;

安装

Ubuntu下,需要安装linux-crashdump:

apt-get install linux-crashdump

安装完成之后,重启之前:

kdump-before-reboot

重启之后:


root@CVM01:/etc/sysctl.d# kdump-config show
DUMP_MODE:        kdump
USE_KDUMP:        1
KDUMP_SYSCTL:     kernel.panic_on_oops=1
KDUMP_COREDIR:    /var/crash
crashkernel addr: 0x2a000000
   /var/lib/kdump/vmlinuz: symbolic link to /boot/vmlinuz-4.14.148-server
kdump initrd:
   /var/lib/kdump/initrd.img: symbolic link to /var/lib/kdump/initrd.img-4.14.148-server
current state:    ready to kdump

kexec command:
  /sbin/kexec -p --command-line="BOOT_IMAGE=/boot/vmlinuz-4.14.148-server root=UUID=ba73a2b3-f5a0-4035-8689-3494a5aeab16 ro video=VGA-1:800x600 quiet i915.modeset=0 nomodeset net.ifnames=1 biosdevname=0  systemd.unit=kdump-tools-dump.service irqpoll nousb ata_piix.prefer_ms_hyperv=0" --initrd=/var/lib/kdump/initrd.img /var/lib/kdump/vmlinuz

通过service –status-all可以看到新加入的kdump相关的服务:

root@CVM01:/etc/sysctl.d# service --status-all
 [ + ]  kdump-tools
 [ + ]  kexec
 [ + ]  kexec-load

如果配置要修改,可以通过如下两种方式来进行:

  1. dpkg-reconfigure kexec-tools 和dpkg-reconfigure kdump-tools
  2. 修改/etc/default/kexec配置文件

参考文献:

Kernel Crash Dump

如何在Ubuntu18.04下安装和配置kdump

]]>
systemd修改服务的资源使用 2021-01-30T10:29:00+00:00 Bean Li http://bean-li.github.io/systemd-limit 前言

有些时候,我们需要修改某些进程或者某个服务的资源,比如:

  • 允许进程打开的最大文件数
  • 修改线程的默认栈大小

Linux提供了ulimit 指令来查看和修改默认资源限制:

root@subhealth1:/lib/systemd# ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 379704
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 4096
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 379704
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

对于systmed来讲,如果修改配置文件,限定或者修改资源的使用呢?

systemd和ulimit的映射

systemd基本上可以修改所有资源的限制,它和ulimit的映射关系如下:

Directive        ulimit equivalent     Unit
LimitCPU=        ulimit -t             Seconds      
LimitFSIZE=      ulimit -f             Bytes
LimitDATA=       ulimit -d             Bytes
LimitSTACK=      ulimit -s             Bytes
LimitCORE=       ulimit -c             Bytes
LimitRSS=        ulimit -m             Bytes
LimitNOFILE=     ulimit -n             Number of File Descriptors 
LimitAS=         ulimit -v             Bytes
LimitNPROC=      ulimit -u             Number of Processes 
LimitMEMLOCK=    ulimit -l             Bytes
LimitLOCKS=      ulimit -x             Number of Locks 
LimitSIGPENDING= ulimit -i             Number of Queued Signals 
LimitMSGQUEUE=   ulimit -q             Bytes
LimitNICE=       ulimit -e             Nice Level 
LimitRTPRIO=     ulimit -r             Realtime Priority  
LimitRTTIME=     No equivalent

我们以OSD为例:

[Unit]
Description=Ceph object storage daemon osd.%i
After=network-online.target local-fs.target time-sync.target ceph-mon.target
Wants=network-online.target local-fs.target time-sync.target
PartOf=ceph-osd.target

[Service]
LimitNOFILE=1048576
LimitNPROC=1048576
EnvironmentFile=-/etc/default/ceph
Environment=CLUSTER=ceph
ExecStart=/usr/bin/ceph-osd -f --cluster ${CLUSTER} --id %i --setuser root --setgroup root
ExecStartPre=/usr/lib/ceph/ceph-osd-prestart.sh --cluster ${CLUSTER} --id %i
ExecReload=/bin/kill -HUP $MAINPID
ProtectHome=true
ProtectSystem=full
PrivateTmp=true
TasksMax=infinity
Restart=on-failure
StartLimitInterval=30min
StartLimitBurst=30
RestartSec=20s

[Install]
WantedBy=ceph-osd.target

注意,Linux默认允许打开的文件数非常有限,而ceph-osd可能需要打开更多的文件,所以通过

LimitNOFILE=1048576

将允许该进程打开的文件数放大到了1M。

如果我们需要修改默认stacksize,那么,我们需要增加如下行:

LimitSTACK=2097152

我们将ceph-osd的栈大小从默认的8M改成了2M。

修改完毕之后需要执行:

systemctl daemon-reload

然后重启相关的服务,即可生效。

检查修改是否生效

如果我们修改了相关的资源限制,如何查看是否生效呢?

cat /proc/[PID]/limits

我们以一个测试环境为例:

root@CVM01:/lib/systemd/system# pidof ceph-osd
978185 928119
root@CVM01:/lib/systemd/system# cat /proc/928119/limits
Limit                     Soft Limit           Hard Limit           Units
Max cpu time              unlimited            unlimited            seconds
Max file size             unlimited            unlimited            bytes
Max data size             unlimited            unlimited            bytes
Max stack size            2097152              2097152              bytes
Max core file size        0                    unlimited            bytes
Max resident set          unlimited            unlimited            bytes
Max processes             1048576              1048576              processes
Max open files            1048576              1048576              files
Max locked memory         65536                65536                bytes
Max address space         unlimited            unlimited            bytes
Max file locks            unlimited            unlimited            locks
Max pending signals       257494               257494               signals
Max msgqueue size         819200               819200               bytes
Max nice priority         0                    0
Max realtime priority     0                    0
Max realtime timeout      unlimited            unlimited            us
root@CVM01:/lib/systemd/system#

我们可以看到我们修改的stacksize已经生效了,从默认的8M降低为2M。

]]>
thread in Rust 2020-12-19T11:00:00+00:00 Bean Li http://bean-li.github.io/thread-in-Rust thread::spawn

本文介绍Rust中的多线程并发。Rust提供了std::thread::spawn来创建线程。

use std::thread ;
use std::time::Duration;

fn main()
{
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {} from spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {} from main thread!", i);
        thread::sleep(Duration::from_millis(1));

    }

    handle.join().unwrap();
}

其中thread::spawn的定义如下:

pub fn spawn<F, T>(f: F) -> JoinHandle<T> 
where
    F: FnOnce() -> T,
    F: Send + 'static,
    T: Send + 'static, 

其返回值是JoinHandle类型。如果操作系统不能创建出来线程,respawn调用会panic。如果你的主程序,需要考虑spawn无法创建出来线程的情况,不能任由程序panic,那么你应该选择本文第三部分介绍的Builder::spawn,该接口会返回Result类型。

输出如下:

manu-Inspiron-5748 RBE/thread ‹master*› » cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/thread`
hi number 1 from main thread!
hi number 1 from spawned thread!
hi number 2 from main thread!
hi number 2 from spawned thread!
hi number 3 from main thread!
hi number 3 from spawned thread!
hi number 4 from main thread!
hi number 4 from spawned thread!
hi number 5 from spawned thread!
hi number 6 from spawned thread!
hi number 7 from spawned thread!
hi number 8 from spawned thread!
hi number 9 from spawned thread!

我们使用了join来等待线程退出,由于主进程和线程都有sleep 1毫秒的行为,如果没有主进程调用的join,那么随着主进程的退出,线程也退出了,后面的语句也不会打印出来。

use std::thread ;
use std::time::Duration;

fn main()
{
    let _handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {} from spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {} from main thread!", i);
        thread::sleep(Duration::from_millis(1));

    }

    //handle.join().unwrap();
}

运行结果如下:

hi number 1 from main thread!
hi number 1 from spawned thread!
hi number 2 from main thread!
hi number 2 from spawned thread!
hi number 3 from main thread!
hi number 3 from spawned thread!
hi number 4 from spawned thread!
hi number 4 from main thread!
hi number 5 from spawned thread!

join: 等待线程退出

std::thread::spawn 函数的返回类型是JoinHandle, 我们可以在JoinHandle上调用join方法,等待线程退出。

use std::thread ;
use std::time::Duration;

fn main()
{
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {} from spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    handle.join().unwrap();

    for i in 1..5 {
        println!("hi number {} from main thread!", i);
        thread::sleep(Duration::from_millis(1));

    }

}

上述代码中,主进程会等待线程退出之后,然后打印主进程的信息:

hi number 1 from spawned thread!
hi number 2 from spawned thread!
hi number 3 from spawned thread!
hi number 4 from spawned thread!
hi number 5 from spawned thread!
hi number 6 from spawned thread!
hi number 7 from spawned thread!
hi number 8 from spawned thread!
hi number 9 from spawned thread!
hi number 1 from main thread!
hi number 2 from main thread!
hi number 3 from main thread!
hi number 4 from main thread!

JoinHandle的join函数,原型如下:

pub fn join(self) -> Result<T>

其返回类型是Result类型。 因此在上面的示例中,我们需要调用unwrap或者expect来处理返回值,如果去除掉unwrap(),就会返回如下的警告:

warning: unused `std::result::Result` that must be used
  --> src/main.rs:14:5
   |
14 |     handle.join();
   |     ^^^^^^^^^^^^^^
   |
   = note: `#[warn(unused_must_use)]` on by default
   = note: this `Result` may be an `Err` variant, which should be handled

建议主线程总是调用join来检测线程的运行状态,原因如下:

  • 如果主线程不等待线程退出,获取线程的返回值,主线程可能较早退出,导致线程也不得不退出,功能失控
  • 如果某个线程的工作崩溃,而无论主线程还是其他线程都不做任何处理,程序可能会失控。而主进程通过join,可以获得线程的返回值是Err,从而得知线程崩溃,作出正确的处理决断。

定制线程

我们通过std::thread::spawn 可以创建一个线程,而这种线程,一般是典型的线程,线程没有名字,而且线程栈大小为默认的2MB,如果像定制特殊的线程,则需要std::thread::Builder。

目前来讲,有两个参数可以设定:

  • name:设定线程的名字
  • stack_size: 制定线程的栈大小
        let builder = Builder::new().name(thread_name).stack_size(size);
        let child = builder.spawn(move || {
            println!("I am child {}", id);
            thread::sleep(Duration::from_secs(100));
            println!("I am child {}", current().name().unwrap());
        }).unwrap();

注意Builder::new之后,还是需要调spawn函数,来唤起线程。

pub fn spawn<F, T>(self, f: F) -> Result<JoinHandle<T>>

主进程为了调用join,需要对Builder::spawn的返回值做unwrap处理,方能调用join函数,否则:

error[E0599]: no method named `join` found for enum `std::result::Result<std::thread::JoinHandle<()>, std::io::Error>` in the current scope
  --> src/main.rs:21:15
   |
21 |         child.join().unwrap();
   |               ^^^^ method not found in `std::result::Result<std::thread::JoinHandle<()>, std::io::Error>`

整体示例代码如下:

use std::thread;
use std::thread::{Builder, current} ;
use std::time::Duration;

fn main()
{
    let mut v = vec![];
    for id in 0..5 {
        let thread_name = format!("thread-{}", id);
        let size: usize = 8192 ;
        let builder = Builder::new().name(thread_name).stack_size(size);
        let child = builder.spawn(move || {
            println!("I am child {}", id);
            thread::sleep(Duration::from_secs(100));
            println!("I am child {}", current().name().unwrap());
        }).unwrap();

        v.push(child)
    }
    for child in v{
        child.join().unwrap();
    }

    for i in 1..5 {
        println!("hi number {} from main thread!", i);
        thread::sleep(Duration::from_millis(1));
    }

}

输出如下:

I am child 0
I am child 1
I am child 2
I am child 3
I am child 4
I am child thread-1
I am child thread-3
I am child thread-0
I am child thread-2
I am child thread-4
hi number 1 from main thread!
hi number 2 from main thread!
hi number 3 from main thread!
hi number 4 from main thread!

我们通过top -H -p [PID]来查看线程的名字是否生效:

使用move闭包

一般来讲,move闭包总是伴随着thread::spawn。我们先看下面的示例:

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(|| {
        println!("Here's a vector: {:?}", v);
    });

    handle.join().unwrap();
}

上述代码执行会出现报错:

error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
 --> src/main.rs:6:32
  |
6 |     let handle = thread::spawn(|| {
  |                                ^^ may outlive borrowed value `v`
7 |         println!("Here's a vector: {:?}", v);
  |                                           - `v` is borrowed here
  |
note: function requires argument type to outlive `'static`
 --> src/main.rs:6:18
  |
6 |       let handle = thread::spawn(|| {
  |  __________________^
7 | |         println!("Here's a vector: {:?}", v);
8 | |     });
  | |______^
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
  |
6 |     let handle = thread::spawn(move || {
  |                                ^^^^^^^

error: aborting due to previous error

线程只是要打印v的内容,闭包会尝试borrow v,但是有个致命的问题,即Rust推断不出来,线程要运行多久,以及v这个变量引用是否一直有效。考虑如下场景:

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(|| {
        println!("Here's a vector: {:?}", v);
    });

    drop(v); // oh no!

    handle.join().unwrap();
}

如果Rust允许执行上述代码,新创建的线程,很可能在主线程drop之后,调用println!,这种情况下,v已经失效了。这就不妙了。

通过在闭包之前,添加move关键字,我们会强制闭包获得他需要的值的所有权,而不仅仅是机遇Rust推导来获得值的借用。

修改正确之后的代码如下:

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn( move || {
        println!("Here's a vector: {:?}", v);
    });

    handle.join().unwrap();
}
]]>
rayon join in Rust 2020-10-07T11:00:00+00:00 Bean Li http://bean-li.github.io/Rayon-in-Rust 前言

Rust提供了std::thread::spawn,可以通过Fork-Join模式,完成并发。这个基本概念对于熟悉C/C++编程的程序员,并没有什么太多的挑战。介绍Rust并发的资料中,很多资料都不约而同地提到了Rayon,这个library非常有趣,也非常强大,他是Niko Matakis这位大神完成的。对这个大神感兴趣的,可以读他的blog

extern creat rayon ;
use rayon::prelude::* ;

let (v1, v2) = rayon::join(fn1, fn2); 

giant_vector.par_iter().for_each(|value| {
    do_something_with_value(value) ; 
});

rayon这个库,提供了非常方便的接口,程序员很容易将串行的接口改造成成并行的:

extern crate rayon ;
use rayon::prelude::*;

//sequential
let total_price = stores.iter()
                        .map(|store| store.compute_price(&list))
                        .sum();
//parallel                       
let total_price = stores.par_iter()
                        .map(|store| store.compute_price(&list))
                        .sum();                      

我们看到,将迭代器iter()变成了par_iter()就完成了并行处理,对串行代码的改造非常方便,其中par_iter是parallel iterator的缩写。

Rayon’s goal is to make it easy to add parallelism to your sequential code

Rayon的核心原语 join

join是Rayon的核心原语,前面提到的par_iter是构建在join之上的。因此,理解rayon,需要先理解join。

join的使用非常简单:

join(|| do_something(), || do_something_else());

其函数原型如下:

pub fn join<A, B, RA, RB>(oper_a: A, oper_b: B) -> (RA, RB) 
where
    A: FnOnce() -> RA + Send,
    B: FnOnce() -> RB + Send,
    RA: Send,
    RB: Send, 

在rayon实现中,是否会有并发线程一起处理两个closure,取决于是否有空闲的CPU core,即join的两个闭包是one by one地串行执行还是并发之行,取决于实际情况。

rayon采用了一种叫做work-stealing的技术,简单地说, join(a,b),我们有两个任务要处理,a和b,而且这两个任务是并发安全的。我们并不知道,threadpool中是否有idle的thread可以处理, 处理方法如下:

  • 把b放入到pending work queue
  • 执行a
  • 如果存在一个thread 空闲,扫瞄pending queue,如果找到任务,则执行它
  • 执行a任务的线程执行a完毕后,检查b的情况:
    • 是否有其他线程执行了b,如果没有,该线程负责执行b
    • 如果存在其他线程执行了b,等待期间,可以偷其他任务完成。

其伪代码大概如下:

fn join<A,B>(oper_a: A, oper_b: B)
    where A: FnOnce() + Send,
          B: FnOnce() + Send,
{
    // Advertise `oper_b` to other threads as something
    // they might steal:
    let job = push_onto_local_queue(oper_b);
    
    // Execute `oper_a` ourselves:
    oper_a();
    
    // Check whether anybody stole `oper_b`:
    if pop_from_local_queue(oper_b) {
        // Not stolen, do it ourselves.
        oper_b();
    } else {
        // Stolen, wait for them to finish. In the
        // meantime, try to steal from others:
        while not_yet_complete(job) {
            steal_from_others();
        }
        result_b = job.result();
    }
}

rayon join 示例

let mut v = vec![5, 1, 8, 22, 0, 44];
quick_sort(&mut v);
assert_eq!(v, vec![0, 1, 5, 8, 22, 44]);

fn quick_sort<T:PartialOrd+Send>(v: &mut [T]) {
   if v.len() > 1 {
       let mid = partition(v);
       let (lo, hi) = v.split_at_mut(mid);
       rayon::join(|| quick_sort(lo),
                   || quick_sort(hi));
   }
}

fn partition<T:PartialOrd+Send>(v: &mut [T]) -> usize {
    let pivot = v.len() - 1;
    let mut i = 0;
    for j in 0..pivot {
        if v[j] <= v[pivot] {
            v.swap(i, j);
            i += 1;
        }
    }
    v.swap(i, pivot);
    i
}

上面给出了一个rayon join的示例,该示例中,join充分利用了多核,即多个CPU一起发挥作用参与排序。在一个4-Core的Macbook Pro上,我们可以看到,随着数组长度的增大,排序效率比单线程的quicksort快很多,因为是4-Core,因此最多也是快4倍,无法更快了。

Array Length Speedup
1K 0.95x
32K 2.19x
64K 3.09x
128K 3.52x
512K 3.84x
1024K 4.01x

这个结果是原作者对代码做了一些优化,即如数组长度低于5K,就使用串行的排序:

fn quick_sort<J:Joiner, T:PartialOrd+Send>(v: &mut [T]) {
    if v.len() <= 1 {
        return;
    }

    if J::is_parallel() && v.len() <= 5*1024 {
        return quick_sort::<Sequential, T>(v);
    }

    let mid = partition(v);
    let (lo, hi) = v.split_at_mut(mid);
    J::join(|| quick_sort::<J,T>(lo),
            || quick_sort::<J,T>(hi));
}
]]>
Arc in Rust 2020-10-07T11:00:00+00:00 Bean Li http://bean-li.github.io/Arc-in-Rust 前言

在Rust语言中,所有权大多数情况下是明确的,对于一个给定的值,你可以准确地判断出,哪个变量拥有它。但是也存在一些情况,单个值可能同时被多个所有者持有。

请看下面的例子:

`  fn process_files_in_parallel(filenames: Vec<string>,  glossary: &GigabyteMap) 
     -> io::Result<()> 
 {
      ...
      for worklist in worklists
      {
          thread_handlers.push(
              spawn(move || process_files(worklist, glossary)
          );
      }
      ....
 }
`

上面的例子中,拟采用多个进程,并发地处理文件。但是处理文件需要一个消耗巨大内存的数据结构, glossary,可能数据结构大小超过GB,这种情况下,拷贝多个副本给不同线程使用无疑是无法承受的。但是上面的代码,存在一个问题,即glossary的生命周期。

因为glossary并不能保证在所有的子进程完成任务之前不被销毁,如果被销毁,会导致子进程无法正常完成任务。

Rc and Arc

Rust提供了一个名为Rc的类型来支持多重所有权,它名称中的Rc是Reference Counting 引用计数的缩写。Rc类型会在内部维护一个记录值引用计数的计数器,从而确定这个值是否仍在被使用。如果对一个值的引用计数变成了0,意味着这个值可以被安全地清理掉。

那Arc是干啥的呢?Arc具备Rc的一切特征,那是Rc不是线程安全的,所以Rust又提供了一个Arc的类型,来确保多个线程之前操作某个变量的安全性。这个Arc中的A指的是Atomic,即Atomic Reference Counting。

那看起来是Arc是更强大的Rc,能够保证安全地用在并发的场景,那为什么不干脆消灭掉Rc,统统只使用Arc呢?

没有免费的午餐,Arc更强大,但是这种强大,需要付出一些性能开销才能做到,如果是单线程条件下,我们其实不需要付出这种开销。因此

  • 单线程条件下,可以使用Rc
  • 多线程并发条件下,建议使用Arc
use std::thread;
use std::time::Duration;
use std::sync::Arc;


fn main() {
    let foo = Arc::new(vec![0]);
    for _ in 0..10 {
        let bar = Arc::clone(&foo);
        thread::spawn(move || {
            thread::sleep(Duration::from_millis(200));
            println!("{:?}", &bar);
        });
    }
    println!("{:?}", foo);
}

上面的用法中, Arc::clone 并不是复制了一份vec,而是仅仅增减了一个引用计数,这是多个线程之间共享数据的一个方法。

回到我们最开始的问题,巨大的数据结构,多个线程之间共享:

fn process_files_in_parallel(filenames: Vec<String>, glossary: Arc<GigabyteMap>)
   -> io::Result<()>
{
   ....
   for worklist in worklists 
   {
       let glossary_for_child = glossary.clone() ;
       thread_handlers.push(
           spawn(move || process_files(worklist, &glossary_for_child))
       );
   }
   ....
   
}
]]>
Option和Result相关的组合算子 2020-05-05T20:00:00+00:00 Bean Li http://bean-li.github.io/Option-Result-combinator 前言

上一篇文章,从宏观的角度介绍了Rust错误处理的方法。提到了Option,作为弱化版的Result。

enum Option<T>{
	None,
	Some(T),
}

Option作为Rust的的系统类型,用来表示值不存在的可能。在编程中,这是一个非常好的做法,因为它强制Rust检测和处理值不存在的情况。

Option和Result,Rust都提供了一些组合算子来,来简化代码的书写,提供更强大的表达力。

Option相关的组合算子

ok_or和ok_or_else

这两个是一组,作用都是从Option转成Result。

pub fn ok_or<E>(self, err: E) -> Result<T, E>

pub fn ok_or_else<E, F>(self, err: F) -> Result<T, E>
where
    F: FnOnce() -> E, 

其作用细细来讲,如下:

  • Some(v) –> Ok(v)
  • None –> Err(err)

其行为大概如下所示:

fn ok_or<T, E>(option: Option<T>, err: E) -> Result<T, E> {
    match option {
        Some(val) => Ok(val),
        None => Err(err),
    }
}

下面以一个例子来展示这个组合算子的作用

use std::env;

fn double_arg(mut argv: env::Args) -> Result<i32, String> {
    argv.nth(1)
        .ok_or("Please give at least one argument".to_owned())
        .and_then(|arg| arg.parse::<i32>().map_err(|err| err.to_string()))
}

fn main() {
    match double_arg(env::args()) {
        Ok(n) => println!("{}", n),
        Err(err) => println!("Error: {}", err),
    }
}

如果执行,不给任何参数:

Error: Please give at least one argument

给一个参数为5,输出如下:

5

nth(1) 因为没有第一个参数(0-index),所以Option的值为None,ok_or将Option转成了Result,最终返回的Err(err)。

map

这个组合算子的作用是将Option 转换成Option

  • Some(T) ->Some(U)
  • None -> None
fn main() {
    let maybe_some_string = Some(String::from("Hello, World!"));
    let maybe_some_len = maybe_some_string.map(|s| s.len());

    assert_eq!(maybe_some_len, Some(13));
}

注意map是值传递,会消耗掉原始的值。

and_then

and_then 又被称为flatmap,它的作用是从一个Option转成另一Option:

  • None -> None
  • Some(x) -> Some(f(x))

对于Some而言,and_then把包裹的值作为参数

fn sq(x: u32) -> Option<u32> { Some(x * x) }
fn nope(_: u32) -> Option<u32> { None }

assert_eq!(Some(2).and_then(sq).and_then(sq), Some(16));
assert_eq!(Some(2).and_then(sq).and_then(nope), None);
assert_eq!(Some(2).and_then(nope).and_then(sq), None);
assert_eq!(None.and_then(sq).and_then(sq), None);

注意sq函数的入参,是u32型,并不是Some()。

Result相关的组合算子

is_ok and is_err

result.is_ok和result.is_err返回bool型的结果。

对于is_ok而言:

  • Ok(T) 返回true
  • Err(E) 返回false

对于is_err而言:

  • Ok(T)返回false
  • Err(E)返回true
let x: Result<i32, &str> = Err("Some error message");
assert_eq!(x.is_err(), true);

let x: Result<i32, &str> = Ok(-3);
assert_eq!(x.is_ok(), true);

ok and err

pub fn ok(self) -> Option<T>
pub fn err(self) -> Option<E>

注意,这两个算子都是将Result转成Option类型,一个关注成功的情况,一个关注出错的情况。

result.ok() 如果Result返回Ok(T),那么返回值是Option,即Some(success_value)。如果Result失败,那么该函数返回None,把Err的值丢弃掉。

let x: Result<u32, &str> = Ok(2);
assert_eq!(x.ok(), Some(2));

let x: Result<u32, &str> = Err("Nothing here");
assert_eq!(x.ok(), None);

result.err()如果Result失败,返回Err(E),那么返回返回Option

let x: Result<u32, &str> = Ok(2);
assert_eq!(x.err(), None);

let x: Result<u32, &str> = Err("Nothing here");
assert_eq!(x.err(), Some("Nothing here"));

and_then

Option也有and_then 算子,Result和Option一样的,语义是一样的。而且有很多这样的算子。

use std::env;

fn double_arg(mut argv: env::Args) -> Result<i32, String> {
    argv.nth(1)
        .ok_or("Please give at least one argument".to_owned())
        .and_then(|arg| arg.parse::<i32>().map_err(|err| err.to_string()))
}

fn main() {
    match double_arg(env::args()) {
        Ok(n) => println!("{}", n),
        Err(err) => println!("Error: {}", err),
    }
}

还是前面的例子, ok_or()之后,Option就转成了Result,如果Ok的情况下,类型是字符串,那么and_then 会尝试将字符串解析成i32。

pub fn and_then<U, F>(self, op: F) -> Result<U, E>
where
    F: FnOnce(T) -> Result<U, E>, 

如果Result的值是Ok(T),那么执行op(T),否则直接返回Err。

n sq(x: u32) -> Result<u32, u32> { Ok(x * x) }
fn err(x: u32) -> Result<u32, u32> { Err(x) }

assert_eq!(Ok(2).and_then(sq).and_then(sq), Ok(16));
assert_eq!(Ok(2).and_then(sq).and_then(err), Err(4));
assert_eq!(Ok(2).and_then(err).and_then(sq), Err(2));
assert_eq!(Err(3).and_then(sq).and_then(sq), Err(3));

map and map_err

上面的例子中给出了map_err的使用。map和map_err是对Result中的Ok(T)或者Err(E)执行某个函数或者执行某种变换,得到新的Result:

pub fn map<U, F>(self, op: F) -> Result<U, E>
where
    F: FnOnce(T) -> U, 
    
pub fn map_err<F, O>(self, op: O) -> Result<T, F>
where
    O: FnOnce(E) -> F, 

对于map函数而言:

  • 如果Result值是Ok(T),对T执行函数
  • 如果Result的值是Err(E),保持不变,新的结果Option的值也是Err(E)

如下实例代码:

let line = "1\n2\n3\n4\n";

for num in line.lines() {
    match num.parse::<i32>().map(|i| i * 2) {
        Ok(n) => println!("{}", n),
        Err(..) => {}
    }
}

对于map_err而言:

  • 如果Result的值是Err(E),对E执行函数
  • 如果Result的值是Ok(T),保持不变。结果Result和输入Result一致。
fn stringify(x: u32) -> String { format!("error code: {}", x) }

let x: Result<u32, u32> = Ok(2);
assert_eq!(x.map_err(stringify), Ok(2));

let x: Result<u32, u32> = Err(13);
assert_eq!(x.map_err(stringify), Err("error code: 13".to_string()));

map_err可以用来对错误进行转换。

代码赏析

最后的最后,赏析一段Error Handle的代码

use std::fs::File;
use std::io::Read;
use std::path::Path;

fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, String> {
    File::open(file_path)
        .map_err(|err| err.to_string())
        .and_then(|mut file| {
            let mut contents = String::new();
            file.read_to_string(&mut contents)
                .map_err(|err| err.to_string())
                .map(|_| contents)
        })
    .and_then(|contents| {
        contents.trim().parse::<i32>()
            .map_err(|err| err.to_string())
    })
    .map(|n| 2 * n)
}

fn main() {
    match file_double("foobar") {
        Ok(n) => println!("{}", n),
        Err(err) => println!("Error: {}", err),
    }
}

上面这段代码中,有很多种不同的错误类型:

  • io::Error
  • num::ParseIntError

通过map_err都转成了Result<i32,String>类型。

上述写法看起来技巧性很强,但是我不喜欢,属于炫技派做法,心智负担比较重。

use std::fs::File;
use std::io::Read;
use std::path::Path;

fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, String> {
    let mut file = match File::open(file_path) {
        Ok(file) => file,
        Err(err) => return Err(err.to_string()),
    };
    let mut contents = String::new();
    if let Err(err) = file.read_to_string(&mut contents) {
        return Err(err.to_string());
    }
    let n: i32 = match contents.trim().parse() {
        Ok(n) => n,
        Err(err) => return Err(err.to_string()),
    };
    Ok(2 * n)
}

fn main() {
    match file_double("foobar") {
        Ok(n) => println!("{}", n),
        Err(err) => println!("Error: {}", err),
    }
}

这段代码,看起来更舒服,更易懂。

]]>
Rust错误处理 2020-05-04T23:00:00+00:00 Bean Li http://bean-li.github.io/Error-Handle-in-Rust 前言

本文介绍Rust的错误处理。Rust的错误处理有其独到之处,非常值得学习。Rust将错误分成两类:

  • 不可恢复的错误
  • 可恢复的错误

不可恢复的错误一般是程序编写出现疏漏,不应该发生的事情发生了

  • 比如除以0 ,引发SIGFPE
  • 访问数组元素越界
  • thread::spawn 无法创建线程 (比如内存耗尽 process bomb等原因)

这些错误是不该发生的,一般伴随着程序员代码中的某种错误,这种情况下,一般是不可处理的,程序员也不知道该如何处理这种错误,程序不宜继续运行下去。对于这种错误,Rust一般采用panic的做法,即让程序崩溃,立刻停止执行。

可恢复的错误,是指一些特殊的情况,但是这种情况发生了,我们完全可以处理。

比如说我们要创建一个配置文件,我们会尝试open这个文件,如果open失败,报不存在这个文件,这尽管是个报错,但是没关系,我们可以处理,我们可以创建此文件。对于这种类型的错误,Rust引入了Result,来处理。

下面我们分别针对panic和Result两种处理方式,分开阐述。

不可恢复的错误与panic!

一般来讲,不可恢复的错误是不该发生的,如下面场景:

  • 访问数组元素越界
  • 除以0
  • 资源耗尽,无法创建进程

这种事情一旦发生,一般伴随着程序中存在bug。这种情况下,任何的修复尝试都是徒劳的,程序尽快终止才是最合理的做法。Rust提供了panic!宏。

panic!宏会接收println!-style的参数,将出错信息打印出来。

fn my_div(n: i32, m: i32) -> i32 {
    if m == 0 {
        panic!("Invalid number: {}, div 0", m);
    }
    n/m
}

fn main() {
    println!("result is {}", my_div(9, 2));
    println!("result is {}", my_div(4, 0));
}

​ 输出如下:

result is 4
thread 'main' panicked at 'Invalid number: 0, div 0', src/main.rs:3:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

出了上面列出的通用场景可能会引发panic,从编程角度讲,有哪些情况可能会产生panic呢?

  • Option::None 遭遇 unwrap()
  • Option::None 遭遇 expect()
  • Result::Err 遭遇unwrap()
  • Result::Err 遭遇 expect()
  • 直接调用panic!宏

发生panic或者调用panic!宏之后,Rust的行为有两种可选

  • unwind (展开)
  • abort (终止)

unwind

unwind是默认的行为,采用这种行为的条件下,下面事情会发生:

  • 打印报错信息
  • 程序会利用栈回退(stack unwind)机制,确保栈内构造的局部变量和指针的析构函数被一一调用掉,完成清理动作。
  • 触发panic的线程会exit。

注意第三点,panic是per thread的。对于一个多线程程序而言,如果一个线程panic了,那么其他线程可以继续正常运行。这是多线程章节里面的内容,此处不展开。

abort

遇到panic!另一种处理方法是abort,即立即退出,不做清理动作。我们可以在Cargo.toml中的[profile]中添加:

panic = 'abort'

或者采用如下方法:

rustc -C panic=abort src/main.rs

选择发生发生panic时,直接abort。

result is 4
thread 'main' panicked at 'Invalid number: 0, div 0', src/main.rs:3:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
[1]    22576 abort (core dumped)  ./main

catch_unwind

我们很多人都学习过C语言,语言崩溃的时候,也可以打印出stack,但是对于这种意想不到的事情发生的时候,尽可能丰富的打印当时的情况下信息,是后续调试 debug和改进的关键。

Rust在std::panic::catch_unwind 来捕捉panic。我们可以看下刚才的程序。

use std::panic ;

fn my_div(n: i32, m: i32) -> i32 {
    if m == 0 {
        panic!("Invalid number: {}, div 0", m);
    }
    n/m
}

fn main() {
    let result = panic::catch_unwind(|| {
        println!("result is {}", my_div(9, 2));
        println!("result is {}", my_div(4, 0));
    }) ;

    match result {
        Ok(()) => println!("every thing goes OK"),
        Err(_) => println!("catch panic")
    }
}

我们调用了panic::catch_unwind ,将我们觉得有危险的部分可能panic的部分,包了起来。如果发生了panic,会被捕捉到。

运行结果如下:

result is 4
thread 'main' panicked at 'Invalid number: 0, div 0', src/main.rs:5:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
catch panic

注意,panic的默认行为还是unwind,所以你看到了错误信息打印之类的,但是进程并没有退出,后面的catch panic得到了执行。

如果不想打印出来panicked at 之类的信息,应该怎么办?

std::panic提供了set_hook和take_hook两个方法:

use std::panic ;

fn my_div(n: i32, m: i32) -> i32 {
    if m == 0 {
        panic!("Invalid number: {}, div 0", m);
    }
    n/m
}

fn main() {
    /*注册新的hook*/
    panic::set_hook(Box::new(|_info| {
        //do_nothing
    }));

    let result = panic::catch_unwind(|| {
        println!("result is {}", my_div(9, 2));
        println!("result is {}", my_div(4, 0));
    }) ;

    match result {
        Ok(()) => println!("every thing goes OK"),
        Err(_) => println!("catch panic")
    }

    let _ = panic::take_hook();

    let result = panic::catch_unwind(|| {
        println!("result is {}", my_div(9, 2));
        println!("result is {}", my_div(4, 0));
    }) ;

    match result {
        Ok(()) => println!("every thing goes OK"),
        Err(_) => println!("catch panic")
    }

}

因为首先用set_hook注册了新的hook,而新的hook什么也不做,所以,前半部的输出如下:

result is 4
catch panic

后半部分,take_hook的作用是 unregister当前的hook,并且返回当前hook值。所以后半部分,有回归到了默认的行为,尽管被捕捉,但是还是会执行默认的unwind。

catch_unwind这个方法,很容易被熟悉python语言或者Java语言的人当作 try-catch来用,用于捕捉异常。但是下面的话需要务必重视:

极其不推荐使用catch_unwind处理常规错误。

不要用catch_unwind来做正常的流程控制,它的主要用途在:

  1. 在FFI场景下的时候,如果说C语言调用了Rust的函数,在Rust内部出现了panic,如果这个panic在Rust内部没有处理好,直接扔到C代码中去,会导致C语言产生“未定义行为(undefined behavior)”
  2. 某些高级抽象机制,需要阻止栈展开,比如线程池,如果一个线程中出现了panic,我们希望只把这个线程关闭,而不至于将整个线程池一起被拖下水

Result

Rust其实提供了一个分层的错误处理方案:

  • if something might reasonably be absent, Option is used
  • if something goes wrong and can reasonably be handled, Result is used
  • if something goes wrong and cannot reasonably be handled, the thread panics
  • if something catastrophic happens, the program aborts

这个分层方案中,第一层就是Option,第二层是Result,其实严格意义上讲,Option,是Result的特例:

enum Option<T>{
	None,
	Some(T),
}

enum Result<T, E>{
	Ok(T),
	Err(E),
}

使用Option的时候,一般表示不太关心出错原因,出错的时候,直接返回None,而Result的表达力要比Option更强,将错误的不同原因也包括进来。因此从这个角度讲,Option不过是Result的特例:

Option<T>  ~ Result<T, ()>

比如从HashMap中取值或者对Vector进行pop操作,前者出错的原因只有键值即key不存在,后者出错的原因只有Vector已经是空的了,这种情况下,Option比Result更合适。

比如从File中读取某个信息,出错的可能性很多,比如文件不存在,没有读取权限,数据不合法等,这种情况下,Option就力不逮己了,需要用Result<T,E>。

Option

什么时候使用Option?

  • 如果一个函数的某个参数的可选的
  • 一个函数有返回值,但是其返回值可能是空的
  • 一个数据类型,其值可能会为空

对于第一种情况:

fn get_full_name(fname: &str, lname: &str, mname: Option<&str>) -> String { // middle name can be empty
  match mname {
    Some(n) => format!("{} {} {}", fname, n, lname),
    None => format!("{} {}", fname, lname),
  }
}

fn main() {
  println!("{}", get_full_name("Galileo", "Galilei", None));
  println!("{}", get_full_name("Leonardo", "Vinci", Some("Da")));
}

middle name可能会空,所以用Option类型传递,如果为空,传None,如果不为空,传递Some(T)。

输出如下:

Galileo Galilei
Leonardo Da Vinci

对于第二种情况,比较典型的是从HashMap查找某键值对应的value,可能会不存在,或者从Vector中pop,可能会Vector已经为空,那么函数的返回值,最好是Option,因为Option类型会强制函数调用方必须要检查是None的情形,防止程序员编程时忽略。

我们以我们的除法为例,除以0会触发panic,但是我们可以封装一个除法,检查除数是否是0,如果除数是0,那么返回None,否则,返回Some(T)。

fn checked_div(n: i32, m: i32) -> Option<i32> {
    if m == 0 {
        None
    }
    else {
        Some(n/m)
    }
}

fn try_div(dividened: i32, divisor: i32){
    match checked_div(dividened, divisor){
        None => println!("{} / {} failed", dividened, divisor),
        Some(result) => {
            println!("{} / {} = {}", dividened, divisor, result)
        },
    }
}

fn main() {
    try_div(9,2);
    try_div(9,0);
}

返回值是Option,所以try_div函数,必须要判断结果等于None时候,防止默认结果总是存在的。输出如下:

9 / 2 = 4
9 / 0 failed

另一个比较典型的例子是dirs::home_dir,在Linux下很多用户可能存在HOME 目录,但是也可能不存在,因此返回值是Option类型:

pub fn home_dir() -> Option<PathBuf>

示例代码如下:

extern crate dirs;

fn main() {
    let home_path = dirs::home_dir();
    match home_path {
        Some(p) => println!("{:?}", p), 
        None => println!("Can not find the home directory!"),
    }
}

输出如下:

"/home/manu"

Result

Option 介绍完毕之后,就可以安心介绍最重要的Error Handle方法了,就是Result:

enum Result<T, E>{
	Ok(T),
	Err(E),
}

Result本质是一种enum,Ok表示执行成功的情况,Err表示执行失败的情况,其中T和E分别是Ok或者Err时,对应返回值的类型。如果你的函数或者方法,可能失败可能出错,建议返回类型选择Result。

枚举类型实例表示定义中的成员中的任意一项,反映到我们Result类型,就是要么时Ok,要么是Err,但是不可能既是Ok,又是Err,因此,如果函数返回Result类型,那么结果处于薛定谔的猫,即处于生和死的叠加,即Result处于Ok和Err的叠加态,函数不返回,无从得知到底是否成功。

那用这种数据类型有什么好处呢?好处在于,无论程序是否出错,函数的返回值的类型是相同的,函数调用者可以用相同的方式处理Result。从这个角度说,无论成功与否,函数调用者都要处理Result,错误处理称为业务逻辑的一部分,而不能随意的忽略。

(过去几年间,我处理过非常多的现场问题,我也修复过非常多的bug,我看过太多bug,仅仅是忽视Error Handle,程序编写者对于可能出现的错误并不在意,轻率或者轻浮地认为,很多错误并不会发生,或者发生了也就随它去吧,并不会认真地思考,这种错误发生的时候,应该怎么处理才比较妥当。)

match处理 Result类型结果

因为Result类型的值是enum,所以典型的处理方法是模式匹配match。

Rust By Example中的的例子比较典型:

mod checked {
    // Mathematical "errors" we want to catch
    #[derive(Debug)]
    pub enum MathError {
        DivisionByZero,
        NonPositiveLogarithm,
        NegativeSquareRoot,
    }

    pub type MathResult = Result<f64, MathError>;

    pub fn div(x: f64, y: f64) -> MathResult {
        if y == 0.0 {
            // This operation would `fail`, instead let's return the reason of
            // the failure wrapped in `Err`
            Err(MathError::DivisionByZero)
        } else {
            // This operation is valid, return the result wrapped in `Ok`
            Ok(x / y)
        }
    }

    pub fn sqrt(x: f64) -> MathResult {
        if x < 0.0 {
            Err(MathError::NegativeSquareRoot)
        } else {
            Ok(x.sqrt())
        }
    }

    pub fn ln(x: f64) -> MathResult {
        if x <= 0.0 {
            Err(MathError::NonPositiveLogarithm)
        } else {
            Ok(x.ln())
        }
    }
}

// `op(x, y)` === `sqrt(ln(x / y))`
fn op(x: f64, y: f64) -> f64 {
    // This is a three level match pyramid!
    match checked::div(x, y) {
        Err(why) => panic!("{:?}", why),
        Ok(ratio) => match checked::ln(ratio) {
            Err(why) => panic!("{:?}", why),
            Ok(ln) => match checked::sqrt(ln) {
                Err(why) => panic!("{:?}", why),
                Ok(sqrt) => sqrt,
            },
        },
    }
}

fn main() {
    // Will this fail?
    println!("{}", op(1.0, 10.0));
}

在上述例子中: \(f(x,y) = sqrt(ln(x/y))\) 这个计算中有很多的失败可能:

  • y = 0 ,导致 x/y panic 即 DivisionByZero
  • x/y的结果 < 0 , 导致 ln运算异常 即NonPositiveLogarithm
  • ln(x/y)的结果 < 0 ,导致sqrt异常 (不考虑无理数)即NegativeSquareRoot

因为有异常的可能,因此,每一步运算的结果,都是Result类型。每一层运算,都要用match来模式匹配,判断是否有异常发生。

thread 'main' panicked at 'NegativeSquareRoot', src/main.rs:48:29
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

因为用match来处理错误是最常用最推崇的方法,我们下面再给出一个例子,来介绍用match来处理Result。

use std::fs::File ;

fn main()
{
    let f = File::open("hello.txt");

    let _f = match f {
        Ok(file) => file ,
        Err(error) => {
            panic!("There was a problem opening the file {:?}", error)
        },
    };
}

结果用match匹配,如果出错,处理方法是panic!。

thread 'main' panicked at 'There was a problem opening the file Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:10:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

上面两个例子中,出现异常都是粗暴的panic,但是以open为例,如果hello.txt不存在的话,可能我们可以接受,比如我们创建一个空文件出来。

use std::fs::File ;
use std::io::ErrorKind ;

fn main()
{
    let f = File::open("hello.txt");

    let _f = match f {
        Ok(file) => file ,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hell.txt") {
                Ok(fc) => fc ,
                Err(e) => panic!("Try to create file but encounter a problem{:?}", e)
            },
            other_error => panic!("There was a problem opening the file {:?}", other_error),
        },
    };
}

open失败的话,如果是NotFound错误,那么我们就尝试创建该文件。

创建file的接口为 std::fs::File::create ,这一次没有错误的写成creat。C接口中,创建文件的系统调用为creat,被戏称成最大的遗憾。go语言没有此遗憾,Rust一样没有此遗憾。

unwrap 和 expect

处理Result类型,一般是用match,对于Err也要必须处理。但是这个世界上懒人是很多的,如果程序员确实不愿意处理Err,或者说处理也无益,直接崩溃是比较合理的做法,Rust对于这种情况提供了快捷方式,即unwrap和expect。

use std::fs::File ;

fn main()
{
    let _f = File::open("hello.txt").unwrap();
}

输出如下:

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:5:14
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

如果使用expect函数:

use std::fs::File ;

fn main()
{
    let _f = File::open("hello.txt").expect("failed to open hello.txt");
}

输出如下:

thread 'main' panicked at 'failed to open hello.txt: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:5:14
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

在我们上面的代码中,File::open返回io::Result, Rust标准库中,有很多Result类型,其中io::Result就是其中之一。io::Result实现了unwrap和expect方法。如果函数执行成功,那么unwrap 和 expect方法就将正确的值取出来,如果出错,不做任何处理,直接panic。expect函数和unwrap函数功能点很接近,unwrap会打印出标准库内置的错误信息,而expect函数允许用户定义一个字符串,在程序挂掉的时候显示。

尽管我们介绍了unwrap和expect,但是这并不是推荐的错误处理方式,事实上,在严肃的商用程序上,不希望看到这两个函数出现。我们一般讲这两个函数用于原型设计。

传播错误

Rust允许程序像其他语言处理异常一样,讲错误扔给更上一层调用者,这被称为传播错误, Propagating Error。

use std::io ;
use std::io::Read;
use std::fs::File ;


fn read_username_from_file() -> Result<String, io::Error>{
    let f = File::open("hello.txt");

    let mut f = match f {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut s = String::new() ;
    match f.read_to_string(&mut s){
        Ok(_) => Ok(s),
        Err(e) => Err(e),
    }

}
fn main()
{
    let _s = read_username_from_file().unwrap();
}

我们看下上面的代码,从hello.txt中读取内容到String中,这个过程中可能会遇到很多错误:

  • open 失败
  • 读取失败

写程序的时候,会写很多的match,总体来讲,这些错误都是同一种类型的错误即 io::Error,对于read_username_from_file而言,对于这些io::Error也并没有做什么特殊的处理,遇到任何io::Error,提前返回错误,否则,将String返回。

Rust提供了传播错误的快捷方式,这个堪称比较好用的语法糖了。

use std::io ;
use std::io::Read;
use std::fs::File ;

fn read_username_from_file() -> Result<String, io::Error>{
    let mut f = File::open("hello.txt")?;
    let mut s = String::new() ;
    f.read_to_string(&mut s)? ;
    Ok(s)

}
fn main()
{
    let _s = read_username_from_file().unwrap();
}

第五行 open后面的? ,作用如下:

  • open成功,将Result中Ok包裹的值,赋值给变量f
  • open如果失败,?会让整个函数退出执行,并且返回Err(error)给调用者。

有了这个神器,代码就可以写的比较简洁了,不需要一坨代码,match来match去。

事实上上面的代码还可以写的更加简洁:

fn read_username_from_file() -> Result<String, io::Error>{
    let mut s = String::new() ;
    File::open("hello.txt")?.read_to_string(&mut s)?;
    Ok(s)
}
]]>