Linux 多线程全面解析

Linux 多线程全面解析

在传统的 Unix 模型中,当一个进程需要由另一个实体执行某件事时,该进程派生(fork)一个子进程,让子进程去进行处理。Unix 下的大多数网络服务器程序都是这么编写的,即父进程接受连接,派生子进程,子进程处理与客户的交互。

虽然这种模型很多年来使用得很好,但是 fork 时有一些问题:

  • fork 是昂贵的。内存映像要从父进程拷贝到子进程,所有描述字要在子进程中复制等等。目前有的 Unix 实现使用一种叫做写时拷贝(copy-on-write)的技术,可避免父进程数据空间向子进程的拷贝。尽管有这种优化技术,fork 仍然是昂贵的。

  • fork 子进程后,需要用进程间通信(IPC)在父子进程之间传递信息。Fork 之前的信息容易传递,因为子进程从一开始就有父进程数据空间及所有描述字的拷贝。但是从子进程返回信息给父进程需要做更多的工作。

线程有助于解决这两个问题。线程有时被称为轻权进程(lightweight process),因为线程比进程 “轻权”,一般来说,创建一个线程要比创建一个进程快 10~100 倍。

一个进程中的所有线程共享相同的全局内存,这使得线程很容易共享信息,但是这种简易性也带来了同步问题。

一个进程中的所有线程不仅共享全局变量,而且共享:进程指令、大多数数据、打开的文件(如描述字)、信号处理程序和信号处置、当前工作目录、用户 ID 和组 ID。但是每个线程有自己的线程 ID、寄存器集合(包括程序计数器和栈指针)、栈(用于存放局部变量和返回地址)、error、信号掩码、优先级。在 Linux 中线程编程符合 Posix.1 标准,称为 Pthreads。所有的 pthread 函数都以 pthread开头。在调用它们前均要包括 pthread.h 头文件,一个函数库 libpthread 实现。

  1. 线程基础介绍:==========

数据结构:

pthread_t:线程的ID
pthread_attr_t:线程的属性

操作函数:

pthread_create():创建一个线程
pthread_exit():终止当前线程
pthread_cancel():中断另外一个线程的运行
pthread_join():阻塞当前的线程,直到另外一个线程运行结束
pthread_attr_init():初始化线程的属性
pthread_attr_setdetachstate():设置脱离状态的属性(决定这个线程在终止时是否可以被结合)
pthread_attr_getdetachstate():获取脱离状态的属性
pthread_attr_destroy():删除线程的属性
pthread_kill():向线程发送一个信号

同步函数:

用于 mutex 和条件变量
pthread_mutex_init()初始化互斥锁
pthread_mutex_destroy()删除互斥锁
pthread_mutex_lock():占有互斥锁(阻塞操作)
pthread_mutex_trylock():试图占有互斥锁(不阻塞操作)。即,当互斥锁空闲时,将占有该锁;否则,立即返回。
pthread_mutex_unlock():释放互斥锁
pthread_cond_init():初始化条件变量
pthread_cond_destroy():销毁条件变量
pthread_cond_signal():唤醒第一个调用pthread_cond_wait()而进入睡眠的线程
pthread_cond_wait():等待条件变量的特殊条件发生
Thread-local storage(或者以Pthreads术语,称作线程特有数据):
pthread_key_create():分配用于标识进程中线程特定数据的键
pthread_setspecific():为指定线程特定数据键设置线程特定绑定
pthread_getspecific():获取调用线程的键绑定,并将该绑定存储在 value 指向的位置中
pthread_key_delete():销毁现有线程特定数据键
pthread_attr_getschedparam();获取线程优先级
pthread_attr_setschedparam();设置线程优先级
  1. 概念:======

线程的组成部分:

  • Thread ID 线程 ID

  • Stack 栈

  • Policy 优先级

  • Signal mask 信号码

  • Errno 错误码

  • Thread-Specific Data 特殊数据

  1. 线程定义 =======

pthread_t pthread_ID , 用于标识一个线程,不能单纯看成整数,可能是结构体,与实现有关

pthread_equal 函数用于比较两个 pthread_t 是否相等

#include <pthread.h>
  
int pthread_equal(pthread_t tid1,pthread_t tid2)

pthread_self 函数用于获得本线程的 thread id

#include <pthread.h>
  
pthread _t pthread_self(void);
  1. 线程的创建 ========

创建线程调用 pthread_create 函数:

#include <pthread.h>
  
 int pthread_create(
        pthread_t*restrict tidp,
        constpthread_attr_t*restrict attr,
        void*(*start_rtn)(void*),void*restrict arg);

参数说明:

  • pthread_t *restrict tidp:返回最后创建出来的 Thread 的 Thread ID

  • const pthread_attr_t *restrict attr:指定线程的 Attributes,后面会讲道,现在可以用 NULL

  • void *(*start_rtn)(void *):指定线程函数指针,该函数返回一个 void ,参数也为 void

  • void *restrict arg:传入给线程函数的参数

  • 返回错误值。

一个进程中的每个线程都由一个线程 ID(thread ID)标识,其数据类型是 pthread_t(常常是 unsigned int)。如果新的线程创建成功,其 ID 将通过 tid 指针返回。

每个线程都有很多属性:优先级、起始栈大小、是否应该是一个守护线程等等,当创建线程时,我们可通过初始化一个 pthread_attr_t 变量说明这些属性以覆盖缺省值。我们通常使用缺省值,在这种情况下,我们将 attr 参数说明为空指针。

最后,当创建一个线程时,我们要说明一个它将执行的函数。线程以调用该函数开始,然后或者显式地终止(调用 pthread_exit)或者隐式地终止(让该函数返回)。函数的地址由 func 参数指定,该函数的调用参数是一个指针 arg,如果我们需要多个调用参数,我们必须将它们打包成一个结构,然后将其地址当作唯一的参数传递给起始函数。

在 func 和 arg 的声明中,func 函数取一个通用指针(void *)参数,并返回一个通用指针(void *),这就使得我们可以传递一个指针(指向任何我们想要指向的东西)给线程,由线程返回一个指针(同样指向任何我们想要指向的东西)。调用成功,返回 0,出错时返回正 Exxx 值。

pthread 函数在出错的时候不会设置 errno,而是直接返回错误值

在 Linux 系统下面,在老的内核中,由于 Thread 也被看作是一种特殊,可共享地址空间和资源的 Process,因此在同一个 Process 中创建的不同 Thread 具有不同的 Process ID(调用 getpid 获得)。而在新的 2.6 内核之中,Linux 采用了 NPTL(Native POSIX Thread Library) 线程模型,在该线程模型下同一进程下不同线程调用 getpid 返回同一个 PID。

不能对创建的新线程和当前创建者线程的运行顺序作出任何假设

  1. 线程的退出 ========
  • exit, _Exit, _exit 用于中止当前进程,而非线程

  • 中止线程可以有三种方式:
    a.在线程函数中 return
    b.被同一进程中的另外的线程 Cancel 掉
    c. 线程调用 pthread_exit 函数

  • pthread_exit 和 pthread_join 函数的用法:
    a.线程 A 调用 pthread_join(B, &rval_ptr),被 Block,进入 Detached 状态(如果已经进入 Detached 状态,则 pthread_join 函数返回 EINVAL)。如果对 B 的结束代码不感兴趣,rval_ptr 可以传 NULL。
    b. 线程 B 调用 pthread_exit(rval_ptr),退出线程 B,结束代码为 rval_ptr。注意 rval_ptr 指向的内存的生命周期,不应该指向 B 的 Stack 中的数据。
    c. 线程 A 恢复运行,pthread_join 函数调用结束,线程 B 的结束代码被保存到 rval_ptr 参数中去。如果线程 B 被 Cancel,那么 rval_ptr 的值就是 PTHREAD_CANCELLED。

两个函数原型如下:

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

该函数等待一个线程终止。把线程和进程相比,pthread_creat 类似于 fork,而 pthread_join 类似于 waitpid。我们必须要等待线程的 tid,很可惜,我们没有办法等待任意一个线程结束。如果 status 指针非空,线程的返回值(一个指向某个对象的指针)将存放在 status 指向的位置。

  • 一个 Thread 可以要求另外一个 Thread 被 Cancel,通过调用 pthread_cancel 函数:
#include <pthread.h>
 
void pthread_cancel(pthread_t tid)

该函数会使指定线程如同调用了 pthread_exit(PTHREAD_CANCELLED)。不过,指定线程可以选择忽略或者进行自己的处理,在后面会讲到。此外,该函数不会导致 Block,只是发送 Cancel 这个请求。

  • 线程可以安排在它退出的时候,某些函数自动被调用,类似 atexit() 函数。需要调用如下函数:
#include <pthread.h>
 
void pthread_cleanup_push(void(*rtn)(void*),void*arg);
void pthread_cleanup_pop(int execute);

这两个函数维护一个函数指针的 Stack,可以把函数指针和函数参数值 push/pop。执行的顺序则是从栈顶到栈底,也就是和 push 的顺序相反。

在下面情况下 pthread_cleanup_push 所指定的 thread cleanup handlers 会被调用:
a.调用 pthread_exit
b.相应 cancel 请求
c.以非 0 参数调用 pthread_cleanup_pop()。(如果以 0 调用 pthread_cleanup_pop(),那么 handler 不会被调用

有一个比较怪异的要求是,由于这两个函数可能由宏的方式来实现,因此这两个函数的调用必须得是在同一个 Scope 之中,并且配对,因为在 pthread_cleanup_push 的实现中可能有一个 {,而 pthread_cleanup_pop 可能有一个}。因此,一般情况下,这两个函数是用于处理意外情况用的,举例如下:

void*thread_func(void*arg)
{
    pthread_cleanup_push(cleanup,“handler”)
 
    // do something
 
    Pthread_cleanup_pop(0);
    return((void*)0);
}

进程函数和线程函数的相关性:

Linux 多线程全面解析

缺省情况下,一个线程 A 的结束状态被保存下来直到 pthread_join 为该线程被调用过,也就是说即使线程 A 已经结束,只要没有线程 B 调用 pthread_join(A),A 的退出状态则一直被保存。而当线程处于 Detached 状态之时,当线程退出的时候,其资源可以立刻被回收,那么这个退出状态也丢失了。在这个状态下,无法为该线程调用 pthread_join 函数。我们可以通过调用 pthread_detach 函数来使指定线程进入 Detach 状态:

#include <pthread.h>
int pthread_detach(pthread_t tid);

通过修改调用 pthread_create 函数的 attr 参数,我们可以指定一个线程在创建之后立刻就进入 Detached 状态

  1. 线程同步 =======

互斥量:Mutex

各个现成向同一个文件顺序写入数据,最后得到的结果是不可想象的。所以用互斥锁来保证一段时间内只有一个线程在执行一段代码。

  • 用于互斥访问

  • 类型:pthread_mutex_t,必须被初始化为 PTHREAD_MUTEX_INITIALIZER

(用于静态分配的 mutex,等价于 pthread_mutex_init(…, NULL))或者调用 pthread_mutex_init。Mutex 也应该用 pthread_mutex_destroy 来销毁。这两个函数原型如下:(attr 的具体含义下一章讨论)

#include <pthread.h>
 
int pthread_mutex_init(
       pthread_mutex_t*restrict mutex,
       constpthread_mutexattr_t*restrict attr)
 
int pthread_mutex_destroy(pthread_mutex_t*mutex);

pthread_mutex_lock 用于 Lock Mutex,如果 Mutex 已经被 Lock,该函数调用会 Block 直到 Mutex 被 Unlock,然后该函数会 Lock Mutex 并返回。pthread_mutex_trylock 类似,只是当 Mutex 被 Lock 的时候不会 Block,而是返回一个错误值 EBUSY。

pthread_mutex_unlock 则是 unlock 一个 mutex。这三个函数原型如下:

#include <pthread.h>
 
int pthread_mutex_lock(pthread_mutex_t*mutex);
 
int pthread_mutex_trylock(pthread_mutex_t*mutex);
 
int pthread_mutex_unlock(pthread_mutex_t*mutex);

举例说明

void reader_function (void);
void writer_function (void);
char buffer;
int buffer_has_item=0;
pthread_mutex_t mutex;
struct timespec delay;
void main (void)
{
pthread_t reader;
/* 定义延迟时间*/
delay.tv_sec =2;
delay.tv_nec =0;
/* 用默认属性初始化一个互斥锁对象*/
pthread_mutex_init (&mutex,NULL);
pthread_create(&reader, pthread_attr_default,(void*)&reader_function), NULL);
writer_function();
}
void writer_function (void){
while(1){
/* 锁定互斥锁*/
pthread_mutex_lock (&mutex);
if(buffer_has_item==0){
buffer=make_new_item();
buffer_has_item=1;
}
/* 打开互斥锁*/
pthread_mutex_unlock(&mutex);
pthread_delay_np(&delay);
}
}
void reader_function(void){
while(1){
pthread_mutex_lock(&mutex);
if(buffer_has_item==1){
consume_item(buffer);
buffer_has_item=0;
}
pthread_mutex_unlock(&mutex);
pthread_delay_np(&delay);
}
}

需要注意的是在使用互斥锁的过程中很有可能会出现死锁:两个线程试图同时占用两个资源,并按不同的次序锁定相应的互斥锁,例如两个线程都需要锁定互斥锁 1 和互斥锁 2,a 线程先锁定互斥锁 1,b 线程先锁定互斥锁 2,这时就出现了死锁。此时我们可以使用函数 pthread_mutex_trylock,它是函数 pthread_mutex_lock 的非阻塞版本,当它发现死锁不可避免时,它会返回相应的信息,程序员可以针对死锁做出相应的处理。另外不同的互斥锁类型对死锁的处理不一样,但最主要的还是要程序员自己在程序设计注意这一点

读写锁:Reader-Writer Locks

多个线程可以同时获得读锁 (Reader-Writer lock in read mode),但是只有一个线程能够获得写锁 (Reader-writer lock in write mode)

读写锁有三种状态

  • 一个或者多个线程获得读锁,其他线程无法获得写锁

  • 一个线程获得写锁,其他线程无法获得读锁

  • 没有线程获得此读写锁

类型为 pthread_rwlock_t

创建和关闭方法如下:

#include <pthread.h>
 
int pthread_rwlock_init(
       pthread_rwlock_t*restrict rwlock,
       constpthread_rwlockattr_t*restrict attr)
 
int pthread_rwlock_destroy(pthread_rwlock_t*rwlock);

获得读写锁的方法如下:

#include <pthread.h>
 
int pthread_rwlock_rdlock(pthread_rwlock_t*rwlock);
 
int pthread_rwlock_wrlock(pthread_rwlock_t*rwlock);
 
int pthread_rwlock_unlock(pthread_rwlock_t*rwlock);
 
int pthread_rwlock_tryrdlock(pthread_rwlock_t*rwlock);
 
int pthread_rwlock_trywrlock(pthread_rwlock_t*rwlock);

pthread_rwlock_rdlock:获得读锁

pthread_rwlock_wrlock:获得写锁

pthread_rwlock_unlock:释放锁,不管是读锁还是写锁都是调用此函数

注意具体实现可能对同时获得读锁的线程个数有限制,所以在调用 pthread_rwlock_rdlock 的时候需要检查错误值,而另外两个 pthread_rwlock_wrlock 和 pthread_rwlock_unlock 则一般不用检查,如果我们代码写的正确的话。

  • Conditional Variable:条件变量

互斥锁一个明显的缺点是它只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条件变量被用来进行线程间的同步。

  • 条件必须被 Mutex 保护起来

  • 类型为:pthread_cond_t,必须被初始化为 PTHREAD_COND_INITIALIZER(用于静态分配的条件,等价于 pthread_cond_init(…, NULL))或者调用 pthread_cond_init

#include <pthread.h>
 
int pthread_cond_init(
       pthread_cond_t*restrict cond,
       constpthread_condxattr_t*restrict attr)
 
int pthread_cond_destroy(pthread_cond_t*cond);

pthread_cond_wait 函数用于等待条件发生(=true)。pthread_cond_timedwait 类似,只是当等待超时的时候返回一个错误值 ETIMEDOUT。超时的时间用 timespec 结构指定。此外,两个函数都需要传入一个 Mutex 用于保护条件

#include <pthread.h>
 
int pthread_cond_wait(
       pthread_cond_t*restrict cond,
       pthread_mutex_t*restrict mutex);
 
int pthread_cond_timedwait(
       pthread_cond_t*restrict cond,
       pthread_mutex_t*restrict mutex,
       conststruct timespec *restrict timeout);

一个简单例子:

pthread_mutex_t count_lock;
pthread_cond_t count_nonzero;
unsigned count;
decrement_count (){
pthread_mutex_lock (&count_lock);
while(count==0)
pthread_cond_wait(&count_nonzero,&count_lock);
count=count -1;
pthread_mutex_unlock (&count_lock);
}
increment_count(){
pthread_mutex_lock(&count_lock);
if(count==0)
pthread_cond_signal(&count_nonzero);
count=count+1;
pthread_mutex_unlock(&count_lock);
}

count 值为 0 时, decrement 函数在 pthread_cond_wait 处被阻塞,并打开互斥锁 count_lock。此时,当调用到函数 increment_count 时,pthread_cond_signal()函数改变条件变量,告知 decrement_count()停止阻塞。

timespec 结构定义如下:

struct timespec {
       time_t tv_sec;       /* seconds */
       long   tv_nsec;      /* nanoseconds */
};

注意 timespec 的时间是绝对时间而非相对时间,因此需要先调用 gettimeofday 函数获得当前时间,再转换成 timespec 结构,加上偏移量。

有两个函数用于通知线程条件被满足(=true):

#include <pthread.h>
 
int pthread_cond_signal(pthread_cond_t*cond);
 
int pthread_cond_broadcast(pthread_cond_t*cond);

两者的区别是前者会唤醒单个线程,而后者会唤醒多个线程。

  1. 线程属性 =======
  • 线程属性设置

我们用 pthread_create 函数创建一个线程,在这个线程中,我们使用默认参数,即将该函数的第二个参数设为 NULL。的确,对大多数程序来说,使用默认属性就够了,但我们还是有必要来了解一下线程的有关属性。

属性结构为 pthread_attr_t,它同样在头文件 pthread.h 中定义,属性值不能直接设置,须使用相关函数进行操作,初始化的函数为 pthread_attr_init,这个函数必须在 pthread_create 函数之前调用。属性对象主要包括是否绑定、是否分离、

堆栈地址、堆栈大小、优先级。默认的属性为非绑定、非分离、缺省的堆栈、与父进程同样级别的优先级。

  • 绑定

关于线程的绑定,牵涉到另外一个概念:轻进程(LWP:Light Weight Process)。轻进程可以理解为内核线程,它位于用户层和系统层之间。系统对线程资源的分配、对线程的控制是通过轻进程来实现的,一个轻进程可以控制一个或多个线程。默认状况下,启动多少轻进程、哪些轻进程来控制哪些线程是由系统来控制的,这种状况即称为非绑定的。绑定状况下,则顾名思义,即某个线程固定的 "绑" 在一个轻进程之上。被绑定的线程具有较高的响应速度,这是因为 CPU 时间片的调度是面向轻进程的,绑定的线程可以保证在需要的时候它总有一个轻进程可用。通过设置被绑定的轻进程的优先级和调度级可以使得绑定的线程满足诸如实时反应之类的要求。

设置线程绑定状态的函数为 pthread_attr_setscope,它有两个参数,第一个是指向属性结构的指针,第二个是绑定类型,它有两个取值:PTHREAD_SCOPE_SYSTEM(绑定的)和 PTHREAD_SCOPE_PROCESS(非绑定的)。下面的代码即创建了一个绑定的线程。

#include <pthread.h>
pthread_attr_t attr;
pthread_t tid;
/*初始化属性值,均设为默认值*/
pthread_attr_init(&attr);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
pthread_create(&tid,&attr,(void*) my_function, NULL);

线程分离状态

线程的分离状态决定一个线程以什么样的方式来终止自己。非分离的线程终止时,其线程 ID 和退出状态将保留,直到另外一个线程调用 pthread_join. 分离的线程在当它终止时,所有的资源将释放,我们不能等待它终止。

设置线程分离状态的函数为

pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)

第二个参数可选为 PTHREAD_CREATE_DETACHED(分离线程)或 PTHREAD _CREATE_JOINABLE(非分离线程)。

这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在 pthread_create 函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用 pthread_create 的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用 pthread_cond_timewait 函数,让这个线程等待一会儿,留出足够的时间让函数 pthread_create 返回。设置一段等待时间,是在多线程编程里常用的方法。

  • 4.优先级

它存放在结构 sched_param 中。用函数
pthread_attr_getschedparam 和函数 pthread_attr_setschedparam 进行存放,一般说来,我们总是先取优先级,对取得的值修改后再存放回去。下面即是一段简单的例子。

#include <pthread.h>
#include <sched.h>
pthread_attr_t attr;pthread_t tid;
sched_param param;
int newprio=20;
/*初始化属性*/
pthread_attr_init(&attr);
/*设置优先级*/
pthread_attr_getschedparam(&attr,¶m); 
param.sched_priority=newprio;
pthread_attr_setschedparam(&attr,¶m);
pthread_create(&tid,&attr,(void*)myfunction, myarg);

来源:入门小站

(版权归原作者所有,侵删)

相关新闻

历经多年发展,已成为国内好评如潮的Linux云计算运维、SRE、Devops、网络安全、容器云、Go、Python开发专业人才培训机构!