Linux高性能server规划——多线程编程(在)

多线程编程


Linux主题概述


线程模型


        线程是程序中完毕一个独立任务的完整执行序列。即一个可调度的实体。

依据执行环境和调度者的身份。线程可分为内核线程和用户线程。内核线程,在有的系统上也称为LWP(Light Weigth Process。轻量级进程)。执行在内核空间,由内核来调度;用户线程执行在用户空间,由线程库来调度。当进程的一个内核线程获得CPU的使用权时。它就载入并执行一个用户线程。可见,内核线程相当于用于线程执行的容器。

一个进程能够拥有M个内核线程和N个用户线程,当中M≤N。而且在一个系统的全部进程中,M和N的比值都是固定的。依照M:N的取值。线程的实现方式可分为三种模式:全然在用户空间实现、全然由内和调度和双层调度。


        全然在用户空间实现的线程无需内核的支持,内核甚至根本不知道这些线程的存在。

线程库负责管理全部运行线程。比方线程的优先级、时间片等。

线程库利用longjmp来切换线程的运行。使它们看起来像是并发运行的。但实际上内核仍然是把整个进程作为最小单位来调度的。换句话说,一个进程的全部运行线程共享该进程的时间片,它们对外表现出同样的优先级。

因此,对这样的实现方式而言,N=1,即M个用户空间线程对于1个内核线程,而该内核线程实际上就是进程本身。全然在用户空间实现的线程的长处是:创建和调度线程都无须内核的干预,因此速度相当快。而且由于它不占用额外的内核资源,全部即使一个进程创建了非常多线程,也不会对系统性能造成明显的影响。

其缺点是,对于多处理器系统,一个进程的多个线程无法运行在不同的CPU上。由于内核是依照其最小调度单位来分配CPU的。此外,线程的优先级仅仅对同一个进程中的线程有效。比較不同进程中的线程的优先级没有意义。


全然由内核调度的模式将创建、调度线程的任务都交给了内核,执行在用户空间的线程无需执行管理任务,这与全然在用户空间实现的线程恰恰相反。全然由内核调度的这样的线程实现方式满足M:N=1:1,即1个用户空间线程被映射为1个内核线程。


双层调度模式是前两种实现模式的混合体:内核调度M个内核线程。线程库调度N个用户线程。这样的线程实现方式结合了前两种方式的长处:不但不会消耗过多的内核资源,并且线程切换速度也较快,同一时候她能够充分利用多处理器的优势。


创建线程和结束线程


pthread_create


#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);


thread參数是新县城的标识符。兴许pthread_*函数通过它来应用新线程。其类型pthread_t定义例如以下:


#include <bits/pthreadtypes.h>

typedef unsigned long int pthread_t


arg參数用于设置新线程的属性。

给它传递NULL表示使用默认线程属性。

线程拥有众多属性,我们将在后面讨论。start_routine和arg參数分别指定新线程将执行的函数及其參数。pthread_create成功时返回0。失败是返回错误码。

pthread_exit


        线程一旦被创建好,内核就能够调度内核线程来运行start_routine函数指针所指向的 函数了。

线程函数在结束时最好调用例如以下函数,以确保安全、干净退出。


#include <pthread.h>

void pthread_exit(void *retval);


pthread_exit函数通过retval參数向线程的回收者传递其退出信息。它运行完之后不会返回到调用者。并且永远不会失败。

pthread_join


一个进程中的全部线程都能够调用pthread_join函数来回收其它线程,即等待其它线程结束,这类似于回收进程的wait和waitpid系统调用。

pthread_join的定义例如以下:


#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);


thread參数是目标线程的标识符,retval參数则是目标线程返回的退出信息。该函数会一直堵塞,知道被回收的线程结束为止。该函数成功时返回0,失败时返回错误码。可能的错误码例如以下表:


错误码

描写叙述

EDEADLK

可能引起死锁。

比方两个线程互相对对方调用pthread_join,或者线程对自身调用pthread_join

EINVAL

目标线程是不可回收的,或者已经有其它线程在回收该目标线程

ESRCH

目标线程不存在


pthread_cancle


有时候我们希望终止一个线程。即取消线程,它是通过例如以下函数实现的:


#include <pthread.h>
int pthread_cancel(pthread_t thread);


thread參数是目标线程的标识符。该函数成功时返回0。失败时返回错误码。只是,接收到取消请求的目标线程能够决定是否同意被取消以及怎样取消,这分别由例如以下两个函数完毕。


#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);


这两个函数的第一个參数分别用于设置线程的取消状态(是否同意取消),和取消类型(怎样取消)。第二个參数则分别 线程原来的取消状态和取消类型。state參数有两个可选值:


PTHREAD_CANCEL_ENABLE,同意线程被取消。它是线程被创建的默认取消状态。


PTHREAD_CANCEL_DISABLE。禁止线程被取消。这样的情况下。假设一个线程收到取消请求。则它会将请求挂起,直到该线程同意被取消。


type參数也有两个可选值:


PTHREAD_CANCEL_ASYNCHRONOUS,线程随时能够被取消。它将使得接收到取消请求的目标线程马上採取行动。


PTHREAD_CANCEL_DEFERROR,同意目标线程推迟行动,直到它调用了所谓的取消点函数。


这两个函数成功时返回0,失败时返回错误码。



线程属性


pthread_attr_t结构体定义了一套完整的线程属性。例如以下所看到的:


#inlcude <bits/pthreadtypes.h>
#define _SIZEOF_PTHREAD_ATTR_T 36
typedef union
{
char __size[__SIZEOF_PTHREAD_ATTR_T];
long int __align;

} pthread_attr_t;


各种线程属性武安不包括在一个字符数组中。线程库定义了一些列函数来操作pthread_attr_t类型的变量,以方便我们获取和设置线程属性。

我们能够用pthread_attr_t结构改动线程默认属性,并把这些属性与创建的线程联系起来。能够使用pthread_attr_init函数初始化pthreaad_attr_t结构(或者叫初始化线程属性对象)。调用pthread_attr_init以后,pthread_attr_t结构所包括的内容就是操作系统实现支持的线程全部属性的默认值。假设要改动当中个别属性的值,须要调用其它的函数。pthread_attr_destroy能够去除对pthread_attr_t结构的初始化(销毁线程属性对象)。


#include<pthread.h>
intpthread_attr_init(pthread_attr_t *attr);
intpthread_attr_destroy(pthread_attr_t *attr);

POSIX.1定义的线程属性主要有detachstate(线程的分离状态属性),guardsize(线程栈末尾的警戒缓冲区大小),stackaddr(线程栈最低地址),stacksize(线程栈的大小(字节数))。


假设对现有的某个线程的终止状态不感兴趣,能够使用pthread_detach函数让操作系统在线程退出时回收所占用的资源。假设在创建线程时就知道不须要了解线程的终止状态。则能够改动pthread_attr_t结果中的detachstate线程属性,让线程以分离状态启动。

能够使用pthread_attr_setdetachstate把线程属性detachstate设置为以下的合法值之中的一个:设置PTHREAD_CREATE_DETACHED,以分离状态启动线程;或者以PTHREAD_CREATE_JOINABLE,正常启动线程,引用程序能够获取线程的终止状态。


能够调用pthread_attr_getdetachstate函数获取当前detachstate线程属性。第二个參数所指向的整数或许被设置为PTHREAD_CREATE_DETACHED。也可能被设置为PTHREAD_CREATE_JOINABLE。


#include<pthread.h>
intpthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate(pthread_attr_t*attr, int *detachstate)

函数pthread_attr_getstack和pthread_attr_setstack能够对线程栈属性进行查询和改动。


#include<pthread.h>
int pthread_attr_setstack(pthread_attr_t*attr, void *stackaddr, size_t stacksize);

int pthread_attr_getstack(pthread_attr_t*attr, void **stackaddr, size_t *stacksize);


这两个函数能够用于管理stackaddr线程属性和stacksize线程属性。应用程序也能够通过pthread_attr_setstacksize和pthread_attr_getstacksize函数读取或设置线程属性stacksize。


#include<pthread.h>
intpthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
intpthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);

线程属性guardsize控制着线程栈末尾之后用以避免栈溢出的扩展内存大小。

这个属性默认设置为PAGESIZE个字节。能够把guardsize线程属性设置为0。从而不同意属性的这样的特征行为发生:在这样的情况下。不会提供警戒缓冲区。相同的,如果对线程属性stackaddr做了改动,系统就会如果我们会自己管理栈,并使警戒缓冲区机制无效,等同于guardsize线程属性设为0。


#include<pthread.h>
intpthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);

intpthread_attr_getguardsize(pthread_attr_t *attr, size_t *guardsize);


假设guardsize线程属性被改动了,操作系统可能把它取为页大小的整数倍。

假设线程的栈指针溢出到警戒区,应用程序就能够通过信号接收到出错信息。


POSIX信号量


线程同步的机制以下讲3种:信号量、相互排斥量和条件变量。


#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);

int sem_post(sem_t *sem);


这些函数的第一个參数sem指向被操作的信号量。


sem_int用于初始化一个未命名的信号量。pshared參数指定信号量的类型。假设pshared參考指定信号量的类型。假设其值为0。就表示这个信号量是当前进程的局部信号量。否则该信号量就能够在多个进程之间共享。

value參数指定信号量的初始值。此外,初始化一个已经被初始化的信号量将导致不可预期的结果。


sem_destroy函数用于销毁信号量,以释放期占用的内核资源。

假设销毁一个正在被其它线程等待的信号量,则将导致不可预期的后果。


sem_wait函数以原子操作的方式将信号量减1。

假设信号量的值为0。则sem_wait将被堵塞。直到这个信号量具有非0值。


sem_trywait与sem_wait函数相似。只是它始终马上返回,而不论被操作的信号是否具有非0值,相当于sem_wait的非堵塞版本号。

当信号量的值非0时,sem_trywait对信号量运行减1操作。当信号量的值非0时,sem_trywait对信号量运行减1操作。

当信号量的值为0时,它将返回-1并设置errno为EAGAIN。


sem_post函数以原子操作的方式将信号量的值加1.当信号量的值大于0时,其它正在调用sem_wait等待信号量的线程将被唤醒。


上面这些函数成功时返回0,失败是返回-1并设置errno。


相互排斥锁


相互排斥锁基础API


POSIX相互排斥锁的相关函数主要有例如以下5个:


#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);

int pthread_mutex_unlock(pthread_mutex_t *mutex);


这些函数的第一个參数mutex指向操作的目标相互排斥锁,相互排斥锁的类型是pthread_mutex_t结构体。


pthread_mutex_init函数用于初始化相互排斥锁。

mutexattr參数指定相互排斥锁的属性。假设将它设置为NULL。则表示使用默认属性。除了这个函数外,我们还能够用例如以下方式初始化一个相互排斥锁:


pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;


宏PTHREAD_MUTEX_INITIALIZER实际上仅仅是把相互排斥锁的各个字段都初始化为0。


pthread_mutex_destroy函数用于小胡相互排斥锁,以释放期占用的内核资源。

销毁一个已经加锁的相互排斥锁将导致不可预期的后果。


pthread_mutex_lock函数以原子操作的方式给一个相互排斥锁加锁。

假设目标相互排斥锁已经被锁上,则pthread_mutex_lock调用将堵塞。直到该相互排斥锁的占有者将其解锁。


pthread_mutex_trylock与pthread_mutex_lock函数类似。只是它始终马上返回。而不论被操作的相互排斥锁是否已经加锁,相当于pthread_mutex_lock的非堵塞版本号。

当目标相互排斥锁未被加锁时,pthread_mutex_trylock对相互排斥锁运行加锁操作。

当相互排斥锁已经被加锁时,pthread_mutex_trylock将返回错误码EBUSY。

须要注意的是,这里讨论的pthread_mutex_lock和pthread_mutex_trylock的行为是针对普通锁而言的。


pthread_mutex_unlock函数以院子操作的方式给一个相互排斥锁解锁。假设此时有其它线程正在等待这个相互排斥锁。则这些线程中的某一个将获得它。


上面这些函数成功时返回0。失败时返回错误码。


相互排斥锁属性


pthread_mutexattr_t结构体定义了一套完整的相互排斥锁属性。

线程库提供了一系列函数来操作pthread_mutexattr_t类型变量,以方便我们获取和设置相互排斥锁属性。这里我们列出当中一些基本的函数:


#include <pthread.h>
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *
restrict attr, int *restrict pshared);
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);
int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr, int *restrict type);

int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);


这里仅仅讨论相互排斥锁的两种经常使用属性:pshared和type。

相互排斥锁属性pshared指定是否同意跨进程共享相互排斥锁。其可选值有两个:


PTHREAD_PROCESS_SHARED。相互排斥锁能够被跨进程共享。

PTHREAD_PROCESS_PRIVATE。

相互排斥锁仅仅能被和锁的初始化线程隶属于同一个进程的线程共享。


相互排斥锁属性type指定相互排斥锁的类型。Linux支持例如以下4种类型的相互排斥锁:


PTHREAD_MUTEX_NORMAL。普通锁。

这是相互排斥锁默认的类型。

当一个线程对一个普通锁加锁以后。其余请求该所的线程将形成一个等待队列,并在该所解锁后按优先级获得它。这样的锁类型保证了资源分配的公平性。但这样的锁也非常easy引发问题:一个线程假设对一个已经加锁的普通锁再次加锁,将引发死锁。对一个已经被其它线程加锁的普通锁解锁,或者对一个已经解锁的普通锁解锁将导致不可预期的后果。


PTHREAD_MUTEX_ERRORCHECK,检错锁。

一个线程假设对一个已经加锁的检错锁再次加锁,则加锁操作返回EDEADLK。对一个已经被其让他线程加锁的检错锁解锁,或者对一个已经解锁的检错锁再次解锁,则检错锁返回EPERM。


PTHREAM_MUTEX_RECURSIVE。嵌套锁。

这样的锁同意一个线程在释放锁之前对他加锁而不发生死锁。只是其它线程假设要获得这个锁,则当前锁的拥有者必须运行对应次数的解锁操作。

对一个已经被其它线程枷锁的嵌套锁解锁。或者对一个已经解锁的嵌套锁再次解锁,则解锁操作返回EPERM。


PTHREAD_MUTEX_DEFAULT。默认锁。

一个线程假设对一个已经加锁的默认锁再次加锁。或者对一个已经被其它线程加锁的默认锁解锁,或者对一个已经解锁的默认锁再次解锁,将导致不可预期的后果。


死锁举例


使用相互排斥锁的一个噩耗是死锁。

死锁使得一个或多个线程被挂起而无法继续运行,并且这样的情况还不easy被发现。

在一个线程中对还有一个已经加锁的普通锁再次加锁将导致死锁,这样的情况可能出如今设计的不够细致的递归函数中。

另外,假设两个线程依照不同的顺序来申请两个相互排斥锁,也easy产生死锁。


例如以下所看到的便是按不同顺序訪问相互排斥锁导致死锁的实例:


#include <pthread.h>
#include <unistd.h>
#include <stdio.h>

int a = 0;
int b = 0;
pthread_mutex_t mutex_a;
pthread_mutex_t mutex_b;

void* another( void* arg )
{
    pthread_mutex_lock( &mutex_b );
    printf( "in child thread, got mutex b, waiting for mutex a
" );
    sleep( 5 );
    ++b;
    pthread_mutex_lock( &mutex_a );
    b += a++;
    pthread_mutex_unlock( &mutex_a );
    pthread_mutex_unlock( &mutex_b );
    pthread_exit( NULL );
}

int main()
{
    pthread_t id;
    
    pthread_mutex_init( &mutex_a, NULL );
    pthread_mutex_init( &mutex_b, NULL );
    pthread_create( &id, NULL, another, NULL );

    pthread_mutex_lock( &mutex_a );
    printf( "in parent thread, got mutex a, waiting for mutex b
" );
    sleep( 5 );
    ++a;
    pthread_mutex_lock( &mutex_b );
    a += b++;
    pthread_mutex_unlock( &mutex_b );
    pthread_mutex_unlock( &mutex_a );

    pthread_join( id, NULL );
    pthread_mutex_destroy( &mutex_a );
    pthread_mutex_destroy( &mutex_b );
    return 0;
}



代码中增加sleep函数来模拟连续调用pthread_mutex_lock之间的时间差,以确保代码中的两个线程各自占有一个相互排斥锁,然后等待另外一个相互排斥锁。这样,两个线程就僵持住了,谁都不能继续往下执行,从而形成死锁。假设代码中不增加sleep函数,则这段代码也许总能成功执行,从而为程序留下一了个潜在的BUG。



版权声明:本文博主原创文章,博客,未经同意不得转载。

原文地址:https://www.cnblogs.com/zfyouxi/p/4856662.html