Linux操作系统——线程

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/weixin_40797414/article/details/82283944

线程:在一个程序里的一个执行路线就叫做线程。更准确的定义是:线程是一个进程内部的控制序列。
一切进程至少都有一个执行线程。
进程和线程:
进程是资源竞争的基本单位。
线程是程序执行的最小单位。、
线程共享进程数据,但也拥有自己的一部分数据:线程ID,一组寄存器,栈,errno,信号屏蔽字,调度优先级。
进程的多个线程共享
同一地址空间,因此Text Segment,Data Segment都是共享的,如果定义一个函数,在各线程中都可以访问到,各线程中都以调用,如果定义一个全局变量,在各线程中都可以访问到,初次之外,各线程还共享以下进程资源和环境。
文件描述符表
每种信号的处理方式(SIG_IGN.SIG——DFL或者自定义的信号处理函数)
当前工作目录用户id和组id
线程的优点:创建一个新线程的代价要比创建一个新进程小的多,与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。
线程占用的资源比进程少很多。
能充分利用多处理器的可并行数量。
在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
线程的缺点
性能损失:
一个很少被外部事件阻塞的计算密集型线程往往无法与共享它的线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
健壮性降低:
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。

缺乏访问控制:进程是访问控制的基本粒度在一个线程中调用某些OS函数会对真个进程造成影响。
编程难度提高:
编写与调试一个多线程程序比单线程程序困难得多。
int pthread_create(pthreate_t thread,const pthread_attr_t *attr,void (start_rutine)(void),void *arg)
thread:返回线程ID
attr:设置线程属性,attr为NULL表示使用默认属性。
start_routine:是个函数地址,线程启动后要执行的函数。
arg:传给线程启动函数的参数
返回值:成功返回0,失败返回错误码。

错误检查:传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)而是将错误代码通过返回值返回。
pthreads同样也提供了线程内errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值判定,因为读取返回值要比读取线程内的errno变量的开销更小。
进程ID和线程ID
在Linux中,目前的线程实现是Native POSIX Thread Libaray,简称NPTL。在这种实现下,线程又被称为轻量级进程,每一个用户态的线程,在内核中都对应一个调度实体,也拥有自己的进程描述符。
没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID。但是引入线程概念之后,情况发生了变化,一个用户进程下管理N个用户态线程,每个线程作为一个独立的调度实体在内核态都有自己的进程描述符,进程和内核的描述符一下子就变成了1:N关系,POSIX标准又要求进程内的所有线程调用getpid函数时返回相同的进程ID。
多线程的进程,又被称为线程组,线程组内的每一个线程在内核之中都存在一个进程描述符与之对应。进程描述符结构体中的pid,表面上看对应的是进程ID,其实不然,它对应的是线程ID;进程描述符中的tgid,含义是Thread Group ID,该值对应的是用户层面的进程ID
ps -L:显示线程ID,线程组内线程的个数。
强调一点:线程和进程不一样,进程有父进程的概念,但在线程组里面,所有的线程都是对等关系。
线程ID及进程地址空间布局
pthread_create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
pthread_create函数产生并标记在第一个参数指向的地址中的线程ID中。属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
线程库NPTL提供了pthread_selt函数,可以获得线程 自身的ID。
pthread_t pthread_self(void);
pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。
线程终止:从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit.
线程可以调用pthread_cancel终止同一进程中的另一个线程。
pthread_exit函数
void pthread_exit(void *value_ptr);
value_ptr:value_ptr不要指向一个局部比变量。
无返回值,跟进程一样,线程结束的时候无法返回到它的调用者。
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
int pthread_cancel(pthread_t thread);
取消一个执行中的线程。
成功返回0,失败返回错误码。
线程等待与分离
为什么需要线程等待?
已经退出的线程,其空间没有释放,仍然在进程的地址空间内。
创建新的线程不会复用刚才退出线程的地址空间。
int pthread_join(pthread_t thread,void** value_ptr_
value_ptr:它指向一个指针,后者指向线程的返回值。
成功返回0,失败返回错误码。
调用该函数的线程将挂起等待,知道id为thread的线程终止,thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的。
如果thread线程通过return 返回,value_ptr所指向的单元里存放的是thread线程函数的返回值。
如果thread线程被别的线程嗲用pthread_cancel异常终止掉,value_ptr所指向的单元里存放的是常数PTHREAD_CANCELED;
如果thread线程是自己调用pthread exit终止的,valueptr所指向的单元存放的是传给pthread_exit的参数。
如果对thread线程的终止状态不感兴趣,可以传NULL给value_ptr参数。
分离线程
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源 。从而造成系统泄漏。
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
int pthread_detach(pthread_t thread);
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离。
pthread_detach(pthread_self());
joinable和分离是冲突的,一个线程不能即是joinable有时分离的。
线程同步与互斥
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个 线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
多个线程并发的操作共享变量,会带来一些问题。
互斥量的接口
初始化互斥量:
静态分配:
pthread_mutex_t mutex=PTHREAD_MTEX_INITIALIZER
动态分配:int pthread_mutex_init(pthred_mutex_t restrict mutex,const pthread_mutexattr_t restrict attr);
mutexL:要初始化的互斥量
attr:NULL;
销毁互斥量:使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要销毁。
不要销毁一个已经加锁的互斥量。
已经销毁的互斥量,要确保后面不会有线程再尝试加锁。
int pthread_mutex_destory(pthread_mutext_t *mutex);
互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
成功返回0,失败返回错误号。
调用pthread_lock时,可能会遇到以下情况:
互斥量处于没有锁状态,该函数会将互斥量锁定,同时返回成功。
发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_lock调用会陷入阻塞,等待互斥量解锁。
条件变量:当一个线程互斥的访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列的这种情况就需要用到条件变量。
int ptread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
cond:要初始化的条件变量
attr:NULL
intpthread_cond_destroy(pthread_cond_t *cond)
等到条件满足
int pthread_cond_wait(pthread_cond_t*restrict cond,pthread_mutext_t *restrictx);
cond:要在这个条件变量上等待。
mutex:互斥量。
唤醒等待:
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
为什么pthread_cond_wait需要互斥量?
条件等待是线程同步间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。
POSIX信号量:POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源的目的。但可以用于线程间同步。
初始化信号量

#include<semaphore.h>
int sem_init(sem_t *sem,int pshared,unsigned int value);
pshared:0 表示线程间共享,非0表示进程间共享。
value:信号量初始值
int sem_destroy(sem_t *sem)
销毁信号量
等待信号量
int sem_wait(sem_t *sem);
发布信号量
int sem_post(sem_t *sem);
读写锁:在编写多线程的时候,有一种情况是十分常见的,那就是,有些公共数据修改的机会比较少,相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找操作,中间耗时很长,给这种代码段加锁,会极大的降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况,那就是读写锁。
注意:写独占,读共享,写锁优先级高。
读写锁接口:
int pthread_rwlock_init(pthread_rwlock_t* restrict rwlock,const pthread_rwlock_t *restrict attr);
销毁:
int pthread_rwlock_destory(pthread_rwlock_t *rwlock);
加锁和解锁
int pthread_rwlock_rdlock(pthread_rwlock_t*rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t*rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t &rwlock);

猜你喜欢

转载自blog.csdn.net/weixin_40797414/article/details/82283944