并发易混淆概念总结

时间:2021-03-01 17:57:32

竞争

线程A执行逻辑经过x逻辑,线程B执行逻辑经过y逻辑。

竞争: 程序执行结果的正确性,取决于B的y逻辑必须要在A的x逻辑前执行,此时就发生了竞争。

感觉这么解释还是比较抽象,下面通过一个c语言的例子来解释:

A线程通过for循环创建多个对等线程,x逻辑表示创建对等线程并传递参数。

B线程得到A线程传递的参数,y逻辑表示打印传递的参数。

#include "test.h"
#define N 4
void *thread(void vargp);
//以下为A线程
int main() {
pthread_t tid[N];
int i;

for(i = 0; i < N; i++) {//线程A的x逻辑
Pthread_create(&tid[i], NULL, thread, &i);
}
for(i = 0; i < N; i++) {
Pthread_join(tid, NULL); //等待创建的对等线程结束
}
exit(0);
}

//线程B
void *thread(void *vargp) {
int myid = *((int*)vargp);//线程B的y逻辑
printf("Hello from thread %\n", myid);
return NULL;
}

实际结果:

>./test
Hello from thread 1
Hello from thread 3
Hello from thread 2
Hello from thread 3

原因:

主线程(A)和对等线程(B)之间存在竞争,导致程序最终执行的结果和预期的是不一致的。A线程传递一个指向本地栈变量的i指针到B 线程。如果B线程y逻辑(vargp参数的间接引用和赋值)能够每次都在A线程x逻辑(对i加1的操作)那么myid就能够得到正确的ID。

解决方案:

可以将线程A的&i的传递改为传递堆空间的值,注意在B线程必须释放堆的内存,否则会造成内存泄露。

线程安全函数

线程安全函数:一个函数被称为线程安全的, 当且仅当被多个并发线程反复地调用时,它会一直产生正确的结果。

线程安全不是很好判断,我们先来理解下它的补集——线程不安全 。一个函数不是线程安全就是线程不安全的。所以只要学会判断一个函数是不是线程不安全的就可以了。

定义四类线程不安全的函数类

  • 不保护共享变量的函数

    这么说还是很抽象,举个例子。线程A,线程B,全局变量x,线程不安全函数func()对变量x执行x++操作,A、B两个线程均要调用func(),如果func()没有使用锁或者信号量来保护这个全局的变量x,那么该函数就是线程不安全的。

  • 保持跨越多个调用状态的函数

    变量next_seed, 函数rand(),函数srand(),第一步:srand()会初始化next_seed变量,第二步:rand()函数会依赖上传初始化后的next_seed变量进行处理然后返回。这就是跨越多个调用状态的函数 的理解。

    unsigned next_seed = 1;

    unsigned rand(void) {
    next_seed = next_seed *1103515245 + 12543;
    return (unsigned) (next_seed>>16)%32768;
    }

    void srand(unsigned new_seed) {
    next_seed = new_seed
    }
  • 返回指向静态变量的指针的函数。

    某类函数(ctime、gethostbytname),将计算结果放在一个static变量中,然后返回一个指向该变量的指针。如果在单线程中,使用该类函数没有任何问题,但是如果是在多线程中A线程返回的static变量的值,可能已经被B线程修改过。

  • 调用线程不安全函数的函数

    问题: 如果函数f调用线程不安全函数g,那么f一定是线程不安全的吗?

    答案: 视情况而定,如果是第一种和第三种情况,用一个互斥锁保护调用位置和任何得到的共享数据,f仍有可能是线程安全的。如果是第二种情况(依赖于跨越多次调用的状态),那么f也不是线程安全的。

可重入函数

可重入函数,其特点在于它们具有被多个线程调用时,不会引起任何共享数据的特性。

感觉说完了,你不能说它的定义是错的但是还是不清楚什么函数是可重入函数。简单点,概念不要这么复杂简单点,就是这个函数能够被再次进入呗!

举个例子:

线程A– ———–>函数F1()————-中断—————–>函数F1()

*可重入函数也可以这样理解,重入即表示重复进入,首先它意味着这个函数可以被中断,其次意味着它除了使用自己栈上的变量以外不依赖于任何环境(包括static),这样的函数就是purecode(纯代码)可重入,可以允许有多个该函数的副本在运行,由于它们使用的是分离的栈,所以不会互相干扰。如果确实需要访问全局变量(包括static),一定要注意实施互斥手段。可重入函数在并行运行环境中非常重要,但是一般要为访问全局变量付出一些性能代价

可重入函数

总结下线程安全、线程不安全和可重入函数的关系。

并发易混淆概念总结

可重入函数一定是线程安全函数,但线程安全函数不一定是可重入函数。其实很好理解,可重入函数一定是没有共享全局变量的符合线程安全函数的第一和第三点要求。

死锁

什么是死锁

死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

死锁发生的条件

  • 互斥条件:线程对资源的访问是排他性的,如果一个线程对占用了某资源,那么其他线程必须处于等待状态,直到资源被释放。

  • 请求和保持条件:线程T1**至少已经保持了一个资源R1占用,但又提出对另一个资源R2请求,而此时,资源R2被其他线程T2占用,于是该线程T1也必须等待,但又对自己保持的资源R1不释放**。

  • 不剥夺条件:线程已获得的资源,在未使用完之前,不能被其他线程剥夺,只能在使用完以后由自己释放

  • 环路等待条件:在死锁发生时,必然存在一个“进程-资源环形链”,即:{p0,p1,p2,…pn},进程p0(或线程)等待p1占用的资源,p1等待p2占用的资源,pn等待p0占用的资源。(最直观的理解是,p0等待p1占用的资源,而p1而在等待p0占用的资源,于是两个进程就相互等待)

举个例子

线程A,线程B,互斥锁S1,互斥锁S2,A,B线程均需要获取两个锁才能继续执行。

线程A获取互斥锁S1的使用权,同时线程B获得互斥锁S2的使用权。此时线程A、线程B均等待对方释放自己需要的锁,但是AB又都不肯释放自己的锁,此时就处于死锁的状态。

饥饿

什么是饥饿

饥饿:是指如果线程T1占用了资源R,线程T2又请求*R,于是T2等待。T3也请求资源R,当T1释放了R上的*后,系统首先批准了T3的请求,T2仍然等待。然后T4又请求*R,当T3释放了R上的*之后,系统又批准了T4的请求……,T2可能永远等待。

举个例子

读者写者的问题上,如果是读者优先,那么写者就可能一直处于饥饿的状态。

读者A先进入,写者B等待资源,但此时不断有读者进入,导致资源一直被占用无法释放,于是写者B就处于饥饿的状态。

死锁和饥饿的区别 线程是否是一直处于阻塞的状态还是有可能从阻塞状态变为非阻塞状态只是时间未可知。