解决难题并更好地支持多线程应用程序设计的一种方法是将线程的创建和管理从应用程序开发人员转移到编译器和运行时库。这称为隐式线程,是当今的流行趋势。
隐式线程主要是使用库或其他语言支持来隐藏线程管理。在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("我是一个平行区域。"); } /* sequential code */ return 0; }
输出结果
我是一个平行区域。
当OpenMP遇到指令时
#pragma omp parallel
它创建尽可能多的线程来处理系统中的核心。因此,对于双核系统,创建两个线程,对于四核系统,创建四个线程;等等。然后所有线程同时执行并行区域。当每个线程退出并行区域时,它将终止。OpenMP提供了一些其他指令来并行运行代码区域,包括并行化循环。
除了提供并行化指令外,OpenMP还允许开发人员在多个并行度级别中进行选择。例如,他们可以手动设置线程数。它还允许开发人员确定数据是在线程之间共享还是对线程私有。OpenMP在Linux,Windows和Mac OS X系统的几种开源和商业编译器上可用。
Grand Central Dispatch(GCD)是Apple Mac OS X和iOS操作系统的一项技术,是对C语言扩展,API和运行时库的组合,允许应用程序开发人员发现可在其中运行的代码段平行。像OpenMP一样,GCD也管理线程的大多数细节。它标识对称为块的C和C ++语言的扩展。块只是一个独立的工作单元。它由插入在一对大括号{}前面的插入符号specified指定。下面是一个简单的块示例-
{ ˆprintf("This is a block"); }
它通过将块放在调度队列中来调度运行时执行的块。当GCD从队列中删除一个块时,它会将其分配给它管理的线程池中的可用线程。它标识两种类型的调度队列:串行和并发。放置在串行队列中的块按FIFO顺序删除。从队列中删除一个块后,它必须先完成执行,然后再删除另一个块。每个进程都有自己的串行队列(称为主队列)。开发人员可以创建特定进程本地的其他串行队列。串行队列对于确保顺序执行多个任务很有用。并发队列上放置的块也按FIFO顺序删除,但是一次可以删除几个块,从而允许多个块并行执行。系统范围内共有三个并发调度队列,它们根据优先级进行区分:低,默认和高。优先级代表对块的相对重要性的估计。非常简单,应将具有较高优先级的块放在高优先级分配队列上。以下代码段说明了如何获取默认优先级的并发队列,并使用分派向该队列提交一个块async()
功能:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch async(queue, ˆ{ printf("这是一个障碍。"); });
在内部,GCD的线程池由POSIX线程组成。GCD主动管理池,允许线程数量根据应用程序需求和系统容量来增加和减少。
在替代语言中,古老的面向对象语言以线程作为对象来提供显式多线程支持。在这些语言形式中,为扩展线程类或实现相应的接口而编写的类区域。这种样式类似于Pthread方法,因为代码是使用显式线程管理编写的。但是,将信息封装在类和额外的同步选项中会修改任务。
Java提供了可以使用的Thread类别和Runnable接口。每一种都需要实现一种公共无效run()
技术,该技术定义了线程的进入目的。一旦分配了对象的实例,就可以通过调用该对象start()
上的技术来启动线程。与Pthreads一样,线程开始是异步的,执行的时间安排是不确定的。
Python另外提供了两种用于多线程的机制。无论将函数名传递到库方法thread.start_new_thread(),一种方法都可以与Pthread样式相提并论。这种方法非常多,并且缺乏在线程启动时加入或终止线程的灵活性。另一种灵活的技术是使用线程模块来概述扩展线程的类。线。几乎与Java方法一样,该类别应具有run()
提供线程进入目的的方法。一旦从该类别实例化了一个对象,就可以显式启动它并在以后将其加入。
较新的编程语言通过将并发执行的假设直接建立到语言样式本身中来避免了竞争状况。例如,Go将琐碎的隐式线程技术(goroutines)与通道(一种定义明确的消息传递通信风格)结合在一起。Rust采用与Pthreads相同的确定线程方法。但是,Rust具有非常强大的内存保护,不需要软件工程师的额外工作。
Go语言包括一个用于隐式线程的简单机制:将关键字go放在调用之前。新线程将关联传递到消息传递通道。然后,大多数线程调用success:= <-messages,在通道上执行干扰扫描。一旦用户输入了正确的7位猜测值,键盘审核员线程便会写入通道,从而允许最多线程前进。
通道和goroutines是Go语言的核心组件,它是在几乎所有程序都是多线程的信念下设计的。这种风格的替代方案简化了事件模型,使语言本身可以负责管理线程和编程。
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的修改,因为它们是不同的副本。两个线程不可能共享对同一内存的访问。