Skip to content

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,而且每次的结果都不一样。

为什么会发生这样的现象呢?这是因为,为普通变量加11 这样的操作并非“原子”操作。我们简化一下这个过程,可以将它分为三个步骤:读数据、执行计算、写数据。理想情况下,我们期望的执行流程应该是下面这样的:

Thread 1Thread 2读/写变量值
0
读数据0
加 10
写数据1
读数据1
减 11
写数据0

然而,线程的调度是不受我们控制的,即便线程 1 和线程 2 内部的执行流程不变,只要调度时机发生了变化,结果也会不同。比如实际的执行过程中,有可能是这样的情况:

Thread 1Thread 2读/写变量值
0
读数据0
读数据0
加 10
减 10
写数据1
写数据-1

根据调度情况的不同,最终的结果也会有所差异,所以我们可以看到这个程序的执行结果不是0,而且循环次数越多,发生数据竞争的机会也越大。

在传统的系统级编程语言中,写多线程代码很容易出错。而 Rust 的一大特点就是消除了数据竞争,保证了线程安全。下面介绍 Rust 中的线程。

Released under the MIT License