Linux系统编程(5)

Linux系统编程(5)——线程

文章目录


前言

这是我自己的一个Linux系统编程学习路上的一个学习笔记,学习的过程中看过一些视频+博客,所以在学习过后根据记录的笔记来完成代码实现的过程中,可能会出现一大段文章内容和别人写的一样或者某些思想也会相同,如有侵权,请联系删除或者添加引用。(本文章不会作为商业用途)

一、线程是什么?

线程: LWP(light weight process),轻量级进程,本质仍然是个进程(仅限于Linux操作系统成立)。
区别: 进程有独立的地址空间,拥有PCB。线程也拥有独立的PCB,但是没有独立的地址空间,它使用的是共享地址空间。在 Linux 操作系统下,线程是最小的执行单位,进程是最小分配资源的单位,可看成只有一个线程的进程。

ps -lf pid 指令查看指定某PID进程下的 LWP 线程号。

线程的优缺点:
1.优点:提高了程序的并发性、开销小、数据通信和共享数据方便。
2.缺点:库函数不稳定、gdb不支持调试、不支持信号。
总的来说,线程的优势非常显著,缺点对整个程序的影响不是很大。

二、使用线程的一些函数

pthread_t pthread_self(void);
//获取当前进程的 ID

 int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);
//创建线程
//thread:传出参数,创建成功后返回该线程的id。
//attr:设置线程属性,一般传NULL。使用系统默认属性。
//void *(*start_routine) (void *):线程真正工作的函数
//arg:线程工作函数的参数

 void pthread_exit(void *retval);
 //将当前单个线程退出
 //retval:表示线程退出状态,通常传NULL。
 exit(0); //退出整个进程,该进程下所有的线程都会退出
 return ; //返回到调用位置

 int pthread_join(pthread_t thread, void **retval);
 //阻塞等待thread线程退出。此时该线程才算真正结束,它的内存空间也会被释放(被调用线程是非分离的)
 //retval:传出参数,获取线程退出状态。

int pthread_cancel(pthread_t thread);
//杀死(取消) thread 线程
//在要杀死的子线程对应的处理的函数的内部, 必须做过一次系统调用
//该函数杀死线程需要一个契机,当函数进入内核(到达一个取消点)杀死一个进程。
//如果没有到达取消点,该函数无效。但是可以在线程函数内部使用pthread_testcancel()添加一个取消点。
//成功被pthread_cancel()杀死的线程,能够被pthread_join()函数回收。

int pthread_detach(pthread_t thread);
//主线程与子线程分离,子线程结束后,资源自动回收。

线程属性: 对用户半透明,不想用户看到底层结构,但是可以通过相关函数对它进行配置。

int pthread_attr_init(pthread_attr_t *attr);
//初始化线程属性
int pthread_attr_destroy(pthread_attr_t *attr);
//销毁线程属性所占用的资源

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
//设置线程的属性是分离或者非分离
//attr:需要设置的线程属性变量
//detachstate:1.PTHREAD_CREATE_DETACHED 设置为分离属性;2.PTHREAD_CREATE_JOINABLE 设置属性为非分离
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
//获取线程属性变量的属性
//detachstate:传出参数,线程属性变量的属性值

也可以使用pthread_join()函数判断thread线程是否为分离状态,join能成功阻塞回收,说明没有分离;若是失败了,则说明已经分离了。

线程使用需要注意:
1.主线程退出而其他线程不退出,主线程应调用 pthread_exit() 函数。
2.避免僵尸线程:pthread_join()、pthread_detach、create时指定分离属性、不应当返回被回收线程栈中的值。
3.new() 和 mmap() 申请的内存可以被其他线程释放。
4.应避免在多线程模型中调用fork(),除非马上exec,子进程中只有调用fork()的线程存在,其他线程在子进程中均自动pthread_exit()掉。(不推荐线程中创建进程)
5.信号的复杂语义很难和多线程共存,应避免在多线程中引入信号机制。用信号就不用多线程,用线程就不要用信号!

三、线程同步

同步: 协同步调,线程按照预定的先后次序执行。
指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其他线程为保证数据的一致性,不能调用该功能。

程序中数据混乱原因: 1.资源共享(独享资源则不会);2.调度随机(意味着数据访问会出现竞争);3.线程间缺乏同步机制。

线程同步方法: 1.互斥量 mutex,也称为互斥锁; 2.条件变量 cond ; 3.信号量

互斥锁 mutex 的使用
1. pthread_mutex_t lock;
//创建锁。本质为一个结构体,但在使用的过程中可以看作一个整数,取值为 0、1 。

2. int pthread_mutex_init(pthread_mutex_t *restrict mutex,
           const pthread_mutexattr_t *restrict attr);
//初始化锁
//restrict:关键字,用来限定指针,表示由该指针指向的内存地址中内容的操作,只能由本指针完成
//attr:设置互斥锁的属性,一般传NULL,表示使用默认的互斥锁属性,默认属性为快速互斥锁 。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 
//静态创建互斥锁,其属性为默认属性。同pthread_mutex_init()的 attr 参数传NULL。

3.int pthread_mutex_lock(pthread_mutex_t *mutex);
//加锁,表示要使用某个共享资源了
//如果把锁 lock 看成一个整数,加锁等同于 lock-- 操作。(此时 lock == 0)

4.访问共享数据

5.int pthread_mutex_unlock(pthread_mutex_t *mutex);
//解锁,表示自己对共享资源的使用已经完成,其他的线程可以使用该共享数据了。
//如果把锁 lock 看成一个整数,解锁等同于 lock++ 操作。(此时 lock == 1)

6.int pthread_mutex_destroy(pthread_mutex_t *mutex);
//销毁互斥锁。由主线程决定什么时候销毁这把锁。

补充:
1.int pthread_mutex_trylock(pthread_mutex_t *mutex);
//尝试加锁
//加锁成功的话,lock-- ,此时等同于pthread_mutex_lock();加锁失败:返回,同时设置错误号errno为BUSY

2.读写锁:写独占,读共享
锁只有一把。但可以有两种模式:
(1). 以读的方式给数据加锁——读锁。
(2). 以写的方式给数据加锁——写锁。
写锁的优先级更高。
pthread_rwlock_t rwlock; //创建读写锁
pthread_rwlock_init(&rwlock, NULL); //初始化读写锁
pthread_rwlock_wrlock(&rwlock); //加写锁。改变共享数据的值的时候加锁,且只能加锁一次,即写独占。
pthread_rwlock_rdlock(&rwlock); //加读锁。读取共享数据的值的时候加锁,可以加无限制个,即读共享。(在我看来,在读数据的时候加不加这个锁似乎都不影响,希望大佬指正)
pthread_rwlock_unlock(&rwlock); //解锁
pthread_rwlock_destroy(&rwlock); //销毁锁

3.死锁:不是一种锁,而是使用锁不恰当导致的现象:
(1)对一个锁反复加锁
(2)两个线程各自持有一把锁,去请求另一把锁。
所以我个人觉得,一个程序中尽量不要设置太多的锁。

互斥锁使用技巧:
尽量保证锁的粒度,越小越好。(访问共享数据前一步加锁,访问结束后立即解锁。)

条件变量的使用:条件变量本身不是锁,但通常结合锁的使用方式来使用。
1. pthread_cond_t cond;  //定义一个条件变量
2. int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
//初始化条件变量
//attr:条件变量属性,一般传NULL,使用系统默认。
   pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//等同于pthread_cond_init()函数的attr参数传NULL
3. int pthread_mutex_lock(pthread_mutex_t *mutex); //加锁
4. int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
//阻塞等待 cond 条件变量不为0。
//mutex:互斥量。因为pthread_cond_wait()函数做了以下三件事情:
//(1).阻塞线程
//(2).释放(解锁)已经掌握的互斥量 mutex;
//(3).当条件变量 cond 满足(cond != 0 )时,线程被唤醒,同时对 mutex 进行加锁处理
5. int pthread_mutex_unlock(pthread_mutex_t *mutex); //解锁
6. int pthread_cond_signal(pthread_cond_t *cond);
//如果把pthread_cond_wait()看成是让 cond-- 的函数,那么它让 cond++
//pthread_cond_signal——唤醒睡眠的线程,一次只能唤醒一个线程
   int pthread_cond_broadcast(pthread_cond_t *cond);
//如果把pthread_cond_wait()看成是让 cond-- 的函数,那么它让 cond++
//pthread_cond_broadcast——唤醒睡眠的线程,一次唤醒所有睡眠的线程
例子:生产者和消费者模型:https://xmuli.blog.csdn.net/article/details/105885580
信号量的使用:
信号量:相当于初始化为 N 的互斥量。信号量和信号没有任何关系,可用于线程也可用于进程间同步
1.sem_t sem;//定义一个信号量
2.int sem_init(sem_t *sem, int pshared, unsigned int value);
//初始化信号量
//pshared:=0:为当前进程的所有线程共享; 其值不为0时,此信号量在进程间共享
//value:信号量的初始值
3.int sem_wait(sem_t *sem); //相当于加锁,sem--
4.int sem_trywait(sem_t *sem);//相当于尝试加锁
5.int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
//abs_timeout 指定一个阻塞的时间上限,如果调用因不能立即执行递减而要阻塞。
6.int sem_post(sem_t *sem); //相当于解锁,sem++
7.int sem_destroy(sem_t *sem);//销毁信号量,一般由主线程来完成信号量的销毁。
例子:同样可以使用生产者消费者模型。
上一篇:Linux - 线程


下一篇:解决ButterKnife8.4版本 @BindView报空指针异常