专题一 Linux下多线程编程——使用Pthread线程库

来源:互联网 发布:python web开发 编辑:程序博客网 时间:2024/06/06 02:08

专题一 多线程编程

线程包含哪些信息?两部分来源,一是所属于的进程的资源,一部分是线程自己的上下文信息

一个进程的所有信息对该进程的所有线程都是共享的,包含五个段(可执行程序的代码,程序的全局内存和堆内存,栈)以及文件描述符。

每个线程都含有表示执行环境所需的信息:线程ID,一组寄存器值,栈,调度优先级和策略,信号屏蔽字,errno变量以及线程私有数据。

1 线程创建与终止

线程标识

#include<pthread.h>int pthread_equal(pthread_t tid1, pthread_t tid2);返回非0表示相等,否则,返回0,表示不相等pthread_t pthread_self();返回调用线程的线程ID

1.1 线程创建

#include <pthread.h>int pthread_create(pthread_t *tidp, const pthread_attr_t *attr, void *(*start_rtn)(void*), void *arg);返回值:返回0成功,否则,返回错误编号注意,4个参数全为4个指针,依次是pthread_t(线程标识),线程属性(pthread_attr_t),线程启动函数以及对应的参数
A 线程创建时不能保证哪个线程先执行,是调用进程还是子进程

B 新子线程可以访问进程的地址空间(五个段+fd) + 继承调用线程的浮点环境和信号屏蔽字,特别注意线程的挂起信号集被清除。

1.2 线程终止

单个线程中来终止整个进程的两个方法:A 任意线程调用exit,_Exit,_exit   B 信号:默认动作为终止整个进程的信号发送到线程

单个线程自己退出,不影响整个进程的三个方法:A 线程启动函数自己正常return B 线程启动函数调用pthread_exit C 线程被同一进程内的其他线程取消 pthread_cancel

AB都是自己干掉自己,C则是被队友干掉了。

1.2.1 pthread_exit单个线程退出

#include<pthread.h>void pthread_exit(void *rval_ptr);
这个exit返回的rval_ptr的void类型的指针用pthread_join来取走,当然这个参数也可以设为NULL,如果不想要返回值的话

#include<pthread.h>int pthread_join(pthread_t thread, void **rval_ptr);
看到join的其实就是阻塞在等待某个线程的结束。一个指定的单个线程有3种退出方法,那么,当AB时能取到正常的线程函数的返回值,C这个情况在join的rval_ptr指针指定的内存单元中将为PTHREAD_CANCEL。

1.2.2 pthread_cancel取消同一进程中的其他单个线程

#include<pthread.h>int pthread_cancel(pthread_t tid);
默认情况下效果上等同于tid这个线程自己调用了pthread_exit(PTHREAD_CANCEL);当然,tid自己也可以设置如何被取消或者忽略被别的线程取消。

那么究竟tid如何自己决定自己如何被取消呢?(进程可以自己通过atexit()来注册退出时要执行的函数,同理,线程也有)

线程清理处理程序(thread cleanup handler): A 一个线程可以建立多个清理处理程序 B 多个线程清理处理程序以栈保存,执行为注册的反序

#include<pthread.h>void pthread_cleanup_push(void (*rtn)(void *), void *arg); // 参数无非就是一个函数指针,一个参数指针void pthread_cleanup_pop(int execute);
清理函数rtn是被pthread_cleanup_push调用执行的,什么时候执行呢?

A 线程自己调用pthread_exit或者直接return B 线程被同一进程内的其他线程pthread_cancel C 非0参数执行pthread_cleannup_pop();

当pthread_cleanup_pop的参数为0时,都将删除上次pthread_cleanup_push建立的那一个清理处理程序

特别注意:因为pthread_cleanup_push与pthread_cleanup_pop是宏定义实现的,所以必须要从语法层面实现两两配对,否则直接编译错误。

所以经常需要用If条件语句,如

if(0)   pthread_cleanup_pop();if(0)   pthread_cleanup_pop();……
来完成宏定义的括号的配对等。必须写出的,注意if下面还不要加括号。

1.3 线程函数和进程函数的相似

fork pthread_create

exit phtread_exit

waitpid pthread_join

atexit pthread_cancel_push

getpid pthread_self

abort pthread_cancel

1.4 线程分离

默认情况下(线程没有分离),线程的终止状态会保存直到对这个pthread_t调用pthread_join()。

如果线程已经分离,线程的底层存储资源可以在线程终止时立即被收回。在分离以后,不能再使用pthread_join()来等待线程终止

#include<pthread.h>int pthread_detach(pthread_t tid);成功返回0,失败返回错误编编号


2 线程同步(5个基本的同步机制)

多线程共享访问同一块内存时,如果存在读写并存或者写写并存,就会造成”数据不一致“的问题。

引起数据不一致的根本原因有两个:

A 体系结构对数据的修改操作不是在一个存储器访问周期内,此时读与写的周期交叉时,会出现—> 解决方法,修改操作改为原子操作

B 程序使用变量的方式,测试变量与根据测试结果来进行加减的操作不是一个原子操作

2.1 互斥量mutex

本质上就是一把锁。注意只有把所有的线程都设计成遵守相同数据访问规则的,互斥机制才能正常工作,如果有线程不是按规则来,那么它可以直接访问资源。

pthread_mutex_t 表示互斥量。如何用呢?

2.1.1 初始化(可以静态初始化,也可以动态初始化)

#include<pthread.h>int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); // attr参数可以使用NULL来使用默认值int pthread_mutex_destroy(pthread_mutex_t *mutex);返回0 成功;否则返回错误编号

注意:A 静态分配的互斥量(直接定义的变量,没有new或者malloc等的),可以直接设为常量PTHREAD_MUTEX_INITIALIZER

B phtread_mutext_init()函数可以对静态动态都行,如果是动态特别记得释放内存前必须调用pthread_mutex_destroy();

2.1.2 加锁和解锁

#include<pthread.h>int pthread_mutex_lock(pthread_mutex_t *mutex); // 没锁住就会阻塞int pthread_mutex_trylock(pthread_mutex_t *mutex); // 没锁住直接返回EBUSY,不阻塞int pthread_mutex_timedlock(pthread_mutex_t *mutex, const struct timespec *tsptr); // 没锁住阻塞,阻塞超时后将直接返回ETIMEDOUT,注意tsptr是绝对时间(等到具体几点),不是等从现在算起多少秒那种int pthread_mutex_unlock(pthread_mutex_t *mutex);成功返回0,否则,返回错误编号

在Linux编程中经常要对时间进行编程,下面这个是将系统的实时时间转换成为人类可读的形式输出的一个代码段。  struct timespec tout;  struct tm *tmp;  char buf[64];  clock_gettime(CLOCK_REALTIME, &tout); // struct timespec;  tmp = localtime(&tout.tv_sec); // struct tm;  strftime(buf, sizeof(buf), "%r", tmp);  printf("current time is %s\n", buf);

2.2 读写锁(又叫共享互斥锁)

2.2.1 读写锁是什么     互斥锁对于读写的一种改进

互斥锁两种状态:加锁,没锁。读写锁三种状态:加读锁(共享模式),加写锁(互斥模式,此时完全就是互斥锁),没锁。

当读写锁在读加锁状态时,注意两个要点:

A 所有以读模式对它加锁的线程都可以得到访问权,当然前提是此时没有写线程想要加锁,因为,为了防止写线程饿死必须在写模式锁请求加锁之后,阻塞后来的读模式的锁请求

B 任何写模式的锁请求全部阻塞

2.2.2 读写锁怎么用

A 初始化与销毁

#include<pthread.h>int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);成功返回0;否则,返回错误编号
必须初init,释放底层内存前必须destroy,因为在init的过程中为读写锁分配了资源。对静态分配的读写锁也可以使用PTHREAD_RWLOCK_INITIALIZER常量直接赋值。

B 加与解读写锁

#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); // 不能加读锁时,立即返回EBUSY,不阻塞int pthread_rwlock_trywrlock(pthread_wrlock_t *rwlock); // 不能加写锁时,立即返回EBUSY,不阻塞int pthread_rwlock_timedrdlock(pthread_wrlock_t *rwlock, const struct timespec *tsptr);int pthread_rwlock_timedwrlock(pthread_wrlock_t *rwlock, const struct timespec *tsptr); // 绝对时间成功返回0;否则,返回错误编号

2.3 条件变量

先看个代码,通常使用一个if来判断一个条件并根据条件的值来决定下一步的操作。

if(condition如i == 1) { <- test          <--------------- 其他线程可以在这里插入来改变i变量的值,这就是一个窗口  做些事情,用sum *= i;  <- set}
但是,在多线程环境中可能在判断condition的时候,条件是符合的,但是在判断完之后i可能被其他线程给改变了,这样语句块内的sum的计算是错误的。

这个问题的根本原因就在于test-and-set是一个可以分开的操作,而不是一个原子操作。

条件变量是用一个互斥锁来锁住一个条件。互斥锁先锁住条件变量然后进行条件检查,检查条件未发生则休眠,再释放互斥锁。等待其他线程来获取互斥锁来改变条件,再发送信号给阻塞在这个条件变量的线程。此时,线程被唤醒,唤醒之后会再锁住条件变量的互斥锁。再检查条件变量的值,注意在线程被唤醒之后,一定要重新计算条件,因为另一个线程可能已经在运行并改变了条件。一定要保证程序的逻辑正确。

2.3.1 init和destroy

include<pthread.h>int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);int pthread_cond_destroy(pthread_cond_t *cond);成功返回0;否则返回错误编号。
条件变量pthread_cond_t如果是静态分配的则可以用PTHREAD_COND_INITIALIZER赋值。

2.3.2 条件变量的wait

#include <pthread.h>int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); // 特别注意这个mutex是由函数的调用者锁住之后传入的,是用来保护互斥访问ptheread_cond_t的int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *tsptr); //带定时的,如果超时了条件还没有出现,重新获取互斥量,返回ETIMEDOUT成功返回0; 否则,返回错误编号
特别注意,返回时,互斥量将重新被锁住;调用成功时线程一定要编程去重新计算条件!!!!一般的写法如下:

while(数据依然不满足要求) {    pthread_cond_wait(&条件变量,&mutex);}// 通过这种写法就可以检查了条件是否真的已经发生变化了
2.3.3 条件变化之后要通知线程,条件已经满足

#include <pthread.h>int pthread_cond_signal(pthread_cond_t *cond); // 至少能唤醒一个等待该条件的线程int pthread_cond_broadcast(pthread_cond_t *cond); // 唤醒等待该条件的所有线程成功返回0;否则,返回错误编号
给线程发信号,必须注意,一定要在改变条件状态之后再给线程发信号!!!!

2.4 自旋锁 内核中很有用,但在用户层,并不是非常有用

自旋锁使用的两个前提:A锁被持有的时间很短 B 线程不希望在重新调度上花费太多成本

自旋锁就是通过在获取锁之前一直忙等(自旋)阻塞状态。就是说线程在没能锁住这个锁的情况下一直在那运行忙等,这样浪费CPU时间。

在分时可抢占的操作系统的用户态下,线程在两种情况下可以被取消调度: 1 线程的时间片到(分时系统) 2 更高调度优先级的线程就绪变为可运行(抢占)

在这两种情况下,如果线程拥有自旋锁同时休眠了,那么其他线程如果在等这个自旋锁的话,他们都会一直自旋,这极大地浪费CPU时间。

2.5 屏障

barrier协调多个线程并行工作,允许每个线程(任意数量的线程)等待,直到所有的合作线程都达到某一个执行点,然后从该点继续执行,而这些线程不需要退出。

pthread_join是一种屏障,但是只是一个线程等待另一个目标线程的情况。

2.5.1 init与destroy

#include<pthread.h>itn pthread_barrier_init(pthread_barrier_t *barrier, const pthread_barrierattr_t *attr, unsigned int count); // 为屏障分配资源int pthread_barrier_destroy(pthread_barrier_t *barrier); // 释放相应分配的资源成功返回0;否则,返回错误编号
count参数指定在所有线程继续运行之前,必须达到屏障的线程数目,使用attr参数指定屏障对象的属性,若为NULL,则用默认属性初始化屏障。

2.5.2 屏障如何用?

#include<pthread.h>int pthread_barrier_wait(pthread_barrier_t *barrier);成功返回0或者PTHREAD_BARRIER_SERIAL_THREAD(返回这个的线程有且仅有一个,可以作为主控线程);否则,返回错误编号
使用这个wait会使调用线程当屏障计数(前面的count)没达到要求的时候阻塞,休眠。一个(有且仅有一个)任意线程返回PTHREAD_BARRIER_SERIAL_THREAD,其余线程全部返回的是0。

屏障在达到屏障计数值(count)之后,线程没有再处于阻塞状态后,屏障就可以被重用了。但是他的count不会改变。

3 线程属性以及线程同步属性

3.1 线程的限制

类Unix系统有很多,我们把Linux也看做是一种。为了在这些不同的系统之间保证软件的可移植性,Unix系统实现了很多幻数和常量作为一种限制。分为两类限制:编译时限制和运行时限制。

系统提供了3种获取不同的限制的方式:

A 编译时限制(头文件提供),如ISO C的编译时限制在<limits.h>

B 运行时限制(与文件或目录无关的)sysconf函数

C 运行时限制(与文件或目录相关的)pathconf和fpathconf函数

#include<unistd.h>long sysconf(int name); // name参数用于标识系统限制,以_SC_打头的常量long pathconf(const char *pathname, int name); // 这两个的name参数为_PC_打头的常量long fpathconf(int fd, int name);成功返回相应值;出错返回-1,若是name参数不对,则errno为EINVAL,不确定的值只返回-1,不设置errno
常用的代码写法示例:

if((val = sysconf(_SC_OPEN_MAX)) < 0) {        if(errno != 0) {            if(errno == EINVAL) {                fputs(" (not supported)\n", stdout);            } else {                fputs("sysconf error\n", stderr);                exit(1);            }        } else {            fputs(" (no limit)\n",stdout);        }    } else {        printf(" %ld \n", val);    }

3.2 线程属性 以及同步属性

线程属性也好,同步属性也好,是pthread用来给用户微调线程和同步对象的行为的。全遵循一个相同的模式:

A 每一个对象与它自己类型的属性对象关联。一个属性对象包括多个属性。属性对象对应用透明,应用程序通过相应的4个函数来管理属性对象(面向对象思想)。下面看这4个函数

B init 初始化函数,把属性设置为默认值

C destroy 销毁属性对象函数,销毁函数负责释放init函数分配的与属性对象关联的资源

D get 每个属性都有一个从属性对象中获取属性值的函数

E set 每个属性都有一个设置属性值的函数,属性值参数按值传递

3.2.1 线程属性

#include<pthread.h>int pthread_attr_init(pthread_attr_t *attr);int pthread_attr_destroy(pthread_attr_t *attr);成功返回0;否则,返回错误编号
3.2.2

4 线程私有数据


5 线程与其他系统调用的关系(进程相关的如信号,fork,IO等)



0 0