POSIX Thread

概述

在传统的unix模型中,当一个进程须要另外一个实体来完成某项任务时,它就fork一个子进程出来处理,好比在一个网络服务器程序中,父进程accept一个链接,而后fork一个子进程,由该子进程处理与链接对端的客户端之间的通讯。html

尽管这种范式好久以来一直用得很好,可是fork调用却存在一些问题:java

  • fork是昂贵的。fork要把父进程的内存映像复制到子进程中,并在子进程中复制全部描述符。当前的实现使用写时复制(COW)的技术,来避免子进程切实须要本身的副本以前把父进程的数据复制到子进程。然而即便有这样的优化措施,fork仍然是昂贵的。
  • fork返回以后,复制进程须要经过进程间通讯(IPC)来传递信息。从父进程传递信息到子进程至关容易,由于子进程将从父进程的数据空间和描述符的副本开始运行,然而从子进程往父进程传递消息却比较费力。

线程有助于解决这两个问题。线程有时称为轻量进程(lightweight process),由于线程比进程更轻量。也就是说,线程的建立可能比进程的建立快10~100倍。同一进程内的全部线程共享相同的全局内存。这使得线程之间易于共享信息,然而伴随这种简易性而来的倒是同步(synchronization)问题。linux

同一进程内的全部线程除了共享全局变量外还共享:编程

  • 进程指令
  • 大多数数据
  • 打开的文件(即描述符)
  • 信号处理函数和信号处置
  • 当前工做目录
  • 用户ID和组ID

不过每一个线程有各自的:服务器

  • 线程ID
  • 寄存器集合,包括程序计数器和栈指针
  • errno
  • 信号掩码
  • 优先级

本文中讲述的是POSIX线程,也成为Pthread。POSIX线程做为POSIX.1c标准的一部分在1995年获得标准化,大多数unix版本支持这类线程。咱们将看到全部的Pthread函数都以ptread_开头。网络

基本线程函数

pthread_create

当一个程序由exec启动执行时,称为初始线程(initial thread)或者主线程(main thread)的单个线程就被建立了,其他线程则由ptread_create函数建立。并发

pthread_t pthread_create(pthread_t *tid,
    const pthread_attr_t *attr, void *(*func)(void *), void * arg);
复制代码

一个进程内的每一个线程都由一个线程ID(thread ID)标识,其数据类型为pthread_t(每每都是 unsigned int)。若是新的线程建立成功,其ID就经过tid指针返回。oracle

每一个线程都有许多属性(attribute):优先级、初始栈大小、是否为守护线程等等。咱们能够在建立线程时经过初始化一个取代默认设置的pthread_attr_t变量指定这些属性。一般状况咱们采用默认的设置,这时咱们把attr参数指定为空指针。函数

建立一个线程时咱们最后指定的参数是由该线程执行的函数及其参数。该线程经过调用该函数开始执行,而后显式(调用pthread_exit)或者隐式(函数返回)地终止。该函数的地址由func参数指定,该函数的惟一调用参数是指针arg。若是咱们须要给该函数传递多个参数,咱们就得把它们打包成一个结构,而后把这个结构的地址做为单个参数传递给func测试

注意funcarg的声明,func所指函数做为参数接受一个通用指针(void *),又做为返回值返回一个通用指针(void *)。这时的咱们能够把一个指针(它指向咱们指望的任何内容)传递给一个线程,又容许线程返回一个指针(它一样指向咱们所指望的任何内容)。

一般状况下Ptread函数的返回值成功时为0,出错时为某个非0值。与套接字及大多数系统调用出错时返回-1并置errno为某个正值的作法不一样的是,Pthread函数出错时做为函数返回值返回正值错误指示。举个例子,若是pthread_create因在线程数目上超过某个系统限制而不能建立新线程,函数返回值将是EAGAIN。Pthread函数不设置errno

pthread_join

咱们能够经过调用pthread_join等待一个给定线程终止。对比线程和unix进程,pthread_create相似于forkpthread_join相似于waitpid

pthread_t pthread_join(pthread_t *tid, void ** status);
复制代码

咱们必须制定要等待的线程的tid。不幸的是,Pthread没有办法等待任意一个线程(相似在waitpid中指定参数为-1)。若是status指针非空,来自所等待的线程的返回值(一个指向某个对象的指针)将存入status所指向的位置。

pthread_self

每一个线程都有一个在进程内标识自身的ID。线程ID由pthread_create返回,而咱们能够在pthread_join中使用它。每一个线程能够经过ptread_self获取自身的线程ID。

pthread_t pthread_self(void);
复制代码

对比unix线程,pthread_self相似于getpid

pthread_detach

一个线程或者是可汇合的(joinable,默认值),或者是脱离的(detached)。当一个可汇合的线程终止时,它的线程ID和退出状态将留存到另外一个线程对它调用pthread_join。脱离的线程却像守护进程,当它们终止时,全部相关资源都将被释放,咱们不能等待它们终止。

pthread_detach函数把指定的线程变为脱离状态。

pthread_t pthread_detach(pthread_t tid);
复制代码

pthread_exit

让一个线程终止的方法之一是调用pthread_exit

void pthread_exit(void *status);
复制代码

若是该线程不曾脱离,它的线程ID和退出状态将一直留存到调用进程内某个其余线程对它调用pthread_join

指针status不能指向局部于调用线程的对象,由于线程终止时,这样的对象也会消失。

让一个线程终止的另外两个方法是:

  1. 启动线程的函数(即pthread_create的第三个参数)能够返回。该函数的返回值就是相应线程的终止状态。
  2. 若是进程的main函数返回或者任何线程调用了exit,整个进程就终止。其中包括它的任何线程。

互斥锁

线程编程称为并发编程(concurrent programming)或者并行编程(parallel programming),由于多个线程能够并发(或者并行)地运行且访问相同的变量。在并发编程中更改同一个变量时可能会产生同步问题,其解决办法是使用一个互斥锁(mutex,表示mutual exclusion)保护共享变量;访问该变量的前提条件是持有该互斥锁。按照Pthread,互斥锁是类型为pthread_mutex_t的变量。咱们使用如下两个函数为一个互斥锁上锁和解锁。

int pthread_mutex_lock(pthread_mutex_t *mptr);
int pthread_mutex_unlock(pthread_mutex_t *mptr);
复制代码

若是试图上锁一个已被另外某个线程锁住的互斥锁,本线程将会被阻塞,直到该互斥锁被解锁为止。

若是某个互斥锁变量是静态分配的,咱们就必须把它初始化为常值PTHREAD_MUTEX_INITIALIZER。若是咱们在共享内存区中分配一个互斥锁,那么必须经过调用pthread_mutext_init函数在运行时将其初始化。

如下是一个利用互斥锁操做计数器的例子:

#include <stdio.h>
#include <pthread.h>
#define NLOOP 5000

int counter; /* 由线程进行递增操做 */
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;

void *doit(void *);

int main(int argc, char **argv) {
    pthread_t tidA, tidB;
    pthread_create(&tidA, NULL, &doit, NULL);
    pthread_create(&tidB, NULL, &doit, NULL);
    /* 等待线程退出 */
    pthread_join(tidA, NULL);
    pthread_join(tidB, NULL);
    exit(0);
}

void *doit(void *vptr) {
    int i, val;
    /* 先打印,再递增 */
    for (i = 0; i < NLOOP; i++) {
        pthread_mutex_lock(&counter_mutex);
        val = counter;
        printf("%d: %d\n", pthread_self(), val + 1);
        counter = val + 1;
        pthread_mutex_unlock(&counter_mutex);
    }
    return(NULL);
}
复制代码

使用互斥锁上锁会带来额外的开销,但并不会太大。

条件变量

互斥锁适合于防止同时访问某个共享变量,但咱们须要另外某种在等待期间让咱们进入睡眠的方式。条件变量(condition variable)结合互斥锁可以提供这样的功能。互斥锁提供互斥机制,条件变量提供信号机制。

按照Pthread,条件变量是类型为pthread_cond_t的变量。如下两个函数用来使用条件变量:

int pthread_cond_wait(pthread_cond_t *cptr, pthread_mutex_t *mptr);
int pthread_cond_signal(pthread_cond_t *cptr);
复制代码

如下是一个使用条件变量的例子:

#include <stdio.h>
#include <pthread.h>
#define NLOOP 5000

int counter; /* 由线程进行递增操做 */
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t counter_cond = PTHREAD_COND_INITIALIZER;
void *doit(void *);
int main(int argc, char **argv) {
    pthread_t tidA, tidB;
    pthread_create(&tidA, NULL, &doit, NULL);
    pthread_create(&tidB, NULL, &doit, NULL);
    /* 主线程循环等待操做完成 */
    pthread_mutex_lock(&counter_mutex);
    while (counter < NLOOP)
    {
        pthread_cond_wait(&counter_cond, &counter_mutex);
    }
    printf("main: %d\n", counter);
    pthread_mutex_unlock(&counter_mutex);
    exit(0);
}
void *doit(void *vptr) {
    int i, val;
    /* 先打印,再递增 */
    for (i = 0; i < NLOOP; i++) {
        pthread_mutex_lock(&counter_mutex);
        val = counter;
        if (val < NLOOP)
        {
            counter = val + 1;
        }
        pthread_cond_signal(&counter_cond);
        pthread_mutex_unlock(&counter_mutex);
    }
    return(NULL);
}
复制代码

主循环阻塞在pthread_cond_wait调用中,等待某个即将终止的线程发送发送信号到与counter关联的条件变量。主循环只在持有互斥锁期间才检查counter变量,若是发现无事可作,那么就调用pthread_cond_wait。该函数把调用线程投入睡眠并释放调用线程持有的互斥锁。此外,当pthread_cond_wait返回时(其余某个线程发送信号到与counter关联的条件变量以后),该线程再次持有该互斥锁。

为何每一个条件变量要关联一个互斥锁呢?由于“条件”一般是线程之间共享的某个变量的值。容许不一样线程设置和测试该变量要求有一个与该变量关联的互斥锁。举例来讲,若是上面的例子中没有使用互斥锁,那么主循环就是这样:

while (counter < NLOOP)
{
    pthread_cond_wait(&counter_cond, &counter_mutex);
}
复制代码

这里存在这样的可能:主线程外最后一个线程在主循环测试counter < NLOOP以后但在调用pthread_cond_wait以前递增了counter。若是发生这样的状况,最后那个“信号”就丢失了,形成主循环永远阻塞在pthread_cond_wait调用中,等待永远再也不发生的某事再次出现。

一样,要求pthread_cond_wait被调用时其所关联的互斥锁必须是上锁的,该函数做为单个原子操做解锁该互斥锁并把调用线程投入睡眠也是出于这个理由。要是该函数不先解锁该互斥锁,到返回时再给它上锁,调用线程就不得不实现解锁过后上锁该互斥锁,测试变量counter的代码将变为:

pthread_mutex_lock(&counter_mutex);
while (counter < NLOOP)
{
    pthread_mutex_unlock(&counter_mutex);
    pthread_cond_wait(&counter_cond, &counter_mutex);
    pthread_mutex_lock(&counter_mutex);
}
复制代码

然而这里也可能存在:主线程外最后一个线程在主线程调用pthread_mutex_unlockpthread_cond_wait之间终止并递增了counter的值。

pthread_cond_signal一般唤醒等在相应条件变量上的单个线程。有时候一个线程知道本身应该唤醒多个线程,这时它能够调用pthread_cond_broadcast唤醒等在相应条件变量上的全部线程。

int pthread_cond_broadcast(pthread_cond_t *cptr);
int pthread_cond_timedwait(pthread_cond_t *cptr, pthread_mutex_t *mptr, const struct timespec *abstime);
复制代码

pthread_cond_timedwait容许线程设置一个阻塞时间的限制。abstime是一个timespec结构,指定该函数必须返回时刻的系统时间,即到时候相应条件变量还没有收到信号的话,就会返回ETIME错误。

这里的abstime是一个绝对时间(absolute time),而不是一个时间增量(time delta)。这一点不一样于selectpselect。使用绝对时间的优势在于,若是该函数过早返回(多是由于捕获了某个信号),那么没必要改动timespec结构就能够再次调用该函数;缺点是首次调用该函数以前不得不调用gettimeofday

总结

建立一个线程一般比调用fork派生一个进程快得多。仅仅这一点就可以体现线程在繁重使用的网络服务器上的优点。

同一进程内的全部线程共享全局变量和描述符,从而容许不一样线程之间共享信息。然而这种共享却引入了同步问题,咱们必须使用Pthread同步原语“互斥锁”和“条件变量”来解决。共享数据的同步几乎是每一个线程化程序必不可少的部分。

条件变量必须和互斥锁配合使用,这是规范的一部分。这么规定的缘由在于若是不配合互斥锁,条件变量会面临可能的信号丢失的问题。这个信号丢失的问题有个专门的名字,叫作lost wake-up problem

关联到java

在java 1.2以后的版本,在java中建立的Thread,在linux平台下实际上就是Pthread。能够看出java中Thread的各个属性与Pthread比较相似(可是没有detached属性)。在同步方面,java有本身的同步机制(synchronized关键字),并无直接使用Pthread中的同步原语。java 1.5以后引入的java.util.concurrent.locks中的库函数,则与Pthread同步原语有更多的类似的地方。

另外,java中Objectwait/notify/notifyAllConditionawait/signal必需要在同步块中,其道理跟条件变量同样,都是为了不信号丢失的问题。

相关文章
相关标签/搜索