- 在用户空间中提供一个没有内核支持的库。这种库的所有代码和数据结构都位于用户空间。这意味着,调用库内的一个函数只是导致了用户空间内的一个本地函数的调用,而不是系统调用。
- 实现由操作系统直接支持的内核级的一个库。对于这种情况,库内的代码和数据结构位于内核空间。调用库中的一个API函数通常会导致对内核的系统调用。
目前使用的三种主要线程库是:POSIX Pthreads、Windows 和 Java:
- Pthreads 作为 POSIX 标准的扩展,可以提供用户级或内核级的库;
- Windows 线程库是用于 Windows 操作系统的内核级线程库;
- Java 线程 API 允许线程在 Java 程序中直接创建和管理。然而,由于大多数 JVM 实例运行在宿主操作系统之上,Java 线程 API 通常采用宿主系统的线程库来实现。这意味着在 Windows 系统上,Java 线程通常采用 Windows API 来实现,而在 UNIX 和 Linux 系统中采用 Pthreads 来实现。
对于 POSIX 和 Windows 线程,全局声明(即在函数之外声明的)的任何数据,可为同一进程的所有线程共享。因为 Java 没有全局数据的概念,所以线程对共享数据的访问必须加以显式安排。属于某个函数的本地数据通常位于堆栈。由于每个线程都有自己的堆栈,每个线程都有自己的本地数据。
在本节的余下部分中,我们将通过这三种线程库介绍简单的线程创建。作为一个说明例子,我们设计了一个多线程程序,以便执行非负整数的求和,这里采用了著名的求和函数:
例如,如果 N 为 5,这个函数表示对从 0 到 5 的整数进行求和,结果为 15。这三个程序根据从命令上输入的求和的上界来运行。因此,如果用户输入 8,那么输出的将是从 0 到 8 的整数值的总和。
我们在继续线程创建的例子之前,介绍多线程创建的两个常用策略:异步线程和同步线程。
对异步线程,一旦父线程创建了一个子线程后,父线程就恢复自身的执行,这样父线程与子线程会并发执行。每个线程的运行独立于其他线程,父线程无需知道子线程何时终止。由于线程是独立的,所以线程之间通常很少有数据共享。如图 1 所示的多线程服务器使用的策略就是异步线程。
图 1 多线程的服务器架构
如果父线程创建一个或多个子线程后,那么在恢复执行之前应等待所有子线程的终止(分叉-连接策略),这就出现了同步线程。这里,由父线程创建的线程并发执行工作,但是父线程在这个工作完成之前无法继续。一旦每个线程完成了它的工作,它就会终止,并与父线程连接。只有在所有子线程都连接之后,父线程才恢复执行。
通常,同步线程涉及线程之间的大量数据的共享。例如,父线程可以组合由子线程计算的结果。所有下面的例子都使用同步线程。
Pthreads
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 线程
采用 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 个参数:
- 等待对象的数量;
- 对象数组的指针;
- 是否等待所有对象信号的标志;
- 超时时长(或INFINITE(无穷));
例如,如果 THandles 为线程 HANDLE 对象的数组,大小为 N,那么父线程可以通过如下语句等待所有子线程都已完成:
WaitForMultipleObjects(N, THandles, TRUE, INFINITE);
Java 线程
Java 程序的线程是程序执行的基本模型,Java 语言和 API 为线程创建和管理提供了丰富的功能。所有 Java 程序至少包含一个控制线程,即使只有方法 main() 的一个简单 java 程序也是在 JVM 中作为一个线程运行的。
Java 线程可运行于提供 JVM 的任何系统,如 Windows、Linux 和 Mac OS X 等,也可用于 Android 应用程序。
在 Java 程序中,有两种技术来创建线程:
- 是创建一个新的类,它从类 Thread 派生并重载函数 run();
- 更常使用的方法是定义一个实现接口 Runnable 的类。Runnable 接口定义如下:
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 中,为新线程分配内存并初始化。
- 调用方法 run(),以便能在 JVM 中运行(再次提醒:我们从不直接调用方法 run(),而是调用方法 start(),然后它会调用方法 run())。
当累加和程序运行时,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 程序。
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/21197.html