Linux系统编程之线程

线程概述

    线程是允许应用程序并发执行多个任务的一种机制,一个进程中可包含多个线程。同一程序中的所有线程都会执行相同的程序,共享一份全局内存区域,其中包括代码段、初始化数据段、未初始化数据段、以及堆内存段、栈及文件描述符。

    多线程可以弥补多进程的缺陷:

  1. 进程之间信息难以共享,而线程则可以方便快速共享信息,只需将数据复制到共享变量中即可
  2. 调用fork()创建进程代价较高,创建线程代价较低。特别是无需采用写时复制来复制内存页,也无需复制页表

    线程也有相对于进程的缺点:

  1. 多线程编程时,需要确保调用线程安全的函数,或者以线程安全的方式调用函数,多进程无需考虑这些
  2. 因为线程之间有共享的地址空间,所以单个线程出现问题可能会影响同一个进程中其他线程
  3. 每个线程都在争用宿主进程中有限的虚拟地址空间,特别是一旦每个线程栈以及线程特有数据(或线程本地存储)消耗掉进程虚拟地址空间的一部分,则后续线程则无缘使用这些区域。

线程的创建、终止与连接

#include <pthread.h>

int pthread_create(pthread_t *tidp,const pthread_attr_t *attr, void *(*start)(void*),void *arg);
//新建一个线程,线程id由tidp返回,attr是线程属性,新建的线程从start函数开始执行,参数由arg传递


void pthread_exit(void *retval);
//线程有三种方式在不终止整个进程的情况下停止线程的控制流
//(1)线程可以简单的从启动例程中返回,返回值是线程的退出码
//(2)线程可以被同一进程中其他线程取消
//(3)调用pthread_exit()

int pthread_join(pthread_t thread,void **retval);
//调用此函数的线程将一直阻塞,直到指定的线程由上述三种方式中任意一种退出,retval就是返回码
//注意:如果在一个线程栈中定义了一个结构,需要返回给调用线程,此时有可能此结构在线程栈中已经被撤销,
//当调用线程使用栈所在的内存时,就会出错(解决方法就是:如果要返回一个结构体,在线程栈中动态分配空间,或者使用全局结构)

int pthread_cancel(pthread_t tid);
//线程可以使用此函数请求取消同一个进程中的其他线程,他并不是等待线程终止,只是提出请求

pthread pthread_self(void);//用于返回自己线程id

int pthread_equal(pthread_t tid1,pthread_t tid2);//判断两个线程id是否相同(因为线程id式结构体,不能像整数一样直接比较)

int pthread_cleanup_push(void (*rtn) (void*),void *arg);

void pthread_cleanup_pop(int execute);
//线程可以在其退出时候调用线程清理处理函数,清理处理程序记录在栈中,执行的顺序与注册时候相反
//当线程执行以下三个操作时候,清理函数是由pthread_cleanup_push函数调用的
//(1)调用pthread_exit()
//(2)响应线程取消请求
//(3)pthread_cleanup_pop中execute非0时候

 

    pthread_exit()终止了线程,其返回值可以通过pthread_join()获取,如果线程并未分离,则必须使用pthread_join()来进行连接,否则会产生僵尸线程。phread_join()执行的功能类似wait,差异在于1.线程之间关系是对等的 2. 无法连接任意线程。

线程同步

  • 互斥量与条件变量

    通过互斥量和条件变量的使用,来帮助线程同步对共享资源的使用。

    互斥量是可以保证对人以共享资源的原子访问,其本质上也是一种锁,有两种状态:已锁定和未锁定。任何时候,至多只有一个线程可以锁定该互斥量,一旦线程锁定互斥量,即成为该互斥量的拥有者,只有拥有者才能解锁互斥量,这时其他线程才能继续竞争这个互斥量。

int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *attr);
//初始化
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//销毁互斥量
int pthread_mutex_lock(pthread_mutex_t *mutex);
//互斥量加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
//线程不阻塞式加锁(如果互斥量未锁住则加锁,如果互斥量锁住则返回EBUSY)
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//互斥量解锁

    条件变量允许一个线程就某个共享变量的状态变化通知其他线程,并让其他线程等待(阻塞于)这一通知。条件本来是由互斥量保护的,线程在改变条件状态前要先锁住互斥量pthread_cond_wait()执行的操作步骤如下:

  1. 解锁互斥量mutex
  2. 阻塞调用线程,直到另一个线程就条件变量发出信号
  3. 重新锁定mutex
int pthread_cond_init(pthread_cond_t *cond,const pthread_condattr_t *attr);
//条件变量初始化
int pthread_cond_destroy(pthread_cond_t *cond);
//条件变量注销
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
//wait来等待条件变量变为真
int pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t *mutex,const struct timespec *tsptr);
//与wait相似,多了一个时间限制代表愿意等待的时间长,若超时还没有响应则返回错误码
int pthread_cond_signal(pthread_cond_t *cond);
//通知线程条件以满足,能唤醒一个等待该条件的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
//能唤醒等待该条件的所有线程

    需要注意的是,pthread_cond_wait()会自动执行最后两步中对互斥量的解锁和加锁动作,等同于一个原子操作,其他线程不可能获取该互斥量,也不可能就该条件变量发出信号。

  •     互斥量和信号量的区别

(1)互斥量用于线程的互斥,信号量用于线程的同步。

这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别。

互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。

同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源

(2)互斥量值只能为0/1,信号量值可以为非负整数。

也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问。

(3)互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。

  • 读写锁

    读写锁与互斥量类似,不同的是读写锁有更高的并行性,读写锁有三种状态:读模式下加锁状态,写模式下加锁状态,不加锁状态。一次只有一个线程可占用写模式下的加锁状态,但是可以有多个线程同时占用读模式下的加锁状态。

    读写锁适合对数据读次数远远大于写的次数,因为读模式下,可以并行,写模式不行。

int pthread_rwlock_init(pthread_rwlock_t *rwlock,const pthread_rwlockattr_t *attr);
//读写锁初始化
int pthread_rwlock_destroy(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);
//解锁
  • 自旋锁

    自旋锁与互斥量相似,但他不是通过休眠使进程阻塞,而是在获取锁之前一直在自旋(忙等)。主要适用于锁被持有时间短,而且线程并不希望在重新调度上花费太多成本,通常作为底层原语实现其他锁。因为当线程自旋等待锁变为可用时,CPU不能做其他的事情,是一种CPU资源的浪费。

int pthread_spin_init(pthread_spinlock_t *lock,int pshared);
//自旋锁初始化
int pthread_spin_destroy(pthread_spinlock_t *lock);
//自旋锁销毁
int pthread_spin_lock(pthread_spinlock_t *lock);
//自旋锁加锁
int pthread_spin_trylock(pthread_spinlock_t *lock);
//尝试加锁(非阻塞)
int pthread_spin_unlock(pthread_spinlock_t *lock);
//解锁

线程安全与线程存储

    若函数可同时供多个线程安全调用,则称之线程安全函数。当使用了在所有线程之间共享的全局或静态变量时,就变成了不安全函数。实现线程安全也有多种方式。其一函数与互斥量关联使用,在调用函数时候加锁,函数返回时候解锁。这种方法访问效率较低,因为对函数的访问是串行的,另一种方法是将共享变量与互斥量关联起来,这需要确认哪些部分使用了共享变量的临界去,且仅在执行到临界区时去获取和释放互斥量。

    库函数可以通过pthread_once()实现一次性初始化,就是不管创建了多少线程,此动作只执行一次,而且无论首次被任何线程所调用,都会执行初始化动作。

    一次性初始化技术要与线程特有数据结合才更加有效。使用线程特有数据技术,可以无需修改函数接口实现已有函数的线程安全。线程特有数据使函数得以为每个调用线程分别维护一份变量的副本,线程特有数据是长期存在的,在同一个线程对相同函数的历次调用间,每个线程的变量会持续存在,函数可以向每个调用线程返回各自的结果缓冲区。

进程中线程个数

    首先要说明的是,在不同的系统中,进程个数限制与一个进程中线程的个数限制不同。对于进程来说,虚拟地址空间大小是固定的,因为进程只有一个栈,其大小不是问题。但是对于一个进程中线程而言,同样大小的虚拟地址空间必须被所有线程栈共享,如果一个进程中使用很多线程,当线程栈累计大小超过可用的虚拟地址空间时候,就会出问题。除此之外,如果在线程中调用的函数有很深的栈帧,也会发现泄露。

    在Linux32位的系统中,虚拟地址空间的大小为4GB, 内核进程和用户进程所占的虚拟内存比例是1:3,所以原理上说线程的最大数量=3GB (用户虚拟地址空间)/线程栈空间大小。(用命令ulimit -a可以看到进程数、线程栈空间大小等限制)

  如果一个多线程的程序中的某个线程调用了fork函数,新创建的子进程只拥有一个执行线程(即调用fork的那个线程的完整复制),而不会在子进程中拥有与父进程相同数量的线程。

线程的私有数据

    (1)由于一个程序中的多个线程共享同一地址空间,因此代码段,数据段内容是共享的。除此之外,以下内容也是共享的:文件描述符表,每种信号的处理方式(SIG_IGN、SIG_DFL或者自定义的信号处理函数),当前工作目录,用户id和组id。但有些资源是每个线程各有一份的,如:线程id,上下文(包括各种寄存器的值、程序计数器和栈指针),栈空间(注意:栈是线程私有的,堆同一个进程中线程共享的),errno变量,信号屏蔽字,调度优先级等。

    (1)为什么需要设定一些数据为线程的私有?第一,有时候需要维护基于每个线程的数据,例如线程ID作为标识,可以防止线程之间数据混淆,第二,这种私有数据提供了让进程的接口适应多线程的编程环境,最典型的就是errno,当每个线程在执行自己的逻辑流的时候,如果发生异常就会赋值errno,但是如果errno是一个共享的数据,就无法判断每个线程会出现什么问题。

    (2)线程中的私有数据采用一键多值的技术,这个键用于获取对线程特定数据的访问。这个键可以被进程中的所有线程使用,但每个线程把这个键与不同的线程私有数据地址进行关联

参考 《APUE》、《TLPI》以及博客

博客https://www.cnblogs.com/alinh/p/6905221.html

博客https://blog.csdn.net/caoyan_12727/article/details/52280604

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值