zoukankan      html  css  js  c++  java
  • Linux平台服务器多线程开发(一)

    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,普通锁。这是互斥锁默认的类型。当一个线程对一个普通锁加锁以后,其余请求该所的线程将形成一个等待队列,并在该所解锁后按优先级获得它。这种锁类型保证了资源分配的公平性。但这种锁也很容易引发问题:一个线程如果对一个已经加锁的普通锁再次加锁,将引发死锁;对一个已经被其他线程加锁的普通锁解锁,或者对一个已经解锁的普通锁解锁将导致不可预期的后果。

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

    PTHREAM_MUTEX_RECURSIVE,嵌套锁。这种锁允许一个线程在释放锁之前对他加锁而不发生死锁。不过其他线程如果要获得这个锁,则当前锁的拥有者必须执行相应次数的解锁操作。对一个已经被其他线程枷锁的嵌套锁解锁,或者对一个已经解锁的嵌套锁再次解锁,则解锁操作返回EPERM。

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

    死锁举例

    使用互斥锁的一个噩耗是死锁。死锁使得一个或多个线程被挂起而无法继续执行,而且这种情况还不容易被发现。在一个线程中对另一个已经加锁的普通锁再次加锁将导致死锁,这种情况可能出现在设计的不够仔细的递归函数中。另外,如果两个线程按照不同的顺序来申请两个互斥锁,也容易产生死锁。

    如下所示便是按不同顺序访问互斥锁导致死锁的实例:

    #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。

  • 相关阅读:
    14_java之变量|参数|返回值|修饰符
    NYOJ 202 红黑树 (二叉树)
    NYOJ 138 找球号(二) (哈希)
    NYOJ 136 等式 (哈希)
    NYOJ 133 子序列 (离散化)
    NYOJ 129 树的判定 (并查集)
    NYOJ 117 求逆序数 (树状数组)
    NYOJ 93 汉诺塔 (数学)
    HDU 2050 折线分割平面 (数学)
    天梯赛L2-008 最长对称子串 (字符串处理)
  • 原文地址:https://www.cnblogs.com/chinasirius/p/13417075.html
Copyright © 2011-2022 走看看