线程库为程序员提供创建和管理线程的 API。实现线程库的主要方法有两种:
目前使用的三种主要线程库是:POSIX Pthreads、Windows 和 Java:
对于 POSIX 和 Windows 线程,全局声明(即在函数之外声明的)的任何数据,可为同一进程的所有线程共享。因为 Java 没有全局数据的概念,所以线程对共享数据的访问必须加以显式安排。属于某个函数的本地数据通常位于堆栈。由于每个线程都有自己的堆栈,每个线程都有自己的本地数据。
在本节的余下部分中,我们将通过这三种线程库介绍简单的线程创建。作为一个说明例子,我们设计了一个多线程程序,以便执行非负整数的求和,这里采用了著名的求和函数:
例如,如果 N 为 5,这个函数表示对从 0 到 5 的整数进行求和,结果为 15。这三个程序根据从命令上输入的求和的上界来运行。因此,如果用户输入 8,那么输出的将是从 0 到 8 的整数值的总和。
我们在继续线程创建的例子之前,介绍多线程创建的两个常用策略:异步线程和同步线程。
对异步线程,一旦父线程创建了一个子线程后,父线程就恢复自身的执行,这样父线程与子线程会并发执行。每个线程的运行独立于其他线程,父线程无需知道子线程何时终止。由于线程是独立的,所以线程之间通常很少有数据共享。如图 1 所示的多线程服务器使用的策略就是异步线程。
如果父线程创建一个或多个子线程后,那么在恢复执行之前应等待所有子线程的终止(分叉-连接策略),这就出现了同步线程。这里,由父线程创建的线程并发执行工作,但是父线程在这个工作完成之前无法继续。一旦每个线程完成了它的工作,它就会终止,并与父线程连接。只有在所有子线程都连接之后,父线程才恢复执行。
通常,同步线程涉及线程之间的大量数据的共享。例如,父线程可以组合由子线程计算的结果。所有下面的例子都使用同步线程。
Pthreads 是 POSIX 标准(IEEE 1003.1c)定义的线程创建与同步 API。这是线程行为的规范,而不是实现。操作系统设计人员可以根据意愿采取任何形式的实现。
许多操作系统都实现了这个线程规范,大多数为 UNIX 类型的系统,如 Linux、Mac OS X 和 Solaris。虽然 Windows 本身并不支持 Pthreads,但是有些第三方为 Windows 提供了 Pthreads 的实现。
#include <pthread.h>
#include <stdio.h>
int sum; /* this data is shared by the thread(s) */
void *runner(void *param); /* threads call this function */
int main(int argc, char *argv[])
{
pthread_t tid; /* the thread identifier */
pthread_attr_t attr; /* set of thread attributes */
if (argc != 2) {
fprintf(stderr,"usage: a.out <integer value>\n");
return -1;
}
if (atoi(argv[1]) < 0) {
fprintf (stderr, "%d must be >= 0\n", atoi (argv [1])); return -1;
}
/* get the default attributes */
pthread_attr_init (&attr);
/* create the thread */
pthread-create (&t id,&attr,runner,argv [1]);
/* wait for the thread to exit */
pthread_join(tid, NULL);
printf ("sum = %d\n",sum);
}
/* The thread will begin control in this function */
void *runner(void *param)
{
int i,upper = atoi(param);
sum = 0;
for (i = 1; i <= upper; i++)
sum += i;
pthread_exit(0);
}
如上所示的 C 程序演示了基本的 Pthreads API,它构造一个多线程程序,用于通过一个独立线程来计算非负整数的累加和。对于 Pthreads 程序,独立线程是通过特定函数执行的。此程序中这个特定函数是 runner() 函数。当程序开始时,单个控制线程从 main() 函数开始。在初始化之后,main() 函数创建了第二个线程,它从 runner() 函数开始控制。两个线程共享全局数据 sum。
下面,我们深入分析这个程序。所有的 Pthreads 程序都要包括头文件 pthread.h。语句 pthread_t tid 声明了创建线程的标识符。每个线程都有一组属性,包括堆栈大小和调度信息。声明 pthread_attr_t attr 表示线程属性;通过调用函数 pthread_attr_init(&attr) 可以设置这些属性。由于没有明确设置任何属性,所以使用缺省属性。通过调用函数 pthread_create() 可以创建一 个单独线程。除了传递线程标识符和线程属性外,还要传递函数名称,这里为 runner(),以 便新线程可以开始执行这个函数。最后,还要传递由命令行参数 argv[1] 提供的整型参数。
此时,本程序已有两个线程:初始(父)线程,即 main();执行累加和(子)线程,即 runner()。这个程序采用上面所述的分叉-连接策略:在创建了累加和线程之后,父线程通过调用 pthread_join() 函数等待 runner() 线程的完成。累加和线程在调用了函数 pthread_exit() 之后就会终止。一旦累加和线程返回,父线程就输出累加和的值。
这个示例程序只创建一个线程。随着越来越多的多核系统的出现,编写包含多个线程的程序也变得越来越普遍。通过 pthread_join() 等待多个线程的一个简单方法:将这个操作包含在一个简单的 for 循环中。
例如,通过如下 Pthreads 代码,你能连接 10 个线程:
#define NUM_THREADS 10
/* an array of threads to be joined upon */
pthread_t workers [NUM_THREADS];
for (int i = 0; i < NUM_THREADS; i++)
pthread_join(workers[i],NULL);
采用 Windows 线程库创建线程的技术,在许多方面都类似于 Pthreads 技术。如下所示的 C 程序说明了 Windows 线程 API:
#include <windows.h>
#include <stdio.h>
DWORD Sum; /* data is shared by the thread(s) */
/* the thread runs in this separate function */
DWORD WINAPI Summation(LPVOID Param)
{
DWORD Upper = *(DWORD*)Param;
for (DWORD i = 0; i <= Upper; i++)
Sum += i;
return 0;
}
int main(int argc, char *axgv[])
{
DWORD ThreadId;
HANDLE ThreadHandle;
int Param;
if (argc != 2) {
fprintf(stderr,"An integer parameter is required\n");
return -1;
}
Pax am = atoi(argv[1]);
if (Param < 0) {
fprintf(stderr,"An integer >= 0 is required\n");
return -1;
}
/* create the thread */
ThreadHandle = CreateThread(
NULL, /* default security attributes */
0, /* default stack size */
Summation, /氺 thread function */
&Param, /* parameter to thread function */
0, /* default creation flags */
&ThreadId); /* returns the thread identifier */
if (ThreadHandle != NULL) {
/* now wait for the thread to finish */
WaitForSingleObject(ThreadHandle,INFINITE);
/* close the thread handle */
CloseHandle(ThreadHandle);
printf (" sum = %d\n",Sum);
}
}
注意,在使用 Windows API 时,我们应包括头文件 windows.h。
前面所讲的 Pthreads 例子中,各个线程共享的数据(这里为 Sum)需要声明为全局 变量(数据类型 DWORD 是一个无符号的 32 位整型);还定义了一个函数 Summation() 以便在单独线程中执行,该函数还要传递一个 void 指针,Windows 将其定义为 LPVOID。执行这个函数的线程将全局数据 Sum 赋值为:从 0 到 Param 的累加和的值,这里 Param 为传递到函数 Summation() 的参数。
线程创建的 Windows API 为函数 CreateThread();与 Pthreads 一样,还要传给这个函数一组线程属性。这些属性包括安全信息、堆栈大小、用于表示线程是否处于暂停状态的标志。这个程序采用这些属性的缺省值(在缺省情况下,新创建线程的状态不是暂停的,而是由 CPU 调度程序来决定它是否可以运行)。
在创建累加和线程后,父线程在输出累加和之前应等待累加和线程的完成,因为该值是累加和线程赋值的。回想一下 Pthreads 程序,通过 pthread_join() 语句,父线程等待累加和线程。执行对应功能的 Windows API 为函数 WaitForSingleObject(),它导致创建者线程阻塞,直到累加和线程退出。
在需要等待多个线程完成的情况下,可以采用函数 WaitForMultipleObjects()。这个函数需要 4 个参数:
例如,如果 THandles 为线程 HANDLE 对象的数组,大小为 N,那么父线程可以通过如下语句等待所有子线程都已完成:
WaitForMultipleObjects(N, THandles, TRUE, INFINITE);
Java 程序的线程是程序执行的基本模型,Java 语言和 API 为线程创建和管理提供了丰富的功能。所有 Java 程序至少包含一个控制线程,即使只有方法 main() 的一个简单 java 程序也是在 JVM 中作为一个线程运行的。
Java 线程可运行于提供 JVM 的任何系统,如 Windows、Linux 和 Mac OS X 等,也可用于 Android 应用程序。
在 Java 程序中,有两种技术来创建线程:
public interface Runnable {
public abstract void run();
}
当一个类实现接口 Runnable 时,它必须定义一个方法 run()。方法 run( ) 的实现代码就是作为一个单独线程来运行的。
class Sum {
private int sum;
public int getSum() {
return sum;
}
public void setSum(int sum) {
this.sum = sum;
}
}
class Summation implements Runnable {
private int upper;
private Sum sumValue;
public Summation(int upper, Sum sumValue)
{
this.upper = upper;
this.sumValue = sumValue;
}
public void run() {
int sum = 0;
for (int i = 0; i <= upper; i++)
sum += i;
sumValue.setSum(sum);
}
}
public class Driver {
public static void main(String[] args)
{
if (args.length > 0)
{
if (Integer.parseInt(args[0] ) < 0)
System.err.println(args[0] + " must be >= 0.");
else {
Sum sumObject = new Sum();
int upper = Integer.parseInt(args[0]);
Thread thrd = new Thread(new Summation(upper, sumObject));
thrd.start();
try {
thrd.join();
System.out.println("The sum of "+upper+" is "+sumObject.getSum());
} catch (InterruptedException ie) { }
}
}
else
System.err.println("Usage: Summation <integer value>");
}
}
}
以上代码为 Java 多线程程序,用于计算非负整数的累加和。类 Summation 实现接口 Runnable。 线程创建是通过创建类 Thread 的一个对象实例并且传给构造函数一个 Runnable对象。
创建 Thread 对象不会创建一个新的线程,实际上,方法 start() 创建新的线程。调用新对象的方法 start() 做两件事:
当累加和程序运行时,JVM 创建两个线程。第一个是父线程,它从函数 main() 开始执行。第二个线程在调用 Thread 对象的方法 start() 时加以创建。这个子线程从类 Summation 的方法 run() 开始执行。在输出总和值之后,该线程在退出方法 run() 时终止。
对于 Windows 和 Pthreads,线程间的数据共享容易,因为共享数据可简单声明成全局数据。作为一个纯面向对象语言,Java 没有这样的全局数据概念。在 Java 程序中,如果两个或更多的线程需要共享数据,那么可以通过向相应线程传递共享对象引用来实现。
在前面的 Java 程序中,线程 main 和累加和线程共享类 Sum 的对象实例。对这个共享对象的访问,采用方法 getSum() 和 setSum()。(你可能好奇为什么不使用 java.lang.Integer 对象,而是设计一个新的 sum 类。这是因为 java.lang.Integer 类是不可变的,即一旦赋值,就不可改变。)
回想一下 Pthreads 和 Windows 库的父线程,它们在继续之前,分别使用 pthread_join() 或 WaitForSingleObject() 等待累加和线程的结束。Java 的方法 join() 提供了类似的功能。(注意,join() 可能会拋出 InterruptedException,但是这里就不细说了。)如果父线程需要等待多个线程的完成,那么可将方法 join() 放到一个 for 循环,类似于前面所示的 Pthreads 程序。