Appearance
27.1 什么是线程
线程是操作系统能够进行调度的最小单位,它是进程中的实际运作单位,每个进程至少包含一个线程。在多核处理器越来越普及的今天,多线程编程也用得越来越广泛。多线程的优势有:
容易利用多核优势;
比单线程反应更敏捷,比多进程资源共享更容易。
多线程编程在许多领域是不可或缺的。但是,多线程并行,非常容易引发数据竞争,而且还非常不容易被发现和 debug。下面,我们用 C++语言来演示一下什么是数据竞争(这里是故意写的有 bug 的版本):
rust
#include <iostream>
#include <stdlib.h>
#include <thread>
#include <string>
#define COUNT 1000000
volatile int g_num = 0;
void thread1()
{
for (int i=0; i<COUNT; i++){
g_num++;
}
}
void thread2()
{
for (int i=0; i<COUNT; i++){
g_num--;
}
}
int main(int argc, char* argv[])
{
std::thread t1(thread1);
std::thread t2(thread2);
t1.join();
t2.join();
std::cout << "final value:" << g_num << std::endl;
return 0;
}
我们可以使用g++ -pthread -std=c++11 temp.cpp
命令编译这段代码。
在这段代码中,我们创建了两个线程。一个线程去修改全局变量global
,循环1000000
次加1
。另外一个线程也去修改全局变量global
,循环1000000
次减1
。如果没有数据竞争的话,这两个线程执行完毕后,数据最终一定是回到初始值0
。然而,我们尝试运行后发现,每次执行的结果都不是0
,而且每次的结果都不一样。
为什么会发生这样的现象呢?这是因为,为普通变量加1
减1
这样的操作并非“原子”操作。我们简化一下这个过程,可以将它分为三个步骤:读数据、执行计算、写数据。理想情况下,我们期望的执行流程应该是下面这样的:
Thread 1 | Thread 2 | 读/写 | 变量值 |
---|---|---|---|
0 | |||
读数据 | ← | 0 | |
加 1 | 0 | ||
写数据 | → | 1 | |
读数据 | ← | 1 | |
减 1 | 1 | ||
写数据 | → | 0 |
然而,线程的调度是不受我们控制的,即便线程 1 和线程 2 内部的执行流程不变,只要调度时机发生了变化,结果也会不同。比如实际的执行过程中,有可能是这样的情况:
Thread 1 | Thread 2 | 读/写 | 变量值 |
---|---|---|---|
0 | |||
读数据 | ← | 0 | |
读数据 | ← | 0 | |
加 1 | 0 | ||
减 1 | 0 | ||
写数据 | → | 1 | |
写数据 | → | -1 |
根据调度情况的不同,最终的结果也会有所差异,所以我们可以看到这个程序的执行结果不是0
,而且循环次数越多,发生数据竞争的机会也越大。
在传统的系统级编程语言中,写多线程代码很容易出错。而 Rust 的一大特点就是消除了数据竞争,保证了线程安全。下面介绍 Rust 中的线程。