Linux基础知识(八)

本篇介绍一些Linux线程相关内容.

1. 多线程管理

进程是具有独立功能的程序在某个数据集合上的一次运行活动, 进程是系统进行资源分配和调用的独立单位

线程是进程的一个实体, 它是竞争分配 CPU 的基本单位

进程和线程的区别

(1) 线程是进程的一部分, 也被称作轻量级的进程
(2) 进程资源分配的基本单位, 线程是 CPU 调度的基本单位
(3) 线程的创建比进程的创建系统开销小

网络服务器程序通常需要处理多个客户端的请求, 使用的并发的技术无非就是多进程和多线程

多进程技术存在很多的局限性, 比如: 要分配大量的资源, 进程 PID, 代码段,数据段,堆区, 栈区..., 其系统消耗是非常大的, 会严重影响服务器的处理速度, 因此在网络编程过程中越来越多的使用线程技术, 实现并发处理

线程是一种轻量级的进程, 对系统资源的需求非常少, 线程隶属于某个进程, 进程内部可以由很多线程, 线程共享进程的资源, 比如: 代码段 , 数据段, 堆区, 环境变量表, 文件描述符...

每个线程只需要建立一个独立的栈区就可以了(除栈区外的其他内容共享)
每个进程内部都至少有一个线程, 叫做主线程( main() ), 主线程一旦结束, 进程随之结束, 进程结束该进程所拥有的所有线程
每个线程的内部到码都是顺序执行的, 多线程之间的代码乱序执行

线程并行的原理: 宏观上并行, 微观上串行

***** 进程是资源分配的单位, 如代码区 数据段等
***** 线程是竞争分配 CPU 的基本单位

2. linux/unix 关于多线程的编程

POSIX (可移植操作系统接口) 标准 : 是IEEE为要在各种UNIX操作系统上运行软件,而定义API的一系列互相关联的标准的总称

unix/linux 系统中提供的系统调用遵循了 POSIX 标准形式,因此系统中给应用工程师提供的系统函数的API ( 应用程序编程接口), 函数名, 参数是统一的, 只要是在 unix/linux 系统, 都能通用

2.1 多线程的创建

Linux基础知识(八)

编码时使用头文件<pthread.h>, 编译链接时使用共享库 pthread/<pthread.h>

int pthread_create ( pthread_t *thread, const pthread_attr_t *attr, 
void *(*start_routine) (void *), void *arg);
thread : 指向线程标识符的指针, 返回创建的新线程的 ID
ID 是线程的唯一标识, 本质就是一个正整数, 是一个传出参数
attr : 创建新线程时可以指定线程的某些属性
一般给 0( NULL) 即可, 代表使用默认属性
start_routine : 函数指针, 指向了线程执行的主体, 线程运行函数的地址
arg : 运行函数的参数, 传给 start_routine 的参数
返回值 : 成功返回0, 失败返回错误编码

注意: 在多线程中尽量不要使用 errno, 因为获取的 errno 有可能不是错误的原因

ps -Lf + PID : 查看对应的进程中有几个线程

2.2 多线程的运行

2.2.1 获取线程的 ID

pthread_t pthread_self ( void );
pthread_self() : 获取线程的 ID, 返回值就是线程的 ID
成功返回调用线程的 ID, 这个函数不会失败

主线程退出, 会导致进程退出, 致使子线程得不到执行, 这种情况实际编程过程中要避免出现, 如果出现该情况, 它属于编程逻辑问题, 编译器检出不出来

2.2.2 线程间的线程传递

(1) 全局变量

(2) ptread_create 函数的第四个参数

2.3 多线程的结束

正确的应该先是子线程结束, 然后后结束主线程

2.3.1 主线程如何监控到子线程的结束

pthread_join 函数

int pthread_join ( pthread_t thread, void **retval );
作用 : 阻塞的方式等待thread指定的线程结束,当函数返回时,被等待线程的资源被收回,如果线程已经结束,那么该函数会立即返回
thread : 参数为被等待的线程标识符
retval : 用户定义的指针,它可以用来存储被等待线程的返回值。
返回值 : 成功返回0, 失败返回错误编码

2.3.2 子线程结束时的返回值问题

如何得到子线程的返回值

(1) 全局变量, 把子线程的返回值放入某一个全局变量, 主线程再去查看

(2) 通过线程函数的返回值

线程函数通过强转得到的( void*)的返回值会传给 pthread_join 的第二个参数,当 pthread_join()函数第二个参数不为 0 的情况下

(3) 通过 pthread_create 函数的第四个参数

2.3.3 子线程是如何结束的

(1) 线程的主体函数中执行 return

(2) pthread_exit 函数

void pthread_exit (void*);
如果在线程主体函数中执行return (void *)-1 等价于 pthread_exit ((void*)-1)

(3) 在进程的任何位置执行 exit 都会导致整个进程的结束

2.4 线程资源的回收

线程的资源 : 栈空间 线程的返回值

如何处理线程资源

(1) 置之不理

等到进程结束后自动回收, 该线程资源什么时候被回收是不确定的

(2) pthread_join 函数

pthread_join 函数等待特定线程的结束( 不管 pthread_join 的第二个参数是否为 0),只要该函数返回, 结束线程的资源就会被立刻回收

(3) pthread_detach 函数

将某个线程设置为分离状态, 设置为分离状态的线程一旦结束, 占据的资源会被立刻回收

因为 pthread_join 函数要回收子线程资源的话, 就必须要等待子线程的结束, 那么主线程就得等待, 如果此时主线程还有其他工作要完成, 这样就会浪费资源

int pthread_detach ( pthread_t thread );
thread : 要把哪个线程设置为分离状态
返回值 : 成功返回0, 失败返回错误编码

(4) pthread_join / pthread_detach 使用

a) 一个新创建的线程最好要么 pthread_join 要么 pthread_detach
b) 一个设置为分离状态的线程, 再去对它调用 join 就没有效果了
c) 需要等待的线程使用 pthread_join, 不需要等带的线程使用 pthread_detach

3. 线程的同步

3.1 线程同步的原因

多个线程共享数据段\BSS 段\堆区, 当定义一个全局变量 cnt, 该变量对于进程中所有线程都可见, 都可以修改 cnt, 子线程同时操作时会产生冲突, 导致 cnt 的结果不确定

3.2. 互斥量

本质就是一把锁( 可以参考文件锁)

编程步骤:

(1) 定义一个互斥量

pthread_mutex_t lock;

(2) 初始化互斥量

int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t * attr);
mutex : 指向要初始化的互斥量
attr : 互斥锁属性,NULL表示缺省属性
返回值 : 成功时返回0,失败时返回错误码

(3) 加锁临界区, 保证只有一个线程可以使用共享资源

int pthread_mutex_lock(pthread_mutex_t *mutex);
mutex : 指向要加锁的互斥锁对象
如果加锁成功, 立即返回

如果 lock 已经处于所得状态, 该函数阻塞等待, 直到 lock 处于unlock 状态, 加锁成功并返回
返回值 : 成功时返回0,失败时返回错误码

pthread_mutex_trylock()函数加锁不成功立刻返回

(4) 操作共享资源

对共享资源进行操作,此时只有当前线程在操作,因此不会发生冲突

(5) 解锁/释放锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);
mutex : 指向要解锁的互斥锁对象
返回值 : 成功时返回0,失败时返回错误码

(6) 回收互斥量/互斥锁

int pthread_mutex_destroy(pthread_mutex_t *mutex);
mutex : 指向要销毁的互斥锁对象
返回值 : 成功时返回0,失败时返回错误码

死锁产生

1) 一个线程试图对一个互斥量加两次锁
2) 线程一获取了 A 锁 再试图获取 B 锁
3) 线程二获取了 B 锁 再试图获取 A 锁, 相互之间有冲突

避免死锁

1) 加锁时按顺序加锁
2) 使用pthread_mutex_trylock()函数
线程一 trylock 获取 A 锁, 再试图 trylock B 锁, 如果不成功释放 A 锁

3.3 信号量

信号量集用于进程之间的同步操作

信号量是用于线程之间的同步操作

信号量本质上就是一个计数器(int), 可以用于控制访问共享资源的最大线程数,当信号量的值为 1 时, 等同于互斥量

信号量的编程步骤:

(1) 定义一个信号量

sem_t sem;

(2) 初始化信号量

int sem_init(sem_t *sem, int pshared, unsigned int value);
sem : 需要初始化的信号量 sem
pshared : 控制信号量的类型,
=0, 代表该信号量用于多线程间的同步
>0, 表示可以共享,用于多个相关进程间的同步
value : 要初始化的目标值, 也就是要把信号量 sem 初始化为多少

返回值 : 成功返回0, 失败返回-1, 错误信息存于errno

(3) 获取信号量

拿到信号量就可以操作共享资源, 拿不到信号量就不能操作共享资源

本质上就是给 计数-1 信号量-1
int sem_wait(sem_t *sem); /*信号量-1, 不够减的话则阻塞等待*/
int sem_trywait(sem_t *sem); /*信号量-1, 不够减返回出错信息*/
int sem_timedwait(sem_t *sem, timeout); /*信号量-1, 不够减阻塞等待, 直到够减或者等待时间超时*/

(4) 操作共享资源

(5) 释放信号量

本质上就是给 计数+1 信号量+1
int sem_post(sem_t *sem); /*指定的信号量 sem 的值加 1*/

(6) 回收信号量

int sem_destroy(sem_t *sem); /*对用完的信号量的清理*/

3.4 条件变量

生产者消费者模型

生产者向循环缓冲区放入产品,如果缓冲区满阻塞,当缓冲区不满的时继续向其中放入产品 

消费者从循环缓冲区取出产品,如果缓冲区空阻塞,当缓冲区不空的时继续从其中取出产品
循环缓冲区是共享资源, 需要使用互斥量加锁访问

条件变量是用来等待线程而不是上锁的,条件变量通常和互斥锁一起使用

条件变量之所以要和互斥锁一起使用,主要是因为互斥锁的一个明显的特点就是它只有两种状态:锁定和非锁定,而条件变量可以通过允许线程阻塞和等待另一个线程发送信号来弥补互斥锁的不足,所以互斥锁和条件变量通常一起使用

编程步骤

(1) 定义条件变量

pthread_cond_t cond;

(2) 初始化条件变量

int pthread_cond_init(pthread_cond_t *cv, const pthread_condattr_t *cattr);
cv : 要初始化的条件变量
cattr : 条件变量的属性, 通常给 0 就可以了

(3) 暂停线程, 使调用线程睡入条件变量 cond 指向的队列, 同时释放互斥锁

int pthread_cond_wait(pthread_cond_t *cv, pthread_mutex_t *mutex);
cv : 把线程放入条件变量 对应的队列
mutex : 要释放那个的互斥锁

(4) 唤醒线程/解除暂停, 把线程从条件变量指定的队列中取出

int pthread_cond_signal(pthread_cond_t *cv);
cv : 唤醒响应的线程

(5) 销毁条件变量

int pthread_cond_destroy(pthread_cond_t *cv);
cv : 要销毁的条件变量


分享到:


相關文章: