线程同步和线程死锁

时间:2021-01-16 23:24:11

线程同步

前面刚介绍了有关线程的基本认识,那我们先来思考一个小问题,两个线程之间有没有可能同时对一个资源发起访问呢,答案是肯定,那么在某些情况下这样的同时访问会引发一系列冲突,先来看一个简单的例子。

创建两个线程,各自将count增加2500次,然后输出最后的结果,如下:

#include<stdio.h>
#include<pthread.h>

int count = 0;
void *thread_count(void *arg)
{
int i =0;
while(i<5000)
{
i++;
count++;
}
return NULL;
}


int main()
{
pthread_t id1,id2;
pthread_create(&id1,NULL,thread_count,NULL);
pthread_create(&id2,NULL,thread_count,NULL);

pthread_join(id1,NULL);
pthread_join(id2,NULL);
printf("count final val is : %d\n",count);
return 0;
}

线程同步和线程死锁
毫无疑问,输出的结果是正确,那么我们来让两个线程一人增加50000000次,那么最后应该输出100000000。结果是不是这样呢?

线程同步和线程死锁

结果是之前的猜想确实大相径庭。

我们先来分析一下问题产生的原因,前面之所以能够正确的打印出来10000,那是因为运算量过小,在一个线程运行的时候,由于时间过短,另一个线程没对它产生影响,而下面的100000000不能准确打印出来,就是因为在第一个线程运行尚未结束时,第二个线程也访问count,所以导致最后的结果是错的,所以我们看出,当程序运行越长,却容易发生这种访问冲突的问题。

那么这个问题怎么解决呢,那就是引入互斥锁,即所谓的同步机理就是通过互斥锁来实现的,说的通俗点就是,拿到锁的线程完成“读-修改-写”这样的操作,然后释放锁给其他线程,而没拿到锁的线程只能等待而不能对共享资源进行访问,这样的话“读-修改-写”三个操作组成了一个原子操作,要么都执行,要么都不执行。不会发生执行一半被打断的现象,这样的话就可以解决访问冲突的问题了。


初始化锁和销毁锁

int pthread_mutex_destroy(pthread_mutex_t *mutex);

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

pthread_mutex_init函数对Mutex做初始化,参数attr设定Mutex的属性,如果attr为NULL则表示缺省参数。
pthread_mutex_init函数初始化的Mutex可以用pthread_mutex_destroy销毁,如果Mutex变量是静态分配的,也可以用宏定义PTHREAD_MUTEX_INITIALIZER 来初始化,相当于pthread_mutex_init初始化并且attr参数为NULL.

加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

一个线程可以通过调用lock()函数获得锁,而当这时其他线程已经先于它拿到锁时,这时该线程会呈挂起状态,一直等到另一个线程利用unlock()函数将锁释放该进程才被换唤醒,并且拿到锁之后才可以继续执行,如果既想获得锁,又不想挂起,那么就调用trylock()函数,如果这时锁被其他线程拿着,那么该进程会返回EBUSY,而不会是挂起状态。

那么现在讲刚才的代码稍加修改一下:

#include<stdio.h>
#include<pthread.h>

pthread_mutex_t mutex_lock = PTHREAD_MUTEX_INITIALIZER;
int count = 0;
void *thread_count(void *arg)
{
int i =0;
while(i<5000)
{
pthread_mutex_lock(&mutex_lock);
i++;
count++;
pthread_mutex_unlock(&mutex_lock);
}
return NULL;
}


int main()
{
pthread_t id1,id2;
pthread_create(&id1,NULL,thread_count,NULL);
pthread_create(&id2,NULL,thread_count,NULL);

pthread_join(id1,NULL);
pthread_join(id2,NULL);
printf("count final val is : %d\n",count);
return 0;
}

线程同步和线程死锁
在每次count++之前加上一把锁,等加完之后再将锁释放,这样,最后的结果便会打印正确,避免的访问冲突的问题。


线程死锁

我们试想下面这样一种情景,假如有2个线程,一个线程想先锁对象1,再锁对象2,恰好另外有一个线程先锁对象2,再锁对象1。
在这个过程中,当线程1把对象1锁好以后,就想去锁对象2,但是不巧,线程2已经把对象2锁上了,也正在尝试去锁对象1。
什么时候结束呢,只有线程1把2个对象都锁上并把方法执行完,并且线程2把2个对象也都锁上并且把方法执行完毕,那么就结束了,但是,谁都不肯放掉已经锁上的对象,所以就没有结果,这种情况就叫做线程死锁。

所以我们在写程序的时候,应当尽量避免同时获得多个锁,如果非得这样的话,则有一个原则:如果所有线程在需要多个锁时,都按照相同的先后顺序获得锁,则不会出现死锁,就是当一个线程拿到第一个锁所,那么它必定是可以拿到后面的锁的。