linux 多线程API

多线程

概述

与进程并发类似,多线程允许应用程序并发的执行多个任务。一个进程可以包含多个线程。同一个进程中的线程会独立执行相同的程序,且共享同一分全局内存空间,其中包括程序文本段、初始化数据段、未初始化数据段、堆等
同一进程的线程可以并发执行。在多处理器环境下,多个线程可以并行执行。如果一个线程因为等待I/O操作而遭到阻塞,那么其他线程可以继续执行。

线程的优势:

  • 进程间的信息难以共享。除去只读代码段外,父、子进程并未共享内存,因此必须采用IPC来进行进程间的信息交换。
  • 调用fork() 创建进程的代价较高。尽管采用写时复制(copy-on-write)技术,但仍需复制内存页表(page table)和文件描述符表等多种进程属性,开销不小。
  • 线程之间能够方便快速地共享信息。只需将数据复制到共享变量(全局、堆或静态)中即可。不过要注意多线程同时修改同一共享变量的情况——可以通过同步技术来避免。
  • 线程的创建比进程快10倍。在Linux上,通过clone()来实现线程。快的原因:不需要复制进程属性。

除去全局内存外,线程还共享以下属性:

  • 进程ID
  • 打开的文件描述符
  • 信号处置
  • 当前工作目录
  • 定时器

  • 以下属性是各个线程共有的:
  • 线程ID
  • 信号掩码
  • 线程特有数据
  • errno变量

Pthreads API

与线程相关的数据类型

Pthread API定义了一系类的数据类型:

数据类型描述
pthread_t线程ID
pthread_mutex_t互斥量Mutex
pthread_mutexattr_t互斥量的属性
pthread_cond_t条件变量condition variable
pthread_condattr_t条件变量的属性
pthread_key_t
pthread_once_t一次性初始化变量
pthread_attr_t线程的属性

不能使用c语言的“==”操作符去比较这些类型的变量。

线程和errno

在传统UNIX API中,errno是一个全局整型变量。这会引发竞态条件(race condition)。因此,在多线程程序中,每个线程都有属于自己的errno。
On Linux, a thread-specific errno is achieved in a similar manner to most other UNIX implementations: errno is defined as a macro that expands into a function call returning a modifiable lvalue that is distinct for each thread. (Since the lvalue is modifiable, it is still possible to write assignment statements of the form errno = value in threaded programs.)

因此,SUSv3不允许将errno声明为extern int errno。每次引用errno会带来一次函数调用。
errno是线程安全,但不是可重入的。可重入的要求更高。

Pthreads 函数的返回值

一般的函数返回0表示成功,返回-1表示失败。
所有Pthread函数均以返回0表示成功返回正值表示失败,且设置errno标识失败的原因。需要包含<errno.h>

编译Pthreads 程序

在Linux平台,编译调用Pthreads API的程序,需要设置 -lpthread的编译选项。

线程的创建

#include<pthread.h>

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

//返回0表示成功,返回正值表示失败

  • 新线程通过调用start(arg)而开始运行。调用pthread_create()的线程会继续执行后面的语句。
  • 参数arg声明为void *类型,意味着可以将指向任意对象的指针传递给start()函数。一般情况下,arg指向一个全局或堆变量(如果要传递局部变量,则必须确保局部变量在新建线程的整个生命周期中不会被销毁),也可以是NULL。如果需要向start()传递多个参数,可以将arg指向一个结构,该结构的各个字段对应各个参数。经过强制转换,可以将int值转换为指针,以达到传递int值的目的。
    int j == (int)((void *)j)
  • start()的返回值类型为void *。与参数arg的使用方式相同,注意事项请看《Linux编程手册P513》
  • 在pthread_create()返回前,会将新创建的线程的ID赋给参数thread。在函数返回之前,新线程可能就已经在运行了,所以新线程应该通过pthread_self()来获取自己的线程ID
  • 参数attr指定了新建线程的属性。如果attr为NULL,则创建线程时将使用各种默认属性。

终止线程的方式

  • 线程的start函数执行return语句返回。
  • 线程调用pthread_exit()。
  • 调用pthread_cancel()取消线程。
  • 任意线程调用了_exit(),或者主线程执行了return语句,都会导致进程中的所有线程立刻终止。已分离的线程也会终止。

pthread_exit()函数会终止调用该函数的线程,且其返回值可由另一个线程通过调用pthread_join()来获取。

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

  • 在线程的start函数所调用的任何函数中调用pthread_exit()都会导致线程的终止。
  • 参数retval指定了线程的返回值。retval所指向的空间不应该在线程栈空间,因为线程终止后,线程栈可能会失效。
  • 如果主线程调用了pthread_exit(),而非调用exit()或执行return,则其他线程会继续执行

线程ID

  • 线程ID可用来标识线程;
  • 线程的ID会通过pthread_create()返回给调用者;
  • 线程通过pthread_self()获取自己的ID。

#include<pthread.h>
pthread_t pthread_self(void);

通过函数pthread_equal()来检查两个线程ID是否相等,而不是“==”操作符,因为pthread_t可能是整数,也可能是指针或结构体。

#include<pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);

在Linux中,线程ID在所有进程中都是唯一的,但其他实现未必如此。

连接(joining)已终止的线程

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

该函数用来等待ID为thread的线程的终止。如果线程已终止。则pthread_join()立即返回。

如果线程没有分离(detached),则必须使用pthread_join()来进行连接。否则线程终止时,会变成僵尸线程。
链接已经链接过的函数会导致未定的行为,因为相同的线程ID可能会被重用。

ptread_join()的功能与waitpid()类似,二者的区别如下:

  • 线程之间的关系对等的(peer)。进程中的任一线程均可通过pthread_join()与进程中的其他线程连接起来。例如,线程A创建线程B,但线程B可以连接线程A。
  • 无法连接任一线程,只能连接同一进程里的线程,也不能以非阻塞方式连接

线程的分离

有时候,程序员并不关心线程的返回状态,只是希望系统在线程终止时能够将线程自动清除并移除。通过pthread_detach()。可将ID为thread的线程标记为分离状态。此后,就不能在使用pthread_join()来连接该线程了。

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

线程可以自我分离:

pthread_detach(pthread_self());

  • 线程一旦分离,就不会再返回“可连接状态”。
  • 其他线程调用exit()或主线程执行return语句时,分离的线程也会立刻终止。

线程的属性

pthread_create()中类型为pthread_attr_t的attr参数可以来指定新建线程的属性,如果为NULL,则使用默认属性创建线程。
线程的属性包括:线程栈的位置和大小、线程调度策略和优先级,以及线程处于可连接状态还是分离状态等。

int pthread_attr_init(&attr);
//初始化一个线程属性变量

线程VS进程

将应用程序实现为一组进程还是线程?
线程的优点:

  • 线程间的数据共享很简单。而进程则需要用到IPC。
  • 线程的创建快于进程。
  • 线程的上下文切换(context switch)也比进程短。

线程的缺点:

  • 多线程编程时,需要确保调用线程安全(thraed-safe)的函数,或者以线程安全的方式来调用函数。

  • 某个线程的bug(通过错误的指针来修改线程)可能会危及进程中的其他线程。而进程则分离的更彻底。

  • 所有的线程都在争用主线程的虚拟地址空间。

  • 在多线程中,需谨慎设计信号的处理;一般建议在多线程中不要使用信号。

  • 线程只能运行同一程序。

  • 线程共享其他信息(文件描述符、信号处置、当前工作目录、用户ID和组ID)。

总结

在多线程程序中,多个线程并发执行同一程序。所有的线程共享相同的全局和堆变量,但每一个线程都分配了用来存放局部变量的私有栈——这些栈空间可以通过指针被其他线程使用。

线程同步

线程用来同步彼此行为的两个工具:互斥量(mutex)和条件变量(condition variable)。互斥量可以帮助线程同步对共享资源的使用,以防止出现下列情况:线程A试图访问一个共享变量,与此同时,线程B正在对其进行修改。条件变量则是对互斥量的一个补充:允许线程互相通知共享变量的状态发生了变化。

Protecting Accesses to Shared Variables: Mutexes

线程的主要优势在于,能够通过全局变量来共享信息。但是,必须请确保多个线程不会同时修改同一变量,某一线程也不会读取有其他线程正在修改的变量。
临界区(critical section)是指访问某一资源的代码片段,且这段代码的执行应该为原子操作,即不能被同时访问该资源的其他线程中断。
互斥变量mutex(mutual exclusion)用来确保同一时刻只有一个线程访问某项共享资源。也就是说可以使用mutex保证对任意共享资源的原子访问。

互斥变量有两种状态:已锁定(locked)和未锁定(unlocked)。任何时候只有一个线程可以锁定该互斥变量。试图对已经锁定的mutex再次加锁会导致线程阻塞或报错

线程在访问共享资源时采用如下协议:

  • 对某一共享资源加锁
  • 访问共享资源
  • 解锁

静态分配的互斥量

互斥量既可以像静态变量那样分配,也可通过malloc()动态创建。

互斥量的类型是pthread_mutex_t,互斥量在使用之前必须初始化。对于静态分配的互斥量,可以将PYHREAD_MUTEX_INITIALIZER赋给它。

静态初始值PTHREAD_MUTEX_INITIALIZER,只能用于对如下互斥量进行初始化:静态分配且携带默认属性。其他情况下,必须调用pthread_mutex_init()进行动态初始化。

#include<pthread.h>
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

According to SUSv3, applying the operations that we describe in the remainder of this section to a copy of a mutex yields results that are undefined. Mutex operations should always be performed only on the original mutex that has been statically initialized using PTHREAD_MUTEX_INITIALIZER or dynamically initialized using pthread_mutex_init() (described in Section 30.1.5).
意思是:1、mutex只能被PTHREAD_MUTEX_INITIALIZER和pthread_mutex_init()初始化。2、互斥量不能相互copy。

加锁和解锁互斥变量

初始化之后,互斥变量处于未锁定状态。

#include<pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex); //加锁
int pthread_mutex_unlock(pthread_mutex_t *mutex); //解锁

pthread_mutex_trylock()和pthread_mutex_timedlock()

互斥量的性能?

互斥量的死锁

当一个线程需要同时访问两个或多个不同的共享资源,而每个资源会被不同的互斥量持有时,如果超过一个线程加锁同一组互斥量时,就有可能发生死锁。

当锁的持有线程再次加锁时,会导致死锁或错误。

避免死锁的方法:

  1. 定义互斥量之间的层级关系。当多个线程对同一组互斥量操作时,总是应该以相同的顺序对该组互斥量进行锁定。比如,先锁定mutex1,在锁定mutex2,这样死锁就不会出现。当互斥量之间的层级关系不明确时,可以强制定义一个层级关系。
  2. 使用pthread_mutex_trylock()。如果调用失败(返回EBUSY),那么该线程释放所有已持有的锁,再等待一段时间再试。该方法效率低,很少使用。

动态初始化互斥量

静态初始值PTHREAD_MUTEX_INITIALIZER,只能用于对如下互斥量进行初始化:静态分配且携带默认属性。其他情况下,必须调用pthread_mutex_init()进行动态初始化。

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

attr用来配置互斥量的属性,在调用之前,必须初始化。当attr为NULL时使用默认属性。

SUSv3规定,初始化一个业已初始化过的互斥量,将导致未定义的行为。
如下情况必须使用函数pthread_mutex_init():

  • 动态分配于堆中的互斥量;
  • 互斥量是栈中分配的自由变量;在退出自由变量的作用域是必须销毁该互斥量
  • 初始化静态互斥量时,不想使用默认属性。

不再使用的自动或动态互斥量时,应使用pthread_mutex_destory()将其摧毁。静态互斥量无须调用。

#include<pthread.h>
int pthread_mutex_destory(pthread_t *mutex);

只有当锁处于未锁定状态,且后续也无任何线程企图锁定时,将其销毁才是安全的。

对于动态互斥量,应先销毁,再释放(free)。
经由pthread_mutex_destory()销毁的互斥量,可调用pthread_mutex_init()对其创新初始化。

互斥量的属性

  • 类型
  • 其他

互斥量的类型

一般来说:

  • 在使用之前必须初始化;
  • 同一线程不能对同一互斥量加锁两次;
  • 线程不应对不是自己加锁的互斥量解锁;
  • 线程不应对尚未锁定的互斥量解锁。

准确的说,上述的结果取决于互斥量的类型(type)。SUSv3定义了一下类型:

  1. PTHREAD_MUTEX_NORMAL
  2. PTHREAD_MUTEX_ERRORCHECK
  3. PTHREAD_MUTEX_RECURSIVE
  4. PTHREAD_MUTEX_DEFAULT

pthread_muteattr_t mtxAttr;
pthread_muteattr_init(&mtxAttr);
pthread_muteattr_settype(&mtxAttr, PTHREAD_MUTEX_ERRORCHECK);

Signaling Changes of State: Condition Variables

A mutex prevents multiple threads from accessing a shared variable at the same time. A condition variable allows one thread to inform other threads about changes in the state of a shared variable (or other shared resource) and allows the other threads to wait (block) for such notification.
互斥量防止多个线程同时访问共享变量。条件变量允许一个线程就某个共享变量(或共享资源)的状态变化通知其他线程,并让其他线程等待这个通知。
条件变量总是结合互斥量使用。条件变量就共享变量的状态改变发出通知,而互斥量则提供对该共享变量的访问的互斥。

静态条件变量

条件变量与互斥量类似,都有两种初始化方式,且在使用之前必须初始化。

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

According to SUSv3, applying the operations that we describe in the remainder of this section to a copy of a condition variable yields results that are undefined. Operations should always be performed only on the original condition variable that has been statically initialized using PTHREAD_COND_INITIALIZER or dynamically initialized using pthread_cond_init() (described in Section 30.2.5).

通知和等待

条件变量的主要操作是通知(signal)和等待(wait)。通知是指:通知一个或多个处于等待状态的线程,某个共享变量的状态已经改变;等待是指:收到一个通知之前,线程处于阻塞状态。

#include<pthread.h>

int pthread_cond_signal(pthread_cond_t *cond); //唤醒被cond阻塞的线程,保证至少唤醒一条线程
int pthread_cond_broadcast(pthread_cond_t *cond);//唤醒所有线程
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);//阻塞线程,知道收到通知
int pthread_cond_timewait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);

在唤醒时,就算没有阻塞的线程,也没有影响。
函数pthread_cond_wait()执行的操作:

  • 解锁互斥量mutex;
  • 阻塞调用线程,直至另一个线程就条件变量发出信号;
  • 重新锁定互斥量mutex。
    前两步属于同一原子操作。

如何把条件变量与互斥量结合起来:

  • pthread_mutex_lock(&mtx);
  • while(检查共享变量的状态是否为我们想要的,如果不是,则执行下面的语句)
    {
    //如果不是
    pthread_cond_wait(&cond, &mtx); //等待被唤醒
    }
  • pthread_mutex_unlock();

生产者和消费者问题

#include<pthread.h>
#include<errno.h>
#define N 10 //存储空间的大小

static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
static int avail = 0;

//生产者代码片段
pthread_mutex_lock(&mtx);
while(avail >= N)	//空间已满,等待消费者取走数据
	pthread_cond_wait(&cond, &mtx);
//do somthing, 往存储空间中放入数据
avail++;
pthread_mutex_unlock(&mtx);
pthread_cond_signal(&cond); //唤醒消费者

//消费者代码片段
pthread_mutex_lock(&mtx);
while( avail == 0 )	//没有数据
{
	pthread_cond_wait(&cond, &mtx); //等待生产者放入数据
}
while( avail > 0 )
{
	//Do something with product unit做一些与产品有管的事
	avail-;
}
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mtx);

条件变量的动态分配

条件变量的动态分配与互斥量的动态分配几乎一样:

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
int pthread_cond_destory(pthread_cond_t *cond); //先销毁再释放;自动变量在其生命结束时,应先销毁。

线程安全

若函数可以同时被多个线程安全调用,则称该函数是线程安全函数(thread-safe)。反之,如果函数不是线程安全的,则不能并发调用。导致线程不安全的典型原因是使用了在所有线程之间共享的全局或静态变量。
实现线程安全的方法:

  • 方法一:将共享变量与互斥量关联使用。在访问共享变量时,加锁,访问完解锁。如果函数库中的所有函数都访问了该共享变量,则应该在所有库函数的临界区加锁。
  • 方法二:如果需要使用静态变量存储信息,则可以替换成有调用者分配的缓冲区。
  • 方式三:使用线程特有数据技术。

一次性初始化(one-time initialization)

有些初始化动作只需要执行一次。例如,互斥量的初始就必须只能执行一次。
通过pthread_once()来实现一次性初始化:

#include<pthread.h>
int pthread_once(pthread_once_t *once_control, void (*init)(void));

利用参数once_control的状态,函数pthread_once可以确保无论有多少线程对pthread_once()调用了多少次,也只会执行一次由init指向的函数。
参数once_control指向一个被初始化为PTHREAD_ONCE_INT静态变量。

线程特有数据TSD(thread-specific data)

Thread-specific data is a technique for making an existing function thread-safe without changing its interface. A function that uses thread-specific data may be slightly less efficient than a reentrant function, but allows us to leave the programs that call the function unchanged.

Thread-specific data allows a function to maintain a separate copy of a variable for each thread that calls the function, Thread-specific data is persistent; each thread’s variable continues to exist between the thread’s invocations of the function. This allows the function to maintain per-thread information between calls to the function, and allows the function to pass distinct result buffers (if required) to each calling thread.
线程特有数据是长期存在的。

线程局部存储

未完待续。。。

线程取消

向线程发送请求,要求线程立即退出。

取消一个线程

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

发送请求后,函数立刻返回,不会等待目标线程的退出。

线程清理函数

  • 如果线程在取消点草草结束,可能会导致全局共享变量处于不一致状态,也可能会导致死锁。
  • 每个线程都有一个清理函数栈。当线程遭到取消时,会沿着这个栈自顶向下依次执行。

向线程发送信号

#include<signal.h>
int pthread_kill(pthread_t thread, int sig);

向同一进程中的线程发送信号。

  • 信号处理函数是进程级的;
  • 信号掩码是线程级的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值