Rust-线程,使用线程同时运行代码

  在大部分现代操作系统中,已执行程序的代码在一个进程(process)中运行,操作系统则负责管理多个进程。在程序内部,也可以拥有多个同时运行的独立部分。运行这些独立部分的功能被程序为线程(threads)。

  将程序中的计算拆分进多个线程可以改善性能,因为程序可以同时进行多个任务,不过这也会增加复杂性。因为线程是同时运行的,所以无法预先保证不同线程中的代码的执行顺序。这会导致如此类的问题:

  • 竞争状态(Race conditions),多个线程以不一致的顺序访问数据或资源
  • 死锁(Deadlocks),两个线程相互等待对方停止使用其所拥有的资源,这会阻止它们继续运行
  • 只会发生在特定情况且难以稳定重现和修复的bug

编程语言有一些不同的方法来实现线程。很多操作系统提供了创建新线程的api。这种由编程调用系统api创建线程的模型有时被称为1:1,一个os线程对应一个语言线程。

很多编程语言提供了自已特殊的线程实现。编程语言提供的线程被称为绿色(green)线程,使用绿色线程的语言会在不同数量的os线程的上下文中执行它们。

为此,绿色线程模式被称为m:n模型:m个绿色线程对应n个OS线程,这里m和n不必相同。

使用spawn创建新线程

为了创建一个新线程,需要调用thread::spawn函数并传递一个闭包,并在其中包含希望在新线程运行的代码。以下例子在主线程打印了一些文本而另一些文本则由新线程打印:

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

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

 注意这个函数编写的方式,当主线程结束后,新线程也会结束,而不管其是否执行完毕。

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

 thread::sleep 调用强制线程停止执行一小段时间,这会允许其它不同的线程运行。这些线程可能会轮流运行,不过并不保证如此:这依赖操作系统如保调度线程。在这里,主线程首先打印,即便新创建线程的打印语句位于程序的开头,甚至即便们告诉新建的线程打印直到i等于9,它在主线程结束之前也只打印到了5。

如果运行代码只看到了主线程的输出,或没有出现重叠打印的现象,尝试增大区间(变量i的范围)来增加操作系统切换线程的机会。

使用join等待所有线程结束

由于主线程结束, 以上代码大部分时候不光会提早结束新建线程,甚至不能实际保证新建线程会被执行。其原因在于无法保证线程运行的顺序。

可以通过将thread::spwan的返回值储存在变量中来修复新建线程部分没有执行或者完全没有执行的问题。thread::spawn的返回类型是JoinHandle。JoinHandle是一个拥有所有权的值,当对其调用join方法时,它会等待其线程结束。以下代码展示了上一个示例中创建的线程的JoinHandle并调用join来确保新建线程在main退出前结束运行:

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

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

    handle.join().unwrap();

通过调用handle的join会阻塞当前线程直到handle所代表的线程结束。阻塞(blocking)线程意味着阻止该线程执行工作或退出。因为我们将join调用放在了主线程的for循环之后,会产生类型这样的输出:

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

这两个线程仍然会交替执行,不过主线程会由于handle.join()调用会等待直到新建线程执行完毕。

不过让我们看看将handle.join()移动到main中for循环之前会发生什么,如下:

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

主线程会等待直到新建线程执行完毕之后才开始执行for循环,所以输也将不会交替出现:

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

线程与move闭包

move闭包,其经常与thread::spawn一起使用,因为它允许我们在一个线程中使用别一个线程的数据。

我们之前了解到可以在参数列表前使用move关键字强制闭包获取其使用的环境的所有权。这个技巧在创建新建线程将值的所有权从一个线程移动到另一个线程时最为实用。

上面的例子中传递给thread::spawn的闭包并没有任何参数:并没有在新建线程代码中使用任何主线程的数据。为了在新建线程中使用来自于主线程的数据,需要新建线程的闭包获取它需要的值。下面示例展示了一个尝试在主线程中创建一个vector并用于新建线程的例子,不过这么写还不能工作,如下所示:

    let v = vec![1, 2, 3];

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

    handle.join().unwrap();

闭包使用了v,所以闭包会捕获v并使其成为闭包环境的一部分。因为thread::spawn在一个新线程中运行这个闭包,所以可以在新线程中访问v。然而当编译这个例子时,会得到如下错误:

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

Rust会推断如何捕获v,因为println!只需要v的引用,而闭包尝试借用v。然而这有一个问题:Rust不知道这个新建线程会执行多久,所以无法知晓v的引有是否一直有效。

为了修复以上的编译错误,我们可以听取错误信息的建议:

help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
   |
53 |     let handle = thread::spawn(move || {
   |                                ^^^^^^^

通过在闭包之前增加move关键字,我们强制闭包获取其使用的值的所有权,而不是任由Rust推断它应该借用值。以下展示修改,可以按照我们的预期编译并运行:

    let v = vec![1, 2, 3];
    let handle = thread::spawn(move || {
        println!("here is a vector: {:?}",v);
    });
    handle.join().unwrap();

使用了move闭包,将会把v移动进闭包的环境中,现在可通过编译并能成功打印v。

如果我们加上drop,并试运行:

    let v = vec![1, 2, 3];
    let handle = thread::spawn(move || {
        println!("here is a vector: {:?}",v);
    });
    drop(v);
    handle.join().unwrap();

会出现以下错误:

error[E0382]: use of moved value: `v`
  --> src/main.rs:56:10
   |
52 |     let v = vec![1, 2, 3];
   |         - move occurs because `v` has type `Vec<i32>`, which does not implement the `Copy` trait
53 |     let handle = thread::spawn(move || {
   |                                ------- value moved into closure here
54 |         println!("here is a vector: {:?}",v);
   |                                           - variable moved due to use in closure
55 |     });
56 |     drop(v);
   |          ^ value used here after move

Rust的所有权规则又一次帮助了我们。当在主线程中使用v时就会违反所有权的规则。move关键字覆盖了Rust默认保守的借用,但它不允许我们违反所有权规则。

 

posted @ 2021-10-02 11:30  johnny_zhao  阅读(212)  评论(0编辑  收藏  举报