天天看点

【Linux】——线程同步与互斥1、同步和互斥的概念2、线程同步方式

文章目录

  • 1、同步和互斥的概念
  • 2、线程同步方式
    • 2.1互斥锁(同步)
    • 2.2条件变量(同步)
    • 2.3读写锁(同步)
    • 2.4信号量(同步与互斥)

1、同步和互斥的概念

为了更加清楚的了解这两个的概念。我们举个例子来说明为什么在有了互斥机制的情况下还需要有同步机制。

  • 假设学校有一间自习室只有一个位置,那有一天你准备去学校自习室上自习,起的很早去拿了钥匙开门进去开始自习,但是在自习了一分钟之后,突然不想自习了想出去玩一会,走到门口刚刚准备把钥匙挂在门上又想得好好学习啊,于是又打开门进了自习室,刚刚坐下一分钟又想出去玩了,于是又出来之后一想还是学习吧,又打开门进去,此时门口其实已经等了一大批人准备在这个教室自习,可以你就不停的打开门进去自习一分钟又出来又打开门进去自习一分钟,重复的做这件事,如此反复导致门口所有等着自习的人均无法进行自习,这就导致了

    饥饿问题

    ,于是老师在自习室门口贴了一条规定,在你进去自习出来之后如果还想接着自习就必须到教室外面排队等待自习,这样就保证了所有的同学都能够得到自习的机会。

从这个例子中我们就可以发现,拿走钥匙不让其他人入内其实就是一种互斥机制,但是为了解决饥饿问题,后来加入了排队机制,其实就是一种同步的机制。

所以,我们可以认为,互斥机制是为了保证安全,同步机制是为了保证合理性

同步

在保证数据安全的前提下,让线程能够按照某种特定的访问顺序访问临界资源,从而有效的避免饥饿问题。

互斥

一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源

2、线程同步方式

线程同步方式和进程的很像,都是对临界资源进行控制,处理线程互斥,同步这两种关系。主要有四种方式:

互斥量,读写锁,信号量,条件变量

2.1互斥锁(同步)

1、概念

在多任务的操作系统中,同时运行的多个任务可能都需要使用同一资源。互斥锁是一种

简单的加锁方法来控制对共享资源的访问

互斥锁只有两种状态,线程在进入临界区之前,加锁操作。线程在退出临界区之后,解锁操作

1.1使用步骤

  • 线程在访问临界资源之前,对临界区代码进行加锁操作,如果锁是加锁状态的,则线程执行加锁操作将被阻塞,直到锁解锁可解除解锁。即表示这块临界区只允许一个线程进行访问
  • 进行临界区代码执行,即对临界资源进行操作的代码运行。
  • 退出临界区之后,进行解锁操作。

    可以概括为:

    【Linux】——线程同步与互斥1、同步和互斥的概念2、线程同步方式

2、互斥锁的类型和方法

互斥变量用pthread_mutex_t数据类型来表示。一般的,将锁定义到全局

  1. 初始化一个互斥锁

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

第一个参数将定义的互斥锁地址传入即可。一般的,第二个参数传为空,表示用默认的属性初始化互斥量。

初始化的锁是处于解锁状态

  1. 加锁和解锁

对互斥量进行加锁,需要调用pthread_mutex_lock,如果互斥量已经上锁,调用线程将阻塞直到互斥量被解锁,对互斥量解锁,需要调用pthread_mutex_unlock.具体的方法实现如下:

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

【注意!】因为上面lock这种加锁操作如果锁是加锁状态就会阻塞.

所以有一种trylock的方式尝试对互斥量进行加锁,如果互斥量处于未锁住状态,那么trylock将锁住互斥量,不会出现阻塞并返回0。否则就会复制失败,不能锁住互斥量,而返回EBUS,具体实现如下:

int pthread_mutex_trylock(pthread_mutex_t *mutex);
           
  1. 销毁锁

int pthread_mutex_destroy(pthread_mutex_t *mutex);
           

3、互斥锁的特点

  • 原子性

    :把互斥量锁定为一个原子操作
  • 唯一性

    :如果一个线程锁定了一个互斥量,在它解除锁定之前,没有其他线程可以多动这个互斥量
  • 非繁忙等待

    :如果一个线程已经锁定了一个互斥量,第二个线程又试图去锁定这个互斥量,则第二个线程将被挂起(不占用任何cpu资源),直到第一个线程解除对这个互斥量的锁定为止,第二个线程则被唤醒并继续执行,同时锁定这个互斥量。
  • 加锁,解锁

    在代码中必须

    成对出现

    ,即一个线程加锁,解锁;不能一个线程加锁,一个线程解锁,会容易出现死锁。
  • 一把锁只能用于对一个资源的互斥访问

    ,不能实现多个资源的多线程互斥问题。

5、实例

实现:

主线程负责接收用户输入,函数线程负责将用户输入打印到终端界面

分析:

  • 数据传递:线程之间需要共享数据,所以不能定义为局部的,必须定义为全局,堆都可以,我们使用全局数组buff来存储数据。
  • 线程关系:主线程没有接收到数据时,函数线程不能打印;函数线程打印时,主线程不能读取;主线程写入时,函数线程不能打印。

总结:buff是临界资源,主线程和函数线程对其进行访问,所以在处理buff前进行加锁操作,处理完成后解锁。

代码实现如下:

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

#include<pthread.h>
#include<time.h>
#include<fcntl.h>

pthread_mutex_t mutex;

char buff[128] = {0};

void *fun(void *arg)
{
	while(1)
	{
		pthread_mutex_lock(&mutex);
		if(strncmp(buff,"end",3) == 0)
		{
			break;
		}

		printf("fun :%s\n",buff);
		memset(buff,0,128);

		int n = rand() % 3 +1;
		sleep(n);

		pthread_mutex_unlock(&mutex);
	    n = rand() % 3 + 1;
		sleep(n);
	}
}

int main()
{
	srand((unsigned int)(time(NULL) * time(NULL)));
	pthread_mutex_init(&mutex,NULL);//初始化的锁是解锁状态的

//创建一个线程
	pthread_t id;
	int res = pthread_create(&id,NULL,fun,NULL);
	assert(res == 0);

	while(1)
	{
		pthread_mutex_lock(&mutex);
		printf("input:");
		fgets(buff,127,stdin);
		pthread_mutex_unlock(&mutex);
		if(strncmp(buff,"end",3) == 0)
		{
			break;
		}
		int n = rand()%3 + 1;
		sleep(n);
	}
	//等待函数线程的结束
	pthread_join(id,NULL);
	pthread_mutex_destroy(&mutex);
	exit(0);

}
           

运行结果如下图所示:

【Linux】——线程同步与互斥1、同步和互斥的概念2、线程同步方式

可以看到主线程获取写入一个,函数线程输出一个。主线程先运行,创建函数线程后并不代表让出CPU,直到sleep时才会让出CPU,这时才出现,函数线程和主线程并发执行,抢占CPU资源。

2.2条件变量(同步)

1、概念

在Linux下的通知机制引入了条件变量来确保了同步的合理性和高效性。那是怎么来确保的呢?我们还是接着上面的那个例子说起走。

  • 假设你很热爱自习,所以每次你都去的最早,所以当其他同学来的时候就要排队等待,这些等待的同学每隔几分钟就去看一次你是否离开,但是你因为热爱自习所以经常自习很久,这些等待的同学有时候会浪费一整天时间来看你是否自习结束。因此自习室后来又加入了通知的机制,当自习室没有人时就会通知其他人,这样使同学们的效率大大提高。

当一个线程互斥的访问某个变量,它可以发现在其他线程改变状态之前,他什么也做不了。

所以总结一下,条件变量提供了一种进程间的通知机制,给多个线程提供了一个回合的场所条件变量是用来自动阻塞一个线程,直到某个特殊情况发生为止。会唤醒等待这个条件的线程,这种方式提高了线程同步的合法性和高效性。

简单来理解一下就是,

一个线程修改条件,另一个线程等待条件,一旦等到自己需要的条件,就去运行。

2、条件变量的类型和方法

  1. 定义一个条件变量

    定义为全局,多个线程可以共享
# include<pthread.h>
pthread_cond_t cond ;//定义一个条件变量
           
  1. 初始化

int pthread_cond_init(pthread_cond_t *cond,
						pthread_condattr_t *cond_attr);
           

这种方法是动态分配的,参数1表示要初始化的条件变量。参数2通常设置为空。

  1. 销毁

int pthread_cond_destroy(pthread_cond_t *cond);
           
  1. 等待条件为真函数

    函数原型如下:
# include<pthread.h>
int pthread_cond_wait(pthread_cond_t*   cond,pthread_mutex_t* mutex);
      //成功返回0,失败返回错误编号
           

传递给此函数的互斥量对条件进行保护,调用者把锁住的互斥量传给函数,函数调用线程放在等待条件的线程列表上,然后对互斥量解锁,这两部是原子操作。等待函数返回时,互斥量再次被锁住。

所以使用此函数的代码为:

pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond,&mutex);//自动阻塞,直到条件发生
pthread_mutex_unlock(&mutex);
           

具体描述一下:

【Linux】——线程同步与互斥1、同步和互斥的概念2、线程同步方式
  • 条件成立唤醒线程

// 至少唤醒一个等待该条件的线程
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒等待该条件的所有线程
int pthread_cond_broadcast(pthread_cond_t *cond); 
           

使用时也应该先加锁,再唤醒,再解锁。

3、实例

通常条件变量和互斥锁同时使用。实现主线程给buff中写入数据,其他两个函数线程等待buff中有数据时才可以进行数据打印,一旦输入end结束符,则线程结束。

分析

  • 创建线程,通过传参,创建出两个线程。
  • 函数线程先pthread_cond_wait()阻塞,等待buff数据写入后,主线程唤醒
  • 主线程向buff中写入数据后,根据写入的数据进行线程唤醒,如果不是结束符,则唤起任意一个等待的线程输出数据,如果是结束符end,则唤起所有等待的线程,所有线程break结束了线程。
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>

#include<pthread.h>

char buff[128] = {0};

pthread_cond_t cond;
pthread_mutex_t mutex;

void *fun(void *arg)
{
	char* s = (char*)arg;
	while(1)
	{
		pthread_mutex_lock(&mutex);
		pthread_cond_wait(&cond,&mutex);
		pthread_mutex_unlock(&mutex);

		if(strncmp(buff,"end",3) == 0)
		{
			break;
		}
		printf("%s:%s",s,buff);
	}
}
int main()
{
	pthread_mutex_init(&mutex,NULL);
	pthread_cond_init(&cond,NULL);

	pthread_t id[2];
	int res = pthread_create(&id[0],NULL,fun,"thread1");
	assert(res == 0);
	res = pthread_create(&id[1],NULL,fun,"thread2");
	assert(res == 0);

	while(1)
	{
		printf("input:");
		fgets(buff,127,stdin);
		//唤醒在条件变量cond上等待的所有线程
		if(strncmp(buff,"end",3) == 0)
		{
			pthread_mutex_lock(&mutex);
			pthread_cond_broadcast(&cond);
			pthread_mutex_unlock(&mutex);
		}
		//唤醒在条件变量上等待的一个线程
		else
		{
			pthread_mutex_lock(&mutex);
			pthread_cond_signal(&cond);
			pthread_mutex_unlock(&mutex);
		}
	}

	pthread_join(id[0],NULL);
	pthread_ioin(id[1],NULL);

	pthread_mutex_destroy(&mutex);
	pthread_cond_destroy(&cond);

	exit(0);
}

           
【Linux】——线程同步与互斥1、同步和互斥的概念2、线程同步方式

2.3读写锁(同步)

1、概念

读写锁就是更高级的互斥锁,有更高的并行性。互斥锁要么是锁住状态,要么就是不加锁状态,而且一次只有一个线程可以对其加锁。读写锁可以有3种状态:读模式下加锁状态、写模式加锁状态、不加锁状态。

一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁(允许多个线程读但只允许一个线程写)

2、特点

  1. 多个读者可以同时读

    ,即当读写锁是读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但如果线程希望以写模式对锁进行加锁,它必须阻塞直到所有的线程释放读锁,故不存在读-写的可能。
  2. 只允许一个写者写,不允许写-写,写-读

    。即当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁=(读锁或写锁)的线程都会被阻塞。
  3. 写者优于读者

    。当读写锁处于读模式锁住状态时,如果有另外的线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁请求,也就是说:一旦有写者,则后续读者必须等待,唤醒时优先考虑写者。
  4. 读写锁适合对数据结构读的次数远大于写的情况下使用。

3、读写锁的方法

  1. 定义读写锁

    读写锁一般也定义在全局:
pthread_rwlock_t rwlock;//定义了名为rwlock的读写锁
           
  1. 初始化和销毁

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
           

第一个参数是将定义的读写锁地址传入即可。第二个参数一般将attr设置为NULL,表示用默认的属性初始化读写锁。

  1. 加锁

    有两种加锁的方式:加读锁,加写锁。
// 申请读锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock ); 
// 申请写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock ); 
           
  1. 解锁

int pthread_rwlock_unlock (pthread_rwlock_t *rwlock); 
           
  1. 销毁锁

pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond,&mutex);//自动阻塞,直到条件发生
pthread_mutex_unlock(&mutex);

           

4、实例:使用读写锁来实现两个线程读写一段数据

思路

:主线程写入时加写锁,两个函数线程读取时加读锁,这样两个函数线程可以同时读取,

  • 主线程→写锁→将数据写入buff→解锁
  • 函数线程fun→读锁→输出buff数据→解读锁
  • 函数线程fun1→读锁→输出buff数据→解读锁

【注意】不要对buff进行memset清0,否则另一个函数线程读不到了。

# include<unistd.h>
# include<stdio.h>
# include<string.h>
# include<time.h>
# include<assert.h>
# include<pthread.h>

pthread_rwlock_t rwlock;//创建读写锁
char buff[128]={0};



void* fun(void* arg)//读数据
{
    while(1)
    {
        pthread_rwlock_rdlock(&rwlock);
        if(strncmp(buff,"end",3)==0)
        {
            break;
        }
        printf("fun:%s\n",buff);
        //memset(buff,0,128);
        
        int n=rand()%3+1;
        sleep(n);

        pthread_rwlock_unlock(&rwlock);
        n=rand()%3+1;
        sleep(1);
    }
}
void* fun1(void* arg)//读数据
{
    while(1)
    {
        pthread_rwlock_rdlock(&rwlock);
        if(strncmp(buff,"end",3)==0)
        {
            break;
        }
        printf("fun1:%s\n",buff);
        //memset(buff,0,128);
        
        int n=rand()%3+1;
        sleep(n);

        pthread_rwlock_unlock(&rwlock);
        n=rand()%3+1;
        sleep(1);
    }
}
int main()
{
    srand((unsigned int)(time(NULL)*time(NULL)));
    pthread_rwlock_init(&rwlock,NULL);//初始化
    pthread_t id[2];
    int res=pthread_create(&id[0],NULL,fun,NULL);//创建线程
    int r=pthread_create(&id[1],NULL,fun1,NULL);
    assert(res==0);

    while(1)//写数据
    {
        pthread_rwlock_wrlock(&rwlock);
        printf("input:");
        fgets(buff,127,stdin);
        pthread_rwlock_unlock(&rwlock);
        if(strncmp(buff,"end",3)==0)
        {
            break;
        }
        int n=rand()%3+1;
        sleep(1);
    }
    
    pthread_join(id[0],NULL);
    pthread_join(id[1],NULL);
    pthread_rwlock_destroy(&rwlock);
}


           

运行结果如下:

【Linux】——线程同步与互斥1、同步和互斥的概念2、线程同步方式

可以看到fun1,fun加了读锁,可以同时读取数据,主线程加了写锁,只能一个线程写数据。

2.4信号量(同步与互斥)

1、概念

这个信号量和进程间用的信号量作用类似,当线程访问一些有限的公共资源时,就必须做到线程间同步访问。其实就类似于一个计数器,有一个初始值用于记录临界资源的个数。

2、信号量特点

  • 信号量一般用于解决线程同步问题

    ,即协调线程对于临界资源的关系,就像一个红绿灯一样,但是不代表不能解决互斥问题。
  • 当信号量的初始值为1时

    ,表示只有一个临界资源,

    可以看成互斥锁

  • ·信号量一般由一个线程释放,另一线程获取·,保证线程同步,这一点和锁有很大的区别。

3、信号量的类型和方法

  1. 初始化和销毁信号量

    信号量sem一般定义在线程共享的全局数据区,sem_init函数是将信号量sem的初始值设置为val,shared参数控制这个信号量是否可以在多个进程之间共享,但是Linux对此不支持,一般设置为0。具体实现如下:
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
           
  1. P操作和V操作

P操作是对信号量sem进行-1操作,如果结果小于0,则此函数调用会阻塞,直到有其他线程执行V操作。相反的,V操作就是对信号量sem进行加一操作。具体实现如下:

int sem_wait(sem_t *sem);//P操作
int sem_post(sem_t *sem);//V操作
           

4、实例

主线程负责接收用户输入,函数线程负责将用户输入打印到终端界面

分析:

两个线程对于buff临界资源的协作关系,即主线程写了,函数线程读,函数线程读了,主线程写;并不是竞争关系。

那么我们就设定两个信号量分别来控制主线程和函数线程,让它们达到一种协作关系:sem1控制函数线程,开始运行时函数线程阻塞等待主线程写数据,所以初值为0;sem2控制主线程,运行程序就开始对buff写数据,所以初值为1。那么两个线程的信号量操作如下:

  • P(sem2),此时sem2=0 → 主线程运行向buff中写数据 → 写完数据,V(sem1),释放信号,此时sem1=1,sem2=0,可以读,不可以写。 即两个线程并发运行,只有函数线程可以运行读取数据,因为可以进行P操作,主线程不能运行,在P(sem2)操作时就会阻塞。
  • P(sem1),此时sem1=0 → 函数线程运行读取数据 → 读取完毕,V(sem2),释放信号,此时sem1=0,sem2=1,可以写,不可以读。 即两个线程并发运行,只有主线程可以运行写入数据,因为可以进行P操作,函数线程不能运行,在P(sem1)操作时就会阻塞。
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <fcntl.h>

char buff[128] = {0};

sem_t sem1;
sem_t sem2;

void* PthreadFun(void *arg)
{
	//函数线程完成将用户输入的数据存储到文件中
	while(1)
	{
		sem_wait(&sem1);//P操作

		if(strncmp(buff, "end", 3) == 0)
		{
			break;
		}

		printf("fun :%s",buff);
		memset(buff ,0,128);
		
		sem_post(&sem2);//函数线程唤醒主线程
	}
}

int main()
{
	sem_init(&sem1, 0, 0);
	sem_init(&sem2, 0, 1);

	pthread_t id;
	int res = pthread_create(&id, NULL, PthreadFun, NULL);
	assert(res == 0);

	//主线程完成获取用户数据的数据,并存储在全局数组buff中
	while(1)
	{
		sem_wait(&sem2);
		printf("please input data: ");

		fgets(buff, 128, stdin);

		sem_post(&sem1);//主线程来唤醒函数线程

		if(strncmp(buff, "end", 3) == 0)
		{
			break;
		}
	}
	
	//等待线程的结束,只有该线程结束了,才能确保没有线程使用这个信号量了
	pthread_join(id,NULL);
	sem_destroy(&sem1);
	sem_destroy(&sem2);
	pthread_exit(NULL);
}
           
【Linux】——线程同步与互斥1、同步和互斥的概念2、线程同步方式

继续阅读