【服务器编程】Linux多线程编程(pthread)

本文详细介绍了Linux下的多线程编程,包括线程模型、线程库NPTL、创建线程和结束线程的方法、互斥锁、条件变量等。还讨论了线程和进程、线程与信号的关系,以及线程安全和信号掩码在多线程环境中的应用。
摘要由CSDN通过智能技术生成

本文讨论的线程相关的内容都属于POSIX线程(简称pthread)标准,线程库是NPTL(Native POSIX Thread Library),以下的具体包括:

  • 创建线程和结束线程
  • 读取和设置线程属性
  • POSIX线程同步方式:POSIX信号量、互斥锁和条件变量

Linux线程概述

线程模型

线程是程序中完成一个独立任务的完整执行序列,即一个可调度的实体。根据运行环境和调度者的身份,线程可分为内核线程和用户线程。内核线程,有的系统上称为“轻量级线程”,运行在内核空间,由内核来调度;用户线程运行在用户空间,由线程库来调度。当进程的一个内核线程获得CPU的使用权时,它就加载并运行一个用户线程。一个进程可以拥有M个内核线程和N个用户线程,其中M≤N。按照M:N的比值,线程的实现方式可分为三种模式:完全在用户空间实现(1:N)、完全由内核调度(1:1)和双层调度。

Linux线程库

Linux上两个最有名的线程库是LinuxThreads和NPTL,它们都是采用1:1的方式实现的。现在Linux上默认使用的线程库是NPTL。NPTL的主要优势在于:

  • 内核线程不再是一个进程
  • 摒弃了管理线程,终止线程,回收线程堆栈等工作都可以由内核来完成
  • 一个进程的线程可以运行在不同的CPU上,从而充分利用了多处理器系统的优势
  • 线程的同步由内核来完成。

创建线程和结束线程

  1. pthread_create

    创建一个线程的函数

    #include <pthread.h>
    int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void* (*start_routine)(void*), void* arg);
    

    其中thread参数是新线程的标识符,attr参数用于设置新线程的属性,strat_routinearg参数分别指定新线程将运行的函数及其参数。

  2. pthread_exit

    线程一旦被创建好,内核就可以调度内核线程来执行start_routine函数指针所指向的函数了。线程函数在结束时最好调用如下函数,以确保安全、干净地退出:

    #include <pthread.h>
    void pthread_exit(void* retval);
    

    该函数通过retval参数向回收者传递其退出信息。它执行完之后不会回到调用者,而且永远不会失败。

  3. pthread_join

    一个进程中的所有线程都可以pthread_join函数来回收其他线程,即等待其他线程结束,这类似于回收进程的waitwaitpid系统调用。

    #include <pthread.h>
    int pthread_join(pthread_t thread, void** retval);
    

    thread参数是目标线程的标识符,retval参数这是目标线程返回的退出信息。该函数会一直阻塞,直到被回收的线程结束为止。

  4. pthread_cancel

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

    #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);
    

    第一个函数用于设置线程的取消状态(是否允许取消),第二个用于设置取消类型(如何取消),参数old*用于记录原来的状态。

线程属性

pthread_attr_t结构体定义了一套完整的线程属性,如下所示:

#include <bit/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类型的变量,以方便获取和设置线程属性。

POSIX信号量

POSIX信号量函数的名字都以sem_开头,并不像大多数线程函数那样以pthread_开头,常用的POSIX信号量函数有以下几个:

#include <semaphore.h>
// 初始化一个未命名的信号量
// pshared指定信号量的类型。如果其值为0,表示这个信号量是当前进程的局部信号量,
// 否则该信号量就可以在多个进程之间共享
int sem_init(sem_t* sem, int pshared, unsigned int value);
// 销毁信号量
int sem_destory(sem_t* sem);
// 原子操作的方式将信号量的值减1.如果信号量为0,则sem_wait将被阻塞,
// 直到这个信号量非0
int sem_wait(sem_t* sem);
// 非阻塞版本,当信号量为0时,返回-1并设置errno为EAGAIN
int sem_trywait(sem_t* sem);
// 以原子操作的方式将信号量加1
int sem_post(sem_t* sem);

互斥锁

互斥锁可以用于保护关键代码段,以确保其独占式的访问。

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

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutextattr_t* mutexattr);
int pthread_mutex_destroy(pthread_mutex_t* mutex);
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结构体。mutexattr指定互斥锁的属性,将它设置为NULL,则表示使用默认属性。

互斥锁还可以使用另外一种方式来初始化:

pthread_mutex_t = mutex = PTHREAD__MUTEX_INITIALIZER;

互斥锁属性

pthread_mutexattr_t结构体定义了一套完整的互斥锁属性。线程库提供了一系列函数来操作pthread_mutexatttr_t类型的变量,以方便获取和设置互斥锁属性。

互斥锁两种常用的属性:psharedtypepshared指定是否允许跨进程共享互斥锁;type指定互斥锁的类型:

  • 普通锁(默认):下列情况会有问题:一个线程内对一个已经加锁的锁再次加锁;对一个已经被其他线程加锁的普通锁解锁;对一个已经被解锁的普通锁解锁;
  • 检错锁:会检错普通锁存在问题的情况,返回对应的错误码:重复加锁返回EDEADLK,重读解锁返回EPERM
  • 嵌套锁:允许多次加锁,但是如果其他线程要获得这个锁,需要执行相应次数解锁。重复解锁会返回EPERM
  • 默认锁:映射为上面三种锁之一。

条件变量

如果说互斥锁是用于同步线程对共享数据的访问的话,那么条件变量则是用于在线程之间同步共享数据的值。条件变量提供一种线程间的通知机制:当某个共享数据达到某个值的时候,唤醒等待这个共享数据的线程。

#include <pthread.h>
int pthread_cond_init(pthread_cond_t* cond, pthread_condattr_t* cond_attr);
int pthread_cond_destory(pthread_cond_t* cond);
int pthread_cond_broadcast(pthread_cond_t* cond);
int pthread_cond_signal(pthread_cond_t* cond);
int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex);

除了pthread_cond_init函数外,我们还可以使用如下方式来初始化一个条件变量:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

pthread_cond_broadcast函数以广播的方式唤醒所有等待目标条件变量的线程。pthread_cond_signal函数用于唤醒一个等待目标条件变量的线程。至于哪个线程将被唤醒,则取决于线程的优先级和调度策略。

pthread_cond_wait用于等待目标条件变量。mutex参数是用于保护条件变量的互斥锁,以确保pthread_cond_wait操作的原子性。在调用pthread_cond_wait前,必须确保互斥锁mutex已经加锁,否则将导致不可预期的结果。pthread_cond_wait函数执行时,首先把调用线程放入条件变量的等待队列中,然后将互斥锁mutex解锁。当pthread_cond_wait函数成功返回时,互斥锁mutex将再次被锁上。

多线程环境

可重入函数

线程安全的函数,如果一个函数能被多个线程同时调用且不发生竞态条件。

线程和进程

考虑这样一种情况:如果一个多线程程序的某个线程调用了fork函数,那么新创建的子进程会将父进程的执行线程完整的复制,并且子进程将自动继承父进程中互斥锁的状态。也就是说,父进程中已经被加锁的互斥锁在子进程中也是被锁住的。这就会引起一个问题:子进程可能不清楚从父进程继承而来的互斥锁的具体状态,可能在父进程已经被加锁了。这种情况下,子进程如果再加锁就会导致死锁。

pthread提供一个专门的函数pthread_atfork,以确保fork调用后父进程和子进程都拥有一个清楚的锁状态。该函数定义如下:

#include <pthread.h>
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));

该函数建立3个fork句柄来帮助我们理清互斥锁的状态。

  • preparefork调用创建出子进程之前被执行;
  • parentfork调用创建出子进程之后,返回之后,在父进程中被执行;
  • childfork返回之前,在子进程中被执行。

线程和信号

每个线程可以独立地设置信号掩码,在多线程环境下pthread版本的信号掩码函数:

#include <pthread.h>
#include <signal.h>
int pthread_sigmask(int how, const sigset_t* newmask, sigset_t* oldmask);

信号处理线程,专门的线程来处理所有的信号,实现步骤如下:

  1. 在主线程创建出其他子线程之前就调用pthread_sigmask来设置好信号掩码,所有新创建的子线程都将自动继承这个信号掩码。

  2. 在某个线程中调用如下函数来等待信号并处理之:

    #include <signal.h>
    int sigwait(const sigset_t* set,int* sig)
    

    set参数指定需要等待信号的集合,sig参数指向函数接收到的信号值,该函数会接收集合里的所有信号。

如果觉得还不错,关注公众号获取更多优质文章 ~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值