进程线程通信同步以及对应原型函数

进程通信和同步(进程的同步是在进程通信基础上使用的)

进程通信(参考APUE)

主要方式: 管道、信号、信号量、消息队列、共享内存、套接字

管道

又分为有名管道和无名管道,管道都是半双工的

  • 有名管道:任意进程之间的通信,有名管道就是FIFO,采用先进先出队列,只允许数据单向流动,linux |就是管道。
  • 无名管道:父子进程通信

匿名管道 用于解决父子关系的,用fork来创建子进程。

#include<unistd.h>
int pipe(int pipefd[2]);

数组中存着前者是管道的读端,后者是写端,一个进程可以关闭某一个端口来实现自己到底是读还是写。
如fork出一个子进程,想要父写子读的话,父进程就关闭读端 close(pipefd[0]) ,子进程就关闭写端close(pipefd[1]); 创建好管道之后就可以通过read 和 write来进行读写操作。
write(pipefd[1],buf,strlen(buf)); read(pipefd[0],buf,strlen(buf));

样例代码参照底部

有名管道

fifo创建一个命名管道,可以解决非血缘关系的进程之间的通信。
管道的是实现方式也是通过文件来实现的。
首先命令行 mkfifo filename就可以创建一个管道文件。
然后在不同进程里传入文件名即可进行读写,open,write,close open,read,close

信号

信号就是由用户、系统和进程发送给目标进程的信息,通知目标进程中某个状态的改变或者异常。

信号的产生分为硬中断和软中断

  1. 终端中驶入特殊字符来产生信号,如 ctrl+某个信号
  2. 系统异常,访问非法内存,浮点数异常
  3. 系统状态变化。设置了alarm定时器,到时间会引起信号
  4. kill指令或者函数。

函数原型: sighandler_t signal(int signum,sighandler_t handler),handler一般是一个函数,这个函数传入信号的数值,根据信号数值表设计判断就可以进行不同的处理了。

SIGINT Term 键盘输入以终端进程(ctrl + C)

共享内存

mmap(系统调用)

将磁盘文件的一部分直接映射到进程的内存中,mmap设置了两种机制:共享和私有
共享映射:内存中对文件进行修改,那么磁盘中对应的文件也会被修改。
私有映射:内存中的文件和磁盘中的文件是独立关系的,两者进行修改都不会对对方造成影响

函数原型

#include<sys/mman.h>
void *mmap(void* addr,size_t length, int prot , int flags ,int fd, off_t offset);
int munmap(void* addr,size_t length);

mmap存在一个问题
当flags为MAP_SHARED的时候,即使修改了内存,并不能保证映射文件能够马上更新,映射文件的更新是由内核虚拟内存调度算法进行的。因此如果两个进程同时写会导致映射文件内容的不可预知性。

shmget(System V)

需要进行同步

int shmget(key_t key, size_t size, int shmflg);//创建共享内存,key命名
void *shmat(int shm_id, const void *shm_addr, int shmflg);//启动共享内存,把共享内存连接到当前进程地址空间,因为是一个void*指针,所以可以用任意对象指针来改变。如 shared = (string *)shm; 然后就可以对shared进行修改就是修改共享内存。
int shmdt(const void *shmaddr);//分离共享内存
int shmctl(int shm_id, int command, struct shmid_ds *buf);//控制共享内存

消息队列

消息队列是在内存中独立于进程的一段内存区域,创建了消息队列,任何进程只要有访问权限就可以访问消息队列。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);//创建一个消息队列 返回msqid
int msgctl(int msqid,int cmd,struct msqid_ds *buf);// 获取和设置消息队列的属性
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);//发送到消息队列, msgp就是发送的数据,数据结构中第一个字段必须为long类型
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);//接受消息。

信号量

int semget(key_t key,int nsems,int flags);//第二个参数是信号量集的个数
int semctl(int semid,int semnum,int cmd,su);//根据cmd来判断是SETVAL还是GETVAL
semop(int id,struct sembuf sb[],int len)// sembuf是一个结构体
struct sembuf{      short sem_num,  //信号量的下标      
                    short sem_op,     // 1表示V操作,-1表示P操作     
                    short  sem_flg   //一般填0,表示如果信号量为0就阻塞;

socket(不在此处介绍)

线程同步

因为线程是共享进程的内存空间的,因此线程基本上是不需要进行数据交换,主要要做的是线程之间的同步。

  1. 锁机制:包括互斥锁/互斥量、读写锁、自旋锁、条件变量
    • 互斥锁 互斥量:用排他的方式来限制数据结构被并发修改。
    • 读写锁:多个线程可以同时读,但是写互斥
    • 自旋锁:互斥锁会引起调用者睡眠,但是自旋锁不会,自旋锁会循环检测资源是否可以访问. 因为自旋锁的使用者一般保持锁很短的时间,因此选择自旋等待而不是睡眠可以提高处理效率.
  2. 信号量机制:无名线程信号量 有名线程信号量
  3. 信号机制:类似于进程的信号
  4. 屏障:屏障允许每个线程等待,直到所有的合作线程都达到某一个点,然后从该点继续执行。
  5. 条件变量

互斥锁

  • 常见问题,两个线程给一个引用变量加1一共10000次,那么最后的结果会是多少呢?比如 sums=sums+i
    最后的结果是不可预知的,因为两个线程使用的一个共享资源,有可能某个线程加的过程中还未赋值的时候切换到另一个线程,另一个线程上加了上去同时赋值了sums,然后切换回原来的线程,但是原来的线程已经完成了sums+i的运算,于是就把sums的值重新覆盖了。

出现这个的原因是 sums++/sums+=1/sums=sums+1 这些操作都不是原子操作,都是通过操作符函数来实现的,操作符函数是对地址的值或者临时变量上加1 然后返回数值;这是分两步走的,所以多线程下会出现不可预知性。

要解决上面这种情况就要多线程之间的互斥锁

  • 第一种锁
    std::mutex mylock; mylock.lock() sums+=1; mylock.unlock();
    线程传入参数 std::thread t1(work,std::ref(val),std::ref(mylock)); 如果线程需要引用参数就必须要用std::ref
  • 第二种锁
{
    std::mutex mylock;
    std::lock_guard<std::mutex> mylock_guard(mylock);//当对象被创建的时候上锁,被销毁的时候解锁; 这个对象只有构造和析构函数两个函数。
}
  • 第三种
{
    std::mutex mylock;
    std::unique_lock<mutex> ulock(mylock);//当对象被创建的时候上锁,被销毁的时候解锁; 这个对象只有构造和析构函数两个函数。
}

unique_lock相比于lock_guard来的复杂一些,lock_gurad只有两个函数,而unique_lock可用的函数更多,因此从操作上来说更加的灵活。

条件变量

condition_variable是一个类,通常搭配互斥量mutex来用。 条件变量一般是在消费者生产者中使用,因为可以用条件变量来唤醒。
需要知道的两个函数是wait和notify_*函数。

notify_one每次只会唤醒一个线程。如果用notify_all就会唤起所有线程,但是每次只有一个线程能够继续后面的工作,剩下的线程又只能继续等待,这样多个线程等待一个唤醒的情况就是惊群效应。

惊群效应消耗了什么资源?
Linux会对每一个线程(进程)进行调度、上下文切换。 上下文切换过高使得CPU就像一个搬运工,在寄存器和运行队列中奔波。
直接的消耗包括 CPU 寄存器要保存和加载(例如程序计数器)、系统调度器的代码需要执行。间接的消耗在于多核 cache 之间的共享数据。
为了确保只有一个进程(线程)得到资源,需要对资源操作进行加锁保护,加大了系统的开销。

参考代码见样例代码。

读写锁

  1. 如果一个线程用读锁锁住了临界区,那么其他的读线程还是可以用读锁来访问,这样就可以由多个线程并行操作。 这时候来一个写锁的话就会被阻塞, 写锁阻塞后,后来的读锁也都会被阻塞,这样做就可以避免读锁长期占用临界区资源,防止写锁饥饿。
  2. 如果一个线程的写锁锁住了临界区,那么之后无论什么锁都会发生阻塞。

实现方法上有两种,一种是C14的新特性,一种是POSIX下的pthread中实现的读写锁的机制。
C14
C14中提供了一个新的锁方式 std::shared_lock 能够以共享模式进行锁定,其实就是读锁。 但是mutex是不可以多次加锁的因此C14中还提供了std::shared_timed_mutex

std::lock_guardstd::shared_timed_mutex writerLock(shared_mutex);//以排他性方式上锁unique_lock可以这么做。
std::shared_lockstd::shared_timed_mutex readerLock(shared_mutex)//;以共享所有权方式上锁
上面两者都是退出作用域就可以自动解锁。

pthread
初始化读写锁 pthread_rwlock_init 语法
读取读写锁中的锁 pthread_rwlock_rdlock 语法
读取非阻塞读写锁中的锁pthread_rwlock_tryrdlock 语法
写入读写锁中的锁 pthread_rwlock_wrlock 语法
写入非阻塞读写锁中的锁pthread_rwlock_trywrlock 语法
解除锁定读写锁 pthread_rwlock_unlock 语法
销毁读写锁 pthread_rwlock_destroy 语法

自旋锁

自旋锁是一种用于保护多线程共享资源的锁,与一般的互斥锁(mutex)不同之处在于当自旋锁尝试获取锁的所有权时会以忙等待(busy waiting)的形式不断的循环检查锁是否可用。在多处理器环境中对持有锁时间较短的程序来说使用自旋锁代替一般的互斥锁往往能提高程序的性能。

CAS操作

CAS(Compare and Swap),即比较并替换,实现并发算法时常用到的一种技术,这种操作提供了硬件级别的原子操作(通过锁总线的方式)

bool CAS(V,A,B)
{
    if(V==A){
        swap(V,B);
        return true;
     }
     return false;
}

自旋锁的用途,本质上是希望使得一个线程在不满足情况下,一直处于轮询状态;x,那么伪代码逻辑

b=true
while(CAS(flag,false,b)==false);//如果flag==true那么就一直循环,如果flag==false就把flag=b,同时退出循环
//do something
flag=false;

样例代码

  1. 无名管道

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>

int main(void){
        char buf[1024] = "Hello Child\n";
        char str[1024];
        int fd[2];
        if(pipe(fd) == -1){
                perror("pipe");
                exit(1);
        }
        pid_t pid = fork();
        // 父写子读 0写端 1读端
        if(pid > 0){
                printf("parent pid\n");
                close(fd[0]);                    // 关闭读端
                sleep(5);
                write(fd[1], buf, strlen(buf));  // 在写端写入buf中的数据
                wait(NULL);
                close(fd[1]);
        }
        else if(pid == 0){
                close(fd[1]);                   // 关闭写端
                int len = read(fd[0], str, sizeof(str));   // 在读端将数据读到str
                write(STDOUT_FILENO, str, len);
                close(fd[0]);
        }
        else {
                perror("fork");
                exit(1);
        }
        return 0;}

2.信号量

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
using namespace std;

void sig_handler(int signum)
{
    if(0 > signum)
    {
        fprintf(stderr,"sig_handler param err. [%d]\n",signum);
        return;
    }
    if(SIGINT == signum)
    {
        printf("Received signal [%s]\n",SIGINT==signum?"SIGINT":"Other");
    }
    if(SIGQUIT == signum)
    {
        printf("Received signal [%s]\n",SIGQUIT==signum?"SIGQUIT":"Other");
    }

    return;
}

int main(int argc,char **argv)
{
    printf("Wait for the signal to arrive.\n ");

    /*登记信息*/
    signal(SIGINT,sig_handler);
    signal(SIGQUIT,sig_handler);

    pause();
    pause();

    signal(SIGINT,SIG_IGN);
    return 0;
}
  1. 条件变量+mutex

#include <iostream>
#include <thread>
#include <mutex>
#include <queue>
#include <windows.h>
#include <condition_variable>

std::mutex mtx;        // 全局互斥锁
std::queue<int> que;   // 全局消息队列
std::condition_variable cr;   // 全局条件变量
int cnt = 1;           // 数据

void producer() {
        while(true) {
                {
                        std::unique_lock<std::mutex> lck(mtx);//在这个作用域内lck可以使用,创建即上锁,销毁即解锁。
                        // 在这里也可以加上wait 防止队列堆积  while(que.size() >= MaxSize) que.wait();
                        que.push(cnt);
                        std::cout << "向队列中添加数据:" << cnt ++ << std::endl;
                        // 这里用大括号括起来了 为了避免出现虚假唤醒的情况 所以先unlock 再去唤醒
                }
                cr.notify_all();       // 唤醒所有wait
        }}

void consumer() {
        while (true) {
                std::unique_lock<std::mutex> lck(mtx);
                while (que.size() == 0) {           // 这里防止出现虚假唤醒  所以在唤醒后再判断一次
                        cr.wait(lck);
                }
                int tmp = que.front();
                std::cout << "从队列中取出数据:" << tmp << std::endl;
                que.pop();
        }}

int main(){
        std::thread thd1[2], thd2[2];
        for (int i = 0; i < 2; i++) {
                thd1[i] = std::thread(producer);
                thd2[i] = std::thread(consumer);
                thd1[i].join();
                thd2[i].join();
        }
        return 0;}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章