线程间通信
进程间通信:
进程间由于进程是各自独立的,各自有各自的虚拟地址空间,所以想要进程之间进行通信,就必须在两个进程之前创建一个通道,作为通讯的媒介。pipe、fifo、msg、shm都是这样的。
线程间通信:
线程间的通信,由于他们是同一个进程的执行流,共享同一个虚拟地址空间,所以大部分资源都是一样的,想要线程之间进行通信,只需要一个全局变量就可以,或者在创建线程之前的一个变量。
存在问题:
无论是进程间通信还是线程之前的通信,都涉及到对资源的访问,由于cpu是随时间片的切换轮询调度的,所以就很容易存在线程安全问题(这种问题在线程是是最明显的),要想达到安全、合理的通信,就必须达到临界资源访问的互斥和同步。
实现同步的方式:
在说线程安全时提到,互斥锁实现互斥,条件变量和互斥锁搭配就可以实现同步,其实posix标准下的信号量,也为进程间和线程间的通信实现了互斥。
信号量
posix标准的信号量,
实现进程间的同步与互斥,进程间的通信就这几种方式,它是使用共享内存实现的,多个进程都可以访问
实现线程间的同步与互斥,线程通信灵活,使用全局变量就可以实现通信。只要是临界资源就可以通信
systemV标准的信号量
本质是内核中的一个计数器
信号量的本质就是一个计数器+pcb等待队列
通过信号量可以实现同步和互斥,主要是同步
同步的实现:
通过自身的计数器对资源进行计数,并且通过计数器的资源计数,判断进程/线程是否符合访问资源的条件。
互斥的实现:保证计数器的计数不大于1,就保障了资源只有一个,同一时间只有一个pcb可以访问临界资源,实现互斥。
代码操作:
1.定义信号量sem_t;
2.初始化信号量:int sem_init(sem_t* sem,int pshared,unsigned int value);
sem:定义一个信号量变量
pshared:0 - 用于线程之间 / 非0 - 用于进程之间
value:初始化信号量的初值,初始资源有多少计数就是多少;
返回值:成功返回0,失败返回-1;
3.P操作
在访问临界资源之前,先访问信号量,判断是否可以访问,计数-1;
int sem_wait(sem_t* sem); -- 通过自身计数判断是否满足访问条件,不满足则直接一直阻塞线程/进程
int sem_trywait(sem_t* sem); -- 通过自身计数判断是否满足条件,不满足则直接报错返回,ETIMEDOUT
int sem_timedwait(sem_t* sem,struct timespace* abs_timeout);
4.V操作
促使访问条件满足之后,计数+1,唤醒阻塞的线程/进程
int sem_post(sem_t* sem); -- 通过信号量唤醒自己阻塞队列上的pcb;
5.销毁信号量
void sem_destroy(sem_t* sem);
通过信号量实现一个生产者消费者模型/线程安全的环形队列
1.实现一个线程安全的环形队列 -- 使用数组实现
2.读写指针控制下标,实现出队入队
实现同步:
使用计数器对资源进行计数
对生产者来说,空闲节点就是资源 --_sem_pro 控制写指针
对消费者来说,数据节点就是资源 --_sem_cum 控制读指针
实现互斥:
这个过程中,队列的资源是都需要访问的
-- 计数器初始化为1,访问之前进行p操作,访问完成之后进行v操作,实现锁的功能
#include<iostream>
#include<cstdio>
#include<semaphore.h>
#include<vector>
using namespace std;
#define MAX_NODE 5
class RingQueue
{
private:
vector<int> q;
int capacity;
int _write_ptr; //写指针(下标)
int _read_ptr;
private:
sem_t _sem_data; sem_t _sem_emp; //两个信号量代表两种资源,分别控制空数据和有数据
sem_t _sem_lock;
public:
//RingQueue(){}
RingQueue(int maxq = MAX_NODE):q(maxq),capacity(maxq),_write_ptr(0),_read_ptr(0)
{
//sem_init(信号量,标志位,资源数) 标志位:0-线程 1-进程
sem_init(&_sem_data,0,0);
sem_init(&_sem_emp,0,maxq);
sem_init(&_sem_lock,0,1); //实现互斥的资源1表示可以访问,p操作之后小于0表示不能访问,即加锁的过程
}
~RingQueue()
{
sem_destroy(&_sem_data);
sem_destroy(&_sem_emp);
sem_destroy(&_sem_lock);
}
public:
//在这个过程中,sem本身自带同步与互斥,操作是原子性的,所以对数据节点和空闲节点的操作是原子性的,不需要加锁
//semwait过来之后是直接阻塞的,如果在操作之前进行加锁,等进程过来的时候,可没有解锁这一步
//这里面唯一需要保护的资源就是要插入数据的位置,即 写指针,所以必须在空闲节点-1之后加锁
bool push(const int& data)
{
sem_wait(&_sem_emp); //空闲节点-1
sem_wait(&_sem_lock); //对锁进行p操作,即加锁 这两步,必须是先进行p操作
q[_write_ptr] = data;
_write_ptr = (_write_ptr+1) % capacity; //环形队列
sem_post(&_sem_data); //对消费者进行v操作,+1即唤醒进程
sem_post(&_sem_lock); //解锁 最后这两部先后顺序没有要求
return true;
}
bool pop(int *data)
{
sem_wait(&_sem_data); //数据节点-1
sem_wait(&_sem_lock);
*data = q[_read_ptr];
_read_ptr = (_read_ptr +1)&capacity;
sem_post(&_sem_emp); //空闲节点+1
sem_post(&_sem_lock);
return true;
}
};
void* producter(void* arg)
{
int data = 0;
RingQueue* q = (RingQueue*)arg;
while(1)
{
q->push(data);
printf("put data %d\n",data++); //注意插入和打印这块不是原子操作,如果线程多了,我们通过打印看到的结果有
有可>能是不对的,但是其实际上只要逻辑闭环,就没有问题
//后++,插入之后数据变化一下
}
return NULL;
}
void* customer(void* arg)
{
RingQueue* q = (RingQueue*)arg;
while(1)
{
int data =0; //由于我们接受数据这里,消费者拿到数据处理,这里的data是一个输出型参数,初不初始化一样
q->pop(&data);
printf("get data: %d\n",data--);
}
return NULL;
}
int main()
{
pthread_t ptid[3],ctid[3];
RingQueue q;
int ret;
for(int i =0;i<3;++i)
{
ret = pthread_create(&ptid[i],NULL,producter,&q);
if(ret != 0)
if(ret != 0)
{
printf("create producter thread %d failed!\n",i);
return -1;
}
ret = pthread_create(&ctid[i],NULL,customer,&q);
if(ret != 0)
{
printf("create customer thread %d failed!\n",i);
return -1;
}
}
for(int i=0;i<3;++i)
{
pthread_join(ptid[i],NULL);
pthread_join(ctid[i],NULL);
}
return 0;
}
和条件变量进行区分:
1.条件变量和互斥锁搭配使用实现同步,条件变量可以实现同步
2.信号量是原子操作,cond_wait中解锁和阻塞是原子的
3.pthread_cond_wait()这个接口有三步操作,解锁,阻塞和加锁
而sem_wait()这个接口直接阻塞
所以cond在访问资源之前进行加锁,wait就可以解锁阻塞,sem在进行P操作之后,使用互斥sem加锁
如果在sem P操作之前加了锁,sem PV操作完成唤醒一个进程来再一次访问时,可不会解锁,直接阻塞,程序卡死。
这个顺序不能改变,否则就是逻辑闭环,死锁