首页 > 编程笔记 > 操作系统笔记

什么是线程库,线程库类别及其应用

线程库为程序员提供创建和管理线程的 API。实现线程库的主要方法有两种:
  1. 在用户空间中提供一个没有内核支持的库。这种库的所有代码和数据结构都位于用户空间。这意味着,调用库内的一个函数只是导致了用户空间内的一个本地函数的调用,而不是系统调用。
  2. 实现由操作系统直接支持的内核级的一个库。对于这种情况,库内的代码和数据结构位于内核空间。调用库中的一个API函数通常会导致对内核的系统调用。

目前使用的三种主要线程库是:POSIX PthreadsWindows 和 Java
对于 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 个参数:
  1. 等待对象的数量;
  2. 对象数组的指针;
  3. 是否等待所有对象信号的标志;
  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 程序中,有两种技术来创建线程:
  1. 是创建一个新的类,它从类 Thread 派生并重载函数 run();
  2. 更常使用的方法是定义一个实现接口 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() 做两件事:
  1. JVM 中,为新线程分配内存并初始化。
  2. 调用方法 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 程序。

所有教程

优秀文章