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\n" );
    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\n" );
    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。

Linux高性能服务器编程——多线程编程(上)

时间: 2024-10-19 11:13:47

Linux高性能服务器编程——多线程编程(上)的相关文章

Linux高性能服务器编程——多线程编程(下)

多线程编程 条件变量 如果说互斥锁是用于同步线程对共享数据的访问的话,那么条件变量则是用于线程之间同步共享数据的值.条件变量提供了一种线程间的通信机制:当某个共享数据达到某个值得时候,唤醒等待这个共享数据的线程. 条件本身是由互斥量保护的.线程在改变条件状态前必须首先锁住互斥量,其他现成在获得互斥量之前不会察觉到这种变化,因为必须锁住互斥量以后才能计算条件. 条件变量的相关函数主要有如下5个: #include <pthread.h> int pthread_cond_destroy(pthr

Linux高性能服务器编程——I/O复用

 IO复用 I/O复用使得程序能同时监听多个文件描述符,通常网络程序在下列情况下需要使用I/O复用技术: 客户端程序要同时处理多个socket 客户端程序要同时处理用户输入和网络连接 TCP服务器要同时处理监听socket和连接socket,这是I/O复用使用最多的场合 服务器要同时处理TCP请求和UDP请求.比如本章将要讨论的会社服务器 服务器要同时监听多个端口,或者处理多种服务. I/O复用虽然能同时监听多个文件描述符,但它本身是阻塞的.并且当多个文件描述符同时就绪时,如果不采用额外措施

Linux高性能服务器编程——定时器

 定时器 服务器程序通常管理着众多定时事件,因此有效组织这些定时事件,使之能在预期的时间点被触发且不影响服务器的主要逻辑,对于服务器的性能有着至关重要的影响.位置我们要将每个定时事件封装成定时器,并使用某种容器类型的数据结构,比如链表.排序链表和时间轮将所有定时器串联起来,以实现对定时事件的统一管理. Linux提供三种定时方法: 1.socket选项SO_RECVTIMEO和SO_SNDTIMEO. 2.SIGALRM信号 3.I/O复用系统调用的超时参数 socket选项SO_RCVTI

Linux 高性能服务器编程——高级I/O函数

重定向dup和dup2函数 [cpp] view plaincopyprint? #include <unistd.h> int dup(int file_descriptor); int dup2(int file_descriptor_one, int file_descriptor_two); dup创建一个新的文件描述符, 此描述符和原有的file_descriptor指向相同的文件.管道或者网络连接. dup返回的文件描述符总是取系统当前可用的最小整数值. dup2函数通过使用参数f

Linux高性能服务器编程——信号及应用

 信号 信号是由用户.系统或者进程发送给目标进程的信息,以通知目标进程某个状态的改变或系统异常.Linux信号可由如下条件产生: 对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号.比如输入Ctrl+C通常会给进程发送一个终端信号. 2.系统异常 系统状态变化 运行kill命令或调用kill函数 Linux信号概述 发送信号 Linux下,一个进程给其他进程发送信号的API是kill函数.其定义如下: #include <sys/types.h> #include <sign

Linux高性能服务器编程——多进程编程

多进程编程 多进程编程包括如下内容: 复制进程影映像的fork系统调用和替换进程映像的exec系列系统调用. 僵尸进程以及如何避免僵尸进程 进程间通信(Inter-Process Communication,IPC)最简单的方式:管道 3种进程间通信方式:信号量,消息队列和共享内存 fork系统调用 #include<unistd.h> pid_tfork(void); 该函数的每次都用都返回两次,在父进程中返回的是子进程的PID,在子进程中返回的是0.该返回值是后续代码判断当前进程是父进程还

Linux高性能服务器编程——系统检测工具

系统检测工具 tcpdump tcpdump是一款经典的转包工具,tcpdump给使用者提供了大量的选项,泳衣过滤数据报或者定制输出格式. lsof lsof是一个列出当前系统打开的文件描述符的工具.通过它我们可以了解感兴趣的进程打开了哪些文件描述符,或者我们感兴趣的文件描述符被哪些进程打卡了. nc nc命令主要被用来快速构建网络连接.我们可以让它以服务器方式运行,监听某个端口并接收客户连接,因此它可用来调试客户端程序.我们也可以使之以客户端方式运行,向服务器发起连接并收发数据,因此它可以用来

Linux高性能服务器编程——进程池和线程池

进程池和线程池 池的概念 由于服务器的硬件资源"充裕",那么提高服务器性能的一个很直接的方法就是以空间换时间,即"浪费"服务器的硬件资源,以换取其运行效率.这就是池的概念.池是一组资源的集合,这组资源在服务器启动之初就完全被创建并初始化,这称为静态资源分配.当服务器进入正是运行阶段,即开始处理客户请求的时候,如果它需要相关的资源,就可以直接从池中获取,无需动态分配.很显然,直接从池中取得所需资源比动态分配资源的速度要快得多,因为分配系统资源的系统调用都是很耗时的.当

Linux高性能服务器编程——Linux网络基础API及应用

 Linux网络编程基础API 详细介绍了socket地址意义极其API,在介绍数据读写API部分引入一个有关带外数据发送和接收的程序,最后还介绍了其他一些辅助API. socket地址API 主机字节序和网络字节序 字节序分为大端字节序和小端字节序.小端字节序又被称为主机字节序,大端字节序被称为网络字节序.大端字节序是指一个整数的高位字节存储在内存的低地址处,低位字节存储在内存的高地址处.小端字节序则相反. Linux提供如下四个函数完成主机字节序与网络字节序之间的转换: #include