隐式线程和基于语言的线程
隐式线程
解决这些难题并更好地支持多线程应用程序设计的一种方法是将线程的创建和管理从应用程序开发人员转移到编译器和运行时库。这种被称为隐式线程的技术是当今的一种流行趋势。
**隐式线程**主要指使用库或其他语言支持来隐藏线程的管理。在 C 语言环境中,最常见的隐式线程库是 OpenMP。
**OpenMP** 是一组编译器指令以及用于 C、C++ 或 FORTRAN 语言编写的程序的 API,它为共享内存环境中的并行编程提供支持。OpenMP 将并行区域识别为可以并行运行的代码块。应用程序开发人员在其代码的并行区域插入编译器指令,这些指令指示 OpenMP 运行时库并行执行该区域。下面的 C 程序演示了在包含 printf() 语句的并行区域上方的一个编译器指令
示例
#include <omp.h> #include <stdio.h> int main(int argc, char *argv[]){ /* sequential code */ #pragma omp parallel{ printf("I am a parallel region."); } /* sequential code */ return 0; }
输出
I am a parallel region.
当 OpenMP 遇到指令时
#pragma omp parallel
它创建与系统中处理核心数量相同的线程。因此,对于双核系统,将创建两个线程;对于四核系统,将创建四个线程;以此类推。然后,所有线程同时执行并行区域。当每个线程退出并行区域时,它将被终止。OpenMP 提供了几个其他指令来并行运行代码区域,包括并行化循环。
除了提供并行化指令外,OpenMP 还允许开发人员在多个并行级别之间进行选择。例如,他们可以手动设置线程数。它还允许开发人员识别数据是在线程之间共享还是对线程私有。OpenMP 可用于 Linux、Windows 和 Mac OS X 系统的多个开源和商业编译器。
Grand Central Dispatch (GCD)
Grand Central Dispatch (GCD)——苹果 Mac OS X 和 iOS 操作系统的一项技术——是 C 语言扩展、API 和运行时库的组合,允许应用程序开发人员指定代码的哪些部分并行运行。与 OpenMP 一样,GCD 也管理大部分线程细节。它识别 C 和 C++ 语言的扩展,称为块。块只是一个自包含的工作单元。它由插入在一对花括号 { } 前面的脱字符 ^ 指定。下面显示了一个简单的块示例:
{ ˆprintf("This is a block"); }
它通过将块放入调度队列中来安排块的运行时执行。当 GCD 从队列中移除一个块时,它会将块分配给它管理的线程池中的可用线程。它识别两种类型的调度队列:串行和并发。放入串行队列中的块按照 FIFO 顺序移除。一旦块从队列中移除,它必须完成执行才能移除另一个块。每个进程都有自己的串行队列(称为主队列)。开发者可以创建附加的串行队列,这些队列特定于进程。串行队列对于确保多个任务的顺序执行非常有用。放入并发队列中的块也按 FIFO 顺序移除,但可以一次移除多个块,从而允许多个块并行执行。有三个系统范围的并发调度队列,它们按优先级区分:低、默认和高。优先级表示块相对重要性的估计。简单来说,较高优先级的块应放在高优先级调度队列中。以下代码段演示了如何获取默认优先级的并发队列,以及使用 dispatch_async() 函数将块提交到队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch async(queue, ˆ{ printf("This is a block."); });
在内部,GCD 的线程池由 POSIX 线程组成。GCD 积极管理池,允许线程数量根据应用程序需求和系统容量而增长和缩小。
线程作为对象
在其他语言中,传统的面向对象语言通过将线程作为对象来提供显式多线程支持。在这些类型的语言中,类被编写为扩展线程类或实现相应的接口。这种风格类似于 Pthread 方法,因为代码是用显式线程管理编写的。但是,类中信息的封装和额外的同步选项会改变任务。
Java 线程
Java 提供了一个 Thread 类和一个 Runnable 接口,可以用来实现。每个都需要实现一个公共 void run() 方法,该方法定义线程的入口点。一旦对象的实例被分配,线程就会通过调用该实例上的 start() 方法来启动。与 Pthreads 一样,启动线程是异步的,即执行的时间安排是不确定的。
Python 线程
Python 还提供了两种多线程机制。一种方法类似于 Pthread 样式,其中函数名传递给库方法 thread.start_new_thread()。这种方法非常基础,并且一旦线程启动就缺乏加入或终止线程的灵活性。一种更灵活的技术是使用 threading 模块来定义扩展 threading.Thread 的类。与 Java 方法类似,该类应该有一个 run() 方法,该方法提供线程的入口点。一旦从该类实例化了一个对象,它就可以被显式启动,并在稍后加入。
并发作为语言设计
较新的编程语言通过将并发执行的假设直接构建到语言设计本身来避免竞争条件。例如,Go 将简单的隐式线程机制(goroutine)与通道(一种定义明确的消息传递通信方式)相结合。Rust 采用与 Pthreads 相同的明确线程方法。但是,Rust 具有非常强大的内存保护,软件工程师无需额外的工作。
Goroutine
Go 语言包含一个简单的隐式线程机制:在调用之前放置关键字 go。新线程会传递到消息传递通道的关联。然后,主线程调用 success := <-messages,这会在通道上执行阻塞扫描。一旦用户输入正确的猜测 7,键盘监听线程就会写入通道,从而允许主线程继续。
通道和 goroutine 是 Go 语言的核心组件,该语言是在假设大多数程序将是多线程的情况下设计的。这种设计选择简化了事件模型,允许语言本身承担管理线程和编程的责任。
Rust 并发
近年来创建的另一种语言是 Rust,其并发性是其核心设计功能。以下示例演示了如何使用 thread::spawn() 创建一个新线程,该线程稍后可以通过在其上调用 join() 来加入。thread::spawn() 的参数从 || 开始,称为闭包,可以认为是匿名函数。也就是说,这里的子线程将打印 a 的值。
示例
use std::thread; fn main() { /* Initialize a mutable variable a to 7 */ let mut a = 7; /* Spawn a new thread */ let child_thread = thread::spawn(move || { /* Make the thread sleep for one second, then print a */ a -= 1; println!("a = {}", a) }); /* Change a in the main thread and print it */ a += 1; println!("a = {}", a); /* Join the thread and print a again */ child_thread.join(); }
但是,这段代码中有一个微妙的点,它是 Rust 设计的核心。在新线程(执行闭包中的代码)中,a 变量与这段代码其他部分中的 a 不同。它强制执行非常严格的内存模型(称为“所有权”),这可以防止多个线程访问相同的内存。在此示例中,move 关键字表示生成的线程将接收 a 的单独副本以供其自身使用。无论两个线程(主线程和子线程)的调度如何,它们都不能干扰彼此对 a 的修改,因为它们是不同的副本。这两个线程不可能共享对相同内存的访问。