Linux 线程同步与互斥

为什么线程需要同步与互斥机制

  • ⼤部分情况,线程使⽤的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程⽆法获得这种变量。
  • 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
  • 多个线程并发的操作共享变量,会带来⼀些问题。

        假设两个线程读写相同变量时,线程A读取变量然后给这个变量赋予一个新的值,但写操作需要两个存储周期。当线程B在这两个写周期读取这个变量时,可能会得到不一致的值。为了避免这个问题,就需要互斥,同一时间只允许一个线程访问该变量。

        假设一个场景,现有100张车票,新建4个线程连续不断进行买票,直到票售完。我们先看代码与运行结果:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>

int ticket = 100;
void* route(void* arg)
{
    char* id = (char*)arg;
    while(1)
    {   
        if(ticket > 0)
        {   
            usleep(1000);
            printf("%s get a ticket:ticket:%d\n",id,ticket);
            ticket--;
        }   
        else
        {   
            break;
        }   
    }   
}


int main()
{
    pthread_t t1,t2,t3,t4;
    pthread_create(&t1,NULL,route,"thread 1");
    pthread_create(&t2,NULL,route,"thread 2");
    pthread_create(&t3,NULL,route,"thread 3");
    pthread_create(&t4,NULL,route,"thread 4");

    pthread_join(t1,NULL);
    pthread_join(t2,NULL);
    pthread_join(t3,NULL);
    pthread_join(t4,NULL);
    return 0;
}

运行结果截图:


我们可以看到,ticket数量已经变为-2,这显然不合常理

这是因为ticket--不是一个原子操作,它对应3条汇编指令

  • load:将共享变量ticket从内存加载到寄存器中
  • update: 更新寄存器⾥⾯的值,执⾏-1操作
  • store:将新值,从寄存器写回共享变量ticket的内存地址

所以,thread1获取ticket的数量时,thread3还没有把ticket--,所以才会出现-1,-2张票。

要解决以上问题需要解决以下3点:

  • 代码必须要有互斥行为:当代码进⼊临界区执⾏时,不允许其他线程进入该临界区。
  • 如果多个线程同时要求执⾏临界区的代码,并且临界区没有线程在执行,那么只能允许⼀个线程进入该临界区。
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区

要实现以上要求则需要一把锁,Linux将这把锁叫互斥量。


初始化互斥量

初始化互斥量有两种⽅法:

  • ⽅法1,静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
  • ⽅法2,动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号

_t *restrict attr);参数:mutex:要初始化的互斥量attr: NULL 销毁互斥量
销毁互斥量需要注意:
  • 使⽤PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁
  • 不要销毁⼀个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后⾯不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);

互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号

调用pthread_ lock 时,可能会遇到以下情况:

  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互
  • 斥量,那么pthread_ lock调⽤会陷⼊阻塞,等待互斥量解锁。

改进上面的售票系统代码:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>

int ticket = 100;
pthread_mutex_t mutex;
void* route(void* arg)
{
	char* id = (char*)arg;
	while(1)
	{
		pthread_mutex_lock(&mutex);
		if(ticket > 0)
		{
			usleep(1000);
			printf("%s get a ticket:ticket:%d\n",id,ticket);
			ticket--;
			pthread_mutex_unlock(&mutex);
		}
		else
		{
			pthread_mutex_unlock(&mutex);
			break;
		}
	}
}


int main()
{
	pthread_mutex_init(&mutex,NULL);
	pthread_t t1,t2,t3,t4;
	pthread_create(&t1,NULL,route,"thread 1");
	pthread_create(&t2,NULL,route,"thread 2");
	pthread_create(&t3,NULL,route,"thread 3");
	pthread_create(&t4,NULL,route,"thread 4");

	pthread_join(t1,NULL);
	pthread_join(t2,NULL);
	pthread_join(t3,NULL);
	pthread_join(t4,NULL);
	pthread_mutex_destroy(&mutex);
	return 0;
}

运行结果:


条件变量

  • 当⼀个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
  • 例如⼀个线程访问队列时,发现队列为空,它只能等待,只到其它线程将⼀个节点添加到队列中。这种情况就需要用到条件变量。

初始化

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *rest
rict attr);
参数:
cond:要初始化的条件变量
attr: NULL

销毁

int pthread_cond_destroy(pthread_cond_t *cond)

等待条件满足

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mute
x);
参数:
cond:要在这个条件变量上等待
mutex:互斥量

唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

简单案例,首先不加条件变量:

void* rout1(void* arg)
{
	while(1)
	{
		printf("I am thread A!\n");
	}
}


void* rout2(void* arg)
{
	while(1)
	{
		printf("I am thread B!\n");
	}
}



int main()
{
	pthread_t t1,t2;

	pthread_create(&t1,NULL,rout1,NULL);
	pthread_create(&t2,NULL,rout2,NULL);

	pthread_join(&t1,NULL);
	pthread_join(&t2,NULL);

	return 0;
}

运行结果:

                            

可见,程序执行后,要么打印大量A,要么大量打印B。若加上条件变量,则会呈现出A、B交替打印的状态

pthread_cond_t cond;
pthread_mutex_t mutex;

void* rout1(void* arg)
{
	while(1)
	{
		pthread_cond_wait(&cond,&mutex);
		printf("I am thread A!\n");
	}
}


void* rout2(void* arg)
{
	while(1)
	{
		printf("I am thread B!\n");
		pthread_cond_signal(&cond);
	}
}



int main()
{
	pthread_t t1,t2;
	pthread_cond_init(&cond,NULL);
	pthread_mutex_init(&mutex,NULL);

	pthread_create(&t1,NULL,rout1,NULL);
	pthread_create(&t2,NULL,rout2,NULL);

	pthread_join(t1,NULL);
	pthread_join(t2,NULL);

	pthread_mutex_destroy(&mutex);
	pthread_cond_destroy(&cond);
	return 0;
}

为什么pthread_cond_wait需要互斥量?

  • 条件等待是线程间同步的⼀种⼿段,如果只有⼀个线程,条件不满⾜,⼀直等下去都不会满足,所以必须要有⼀个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
  • 条件不会无缘无故的突然变得满⾜了,必然会牵扯到共享数据的变化。所以⼀定要用互斥锁来保护。没有互斥锁就⽆法安全的获取和修改共享数据。

猜你喜欢

转载自blog.csdn.net/ihaha233/article/details/80302113