订阅
纠错
加入自媒体

一文搞懂多线程中各个难点

2021-06-28 14:13
一口Linux
关注

2.3线程注意点

1、线程ID是进程地址空间内的一个地址, 要在同一个线程组内进行线程之间的比较才有意义。不同线程组内的两个线程, 哪怕两者的pthread_t值是一样的, 也不是同一个线程。

2、线程ID就有可能会被复用:

1、线程退出。

2、线程组的其他线程对该线程执行了pthread_join, 或者线程退出前将分离状态设置为已分离。

3、再次调用pthread_create创建线程。

2.4线程创建出来的默认值

线程创建的第二个参数是pthread_attr_t类型的指针, pthread_attr_init函数会将线程的属性重置成默认值。

线程属性及默认值:

如果确实需要很多的线程, 可以调用接口来调整线程栈的大小:

#include

线程终止,但进程不会终止的方法:

1、入口函数的return返回,线程就退出了

2、线程调用pthread_exit(NULL),谁调用谁退出

#include

void pthread_exit(void *retval);

参数:retval是返回信息,”临终遗言“,可以给可以不给

该变量不能使用临时变量。

可使用:全局变量、堆上开辟的空间、字符串常量。

pthread_exit和线程启动函数(start_routine) 执行return是有区别的。在start_routine中调用的任何层级的函数执行pthread_exit() 都会引发线程退出, 而return, 只能是在start_routine函数内执行才能导致线程退出。

3、其它线程调用了pthread_cancel函数取消了该线程

int pthread_cancel(pthread_t thread);

thread:线程标识符

调用该函数的执行流可以取消其它线程,但是需要知道其它线程的线程标识符,也可以执行流自己取消自己,传入自己的线程标识符。

如果线程组中的任何一个线程调用了exit函数, 或者主线程在main函数中执行了return语句, 那么整个线程组内的所有线程都会终止。

4.线程等待4.1线程等待接口#include

调用该函数,该执行流在等待线程退出的时候,该执行流是阻塞在pthread_joind当中的。

4.2线程等待和进程等待的不同

第一点不同之处是进程之间的等待只能是父进程等待子进程, 而线程则不然。线程组内的成员是对等的关系, 只要是在一个线程组内, 就可以对另外一个线程执行连接(join) 操作。

第二点不同之处是进程可以等待任一子进程的退出 , 但是线程的连接操作没有类似的接口, 即不能连接线程组内的任一线程, 必须明确指明要连接的线程的线程ID。

pthread_join()错误码:

4.3为什么要等待退出的线程?

如果不连接已经退出的线程, 会导致资源无法释放。所谓资源指的又是什么呢?

1、已经退出的线程, 其空间没有被释放, 仍然在进程的地址空间之内。

2、新创建的线程, 没有复用刚才退出的线程的地址空间。

如果不执行连接操作, 线程的资源就不能被释放, 也不能被复用, 这就造成了资源的泄漏。

纵然调用了pthread_join, 也并没有立即调用munmap来释放掉退出线程的栈, 它们是被后建的线程复用了。释放线程资源的时候, 若进程可能再次创建线程, 而频繁地munmap和mmap会影响性能, 所以将该栈缓存起来, 放到一个链表之中, 如果有新的创建线程的请求, 会首先在栈缓存链表中寻找空间合适的栈, 有的话, 直接将该栈分配给新创建的线程。

例:

#include

默认情况下, 新创建的线程处于可连接(Joinable) 的状态, 可连接状态的线程退出后, 需要对其执行连接操作, 否则线程资源无法释放, 从而造成资源泄漏。

如果其他线程并不关心线程的返回值, 那么连接操作就会变成一种负担:你不需要它, 但是你不去执行连接操作又会造成资源泄漏。这时候你需要的东西只是:线程退出时, 系统自动将线程相关的资源释放掉, 无须等待连接。

可以是线程组内其他线程对目标线程进行分离, 也可以是线程自己执行pthread_detach函数。

线程的状态之中, 可连接状态和已分离状态是冲突的, 一个线程不能既是可连接的, 又是已分离的。因此, 如果线程处于已分离的状态, 其他线程尝试连接线程时, 会返回EINVAL错误。

pthread_detach错误码:

注意:这里的已分离不是指线程失去控制,不归线程组管,而是指线程退出后,系统会自动释放线程资源。若是线程组内的任意线程执行了exit函数,即使是已分离的线程,也仍会收到影响,一并退出。

6.线程安全

线程安全中涉及到的概念:

临界资源:多线程中都能访问到的资源
临界区:每个线程内部,访问临界资源的代码,就叫临界区6.1什么是线程不安全?

多个线程访问同一块临界资源,导致资源产生二义性的现象。

6.1.1举一个例子

假设现在有两个线程A和B,单核CPU的情况下,此时有一个int类型的全局变量为100,A和B的入口函数都要对这个全局变量进行–操作。

线程A先拿到CPU资源后,对全局变量进行–操作并不是原子性操作,也就是意味着,A在执行–的过程中有可能会被打断。假设A刚刚将全局变量的值读到寄存器当中,就被切换出去了,此时程序计数器保存了下一条执行的指令,上下文信息保存寄存器中的值,这两个东西是用来线程A再次拿到CPU资源后,恢复现场使用的。

此时,线程B拿到了CPU资源,对全局变量进行了–操作,并且将100减为了99,回写到了内存中。

A再次拥有了CPU资源后,恢复现场,继续往下执行,从寄存器中读到的值仍为100,减完之后为99,回写到内存中为99。

上述例子中,线程A和B都对全局变量进行了–操作,全局变量的值应该变为98,但程序现在实际的结果为99,所以这就导致了线程不安全。

6.2如何解决线程不安全现象?

解决方案只需做到下述三点即可:

1、代码必须要有互斥的行为:当一个线程正在临界区中执行时, 不允许其他线程进入该临界区中。

2、如果多个线程同时要求执行临界区的代码, 并且当前临界区并没有线程在执行, 那么只能允许一个线程进入该临界区。

3、如果线程不在临界区中执行, 那么该线程不能阻止其他线程进入临界区。

则本质上,我们需要对该临界区加一把锁:

锁是一个很普遍的需求, 当然用户可以自行实现锁来保护临界区。但是实现一个正确并且高效的锁非常困难。纵然抛下高效不谈, 让用户从零开始实现一个正确的锁也并不容易。正是因为这种需求具有普遍性, 所以Linux提供了互斥量。

6.3互斥量接口

6.3.1互斥量的初始化

1、静态分配:

#include

2、动态分配:

int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);

调用int pthread_mutex_init()函数后,互斥量是处于没有加锁的状态。

6.3.2互斥量的销毁

int pthread_mutex_destroy(pthread_mutex_t *mutex);

注意:

1、使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量无须销毁。

2、不要销毁一个已加锁的互斥量, 或者是真正配合条件变量使用的互斥量。

3、已经销毁的互斥量, 要确保后面不会有线程再尝试加锁。

当互斥量处于已加锁的状态, 或者正在和条件变量配合使用, 调用pthread_mutex_destroy函数会返回EBUSY错误码。

6.3.3互斥量的加锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);

第一个接口:int pthread_mutex_lock(pthread_mutex_t *mutex);

1、该接口是阻塞加锁接口。

2、mutex为传入互斥锁变量的地址

3、如果mutex当中的计数器为1,pthread_mutex_lock接口就返回了,表示加锁成功,同时计数器当中的值会被更改为0.

4、如果mutex当中的计数器为0,pthread_mutex_lock接口就阻塞了,pthread_mutex_lock接口没有返回了,阻塞在函数内部,直到加锁成功

第二个接口:int pthread_mutex_trylock(pthread_mutex_t *mutex);

1、该接口为非阻塞接口

2、mutex中计数器为1时,加锁成功,计数器置为0,然后返回

3、mutex中计数器为0时,加锁失败,但也会返回,此时加锁是失败状态,一定不要去访问临界资源

4、非阻塞接口一般都需要搭配循环来使用。

第三个接口:int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);

1、带有超时时间的加锁接口

2、不能直接获取互斥锁的时候,会等待abs_timeout时间

3、如果在这个时间内加锁成功了,直接返回,不需要再继续等待剩余的时间,并且表示加锁成功

4、如果超出了该时间,也返回了,但是加锁失败了,需要循环加锁

上述三个加锁接口,第一个接口用的最多。

<上一页  1  2  3  4  下一页>  
声明: 本文由入驻维科号的作者撰写,观点仅代表作者本人,不代表OFweek立场。如有侵权或其他问题,请联系举报。

发表评论

0条评论,0人参与

请输入评论内容...

请输入评论/评论长度6~500个字

您提交的评论过于频繁,请输入验证码继续

暂无评论

暂无评论

人工智能 猎头职位 更多
扫码关注公众号
OFweek人工智能网
获取更多精彩内容
文章纠错
x
*文字标题:
*纠错内容:
联系邮箱:
*验 证 码:

粤公网安备 44030502002758号