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

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

目前使用的三种主要线程库是:POSIX PthreadsWindows 和 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 个参数:
  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 程序。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


起步 处理器架构,参考 x86是指intel的开发的一种32位指令集 intel和amd早期的cpu都支持这种指令集 AMD比Intel率先制造出了商用的兼容x86的CPU,AMD称之为AMD64 Intel选择了设计一种不兼容x86的全新64为指令集,称之为IA-64,后来支持AMD64的指令集,
pscp pscp -P 22 C:\work\test.txt root@192.168.1.5:/home/data pscp -P 22 root@192.168.1.5:/home/data/test.txt C://work// 检索 find / -name default.config
文件处理 ls -a # 显示所有文件 ls -l # 显示详细信息 ls -d # 显示路径 mkdir /目录名称 # 创建目录 cd /目录名称 # 切换目录 pwd # 显示当前路径 rmdir /目录名称 # 删除目录 cp -rp [目录名称] [目标目录] # 复制目录到目标目录 cp
准备一台电脑(我就用联想拯救者r7000演示) 参考博客制作启动盘 插上U盘,启动电脑,一直按F2 进入如下页面后,将U盘设置为第一启动项,点击exit,保存并退出 之后进入如下页面,选择第三项 进入如下页面,选择第四项 进入如下页面,选择第一项,选中后,先不要点Enter 按e键,将inst.st
认识 Linux系统是参考了UNIX系统作为模板开发的,但没有使用UNIX的代码;是UNIX的一种,但不是衍生版 在Linux内核的基础上开发是发行版 分区 逻辑分区永远从5开始 步骤 挂载:可理解为分配盘符,挂载点即是盘符名;不同之处:Linux中是以空目录名称作为盘符 Hda 第一块硬盘 Hda
文件处理命令 以 . 开头的文件是隐藏文件 以 - 开头表示这是一个文件 以 d 开头表示是一个目录 以 l 开头表示是一个软链接 第一个root是所有者,第二个root是所属组 ls -h 以文件默认大小后缀 显示 ls -i 查看i节点(唯一标识) 所有者:只能有一个,可变更 所属组:只能有一个
参考 01 02 03 前提环境 本地安装VirtualBox,并安装CentOS8,配置网络后,window系统上putty能连接到CentOS8服务器 配置步骤 右键服务器复制 启动复制后的服务器,查看ip和hostname发现和原来的服务器一样,需要修改 hostname # 查看主机名 vi
文件搜索命令 星号匹配任意字符,问号匹配任意单个字符 -iname 根据文件名查找且不区分大小写 -ok 命名会有一个询问的步骤 如果没有找到指定文件,可输入命令:updatedb 更新文件资料库;除tmp目录不在文件资料库收录范围之内 locate -i 文件名 # 检索时不区分大小写 which
安装环境 安装最新版的Virtual Box,点击安装 下载centos8镜像 创建虚拟机,可参考 选择下载到本地的镜像 设置启动顺序 点击启动 启动过程中报错:“FATAL:No bootable medium found!” 1.没有选择iso镜像 2.光驱没有排在第一位置 3.镜像只能选择x8
Linux严格区分大小写 所有内容文件形式保存,包括硬件 Linux不靠扩展名区分文件类型 挂载:将设备文件名和挂载点(盘符)连接的过程 Linux各个目录的作用 bin表示二进制 服务器注意事项 远程服务器不允许关机,只能重启 重启时应该关闭服务 不要在服务器访问高峰运行高负载命令 远程配置防火墙
IDE连接Linux,上传下载文件 参考1 参考2 连接Linux 上传下载文件 本地项目打包后上传 查看是否上传成功,右键下载 补充 后端项目开发完成后,需clean掉临时文件target文件夹,且只推送修改过的文件 前端项目开发的过程中,需要在每个子组件中使用scoped,确保每个子组件中的编码
起步 LTS与普通版本的区别 LTS版本的发布周期更长,更加稳定 安装jdk sudo mkdir /usr/lib/jvm # 在Ubuntu中创建目录 pscp D:\安装包\linux源码包\jdk-8u291-linux-x64.tar.gz chnq@192.168.0.102:/tmp
前言 最近在b站上看了兄弟连老师的Linux教程,非常适合入门:https://www.bilibili.com/video/BV1mW411i7Qf 看完后就自己来试着玩下,正好手上有台空闲的电脑就尝试不使用虚拟机的方式安装Linux系统 安装步骤 制作启动盘 下载ISO镜像,我这里下载的是Cen
新建虚拟电脑 设置内存和处理器 设置硬盘大小 完成 设置 查看光驱 设置启动顺序 点击启动 选择第1项 进入图形安装界面 选择安装位置,开始安装 设置root密码 重启 登录 查看本地文件夹 配置网络,点击设置 查看宿主机ip C:\Users\ychen λ ipconfig 无线局域网适配器 W
源码包安装需手动下载后安装 二进制包则在package目录下 rpm命令管理rpm包 若某个rpm包依赖于某个模块,需要到网站www.rpmfind.net查询该模块依赖的包,安装这个包后自动安装模块,之后就能安装rpm包了 安装升级时使用包全名 查询卸载时使用包名 虚拟机中的Linux系统安装rp
首先进入命令模式,再输入以下命令 命令模式用于输入命令 插入模式可对文件编写操作 编辑模式下的命令是在冒号后输入 :12, 15d # 删除指定范围的行,这里是删除12到15行 :n1,n2s/old/new/g ## 表示从n1行到n2行,old表示旧的字符串 vim使用小技巧:自定义快捷键,如快
使用源码包安装,需要自己指定安装位置,通常是 /usr/local/软件名/ linux中要想启动执行文件,应使用绝对路径 /绝对路径/rpm包名 start ## 执行方式一 service rpm包名 start ## 执行方式二 使用源码包安装后,由于自定义安装路径,就不能使用service命
网络命令 在收邮件的用户中,输入 mail 可查看邮件信息,输入序列号查看详细信息 在mail命令下,输入h 查看所有邮件的列表 输入:d 序列号 # 删除邮件 last # 统计所有用户登录或重启时间,用于日志查询 lastlog # 显示包括未登录用户的登录时间 lastlog -u 用户id
若要使用yum管理,必须能连接网络,首先配置网络IP 进入yum源文件中启动容器 使用yum源头安装rpm包不需要进入package路径,同时也不需要使用包全名,会有yum自动管理 安装软件组
简介 client即是本机安装的docker,相当于git Docker_host相当于centos系统 registry则是docker仓库,相当于GitHub 镜像用于创建docker容器,一个镜像可以创建多个docker容器 容器是由镜像创建的运行实例,(镜像相当于类,容器相当于类创建的对象)