进程线程 协程 通信理解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/yangxiaodong88/article/details/86522189

线程与进程之间的区别

区分这些之间的不同以及之间的通信原则, 也是真正理解和使用并发的基础, 如果这些关系理解不透彻那么也 并发开发也是一句空话。
对理解数据库设置的连接池, 全局变量 最后算应该有多少个连接数的时候用的到。

区别理解

每个进程都有自己的地址空间。 两个进程中的地址即使相同, 实际指向的位置也是不同的。

进程间通信一般都是通过操作系统的公共区进行。

同一进程中的线程因同属于一个地址空间, 可直接通信。
线程不仅是系统内部独立运行的实体,而且是独立竞争资源的实体。

线程也被称为轻权进程, 同一进程的线程共享全局变量和内存。使得线程之间共享数据是很容易的, 但会带来某些共享数据的互斥问题。
父子进程的派生是非常昂贵的,而且父子进程的通讯需要ipc或者其他方法来实现,比较麻烦。而线程的创建就花费少得多,并且同一进程内的线程共享全局存储区,所以通讯方便。

线程的缺点也是由它的优点造成的,主要是同步,异步和互斥的问题,值得在使用的时候小心设计

只有进程间需要通信, 同一进程的线程共享地址空间, 没有通信的必要。但是要做好同步 /互斥mutex 来保护共享的全局变量。线程有自己的栈,同步/互斥是原语primitives.
进程间的通信无论是信号, 管道pipe还是共享内存都是由操作系统保证的, 是系统调用。

线程间通信:由于多线程共享地址空间和数据空间, 所以多线程间的通信是一个线程的数据可以直接提供给其他的线程使用,而不必通过操作系统, 也就是内核的调度。
进程间通信: 他的数据空间的独立性决定了他的通信相对比较复杂, 需要通过操作系统。以前进程间的通信只能是单机版的,现在操作系统都继承了基于套接字(socket)的进程间的通信机制。这样进程间的通信就不局限于单台计算机了,实现了网络通信

进程间的通信方式

  • 管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
  • 有名管道 (namedpipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
  • 信号量(semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  • 消息队列( messagequeue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  • 信号 (sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
  • 共享内存(shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
  • 套接字(socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。

线程间的通信方式

  • 锁机制:包括互斥锁、条件变量、读写锁
  • 互斥锁提供了以排他方式防止数据结构被并发修改的方法
  • 读写锁允许多个线程同时读共享数据,而对写操作是互斥的
  • 条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用
  • 信号量机制(Semaphore):包括无名线程信号量和命名线程信号量
  • 信号机制(Signal):类似进程间的信号处理

线程间的通信目的主要用于线程同步, 所以线程没有像进程通信中用于数据交换的通信机制

进程间通信和线程同步

七大通信方式
signal、file、pipe、shm、sem、msg、socket

1,signal

信号通信的目的:某某事件发生!此时需要处理什么,进程间(可以是不相关的进程)传递信号

场景:信号又被称之为中断,需要处理什么对应的是中断处理函数,此时设置断点,形参入栈,保存现场信息,然后去执行中断处理函数,当处理完成之后,恢复现场信息,程序继续往下执行

Linux下可以通过kill -l查看其所有信号(其一共64种信号)

发送信号:kill(pid, 信号) //对指定的进程发送什么信号

raise(信号) <==> kill(getpid(), 信号) //就是给自己发送指定的信号

alarm(秒数) :定时产生一个SIGALRM信号,调用alarm方法之后,只会产生一次该信号

接收信号:signal(信号,函数指针) //对该信号接收,并调用自己的函数指针进行处理

信号通信方式的局限性:不能够传递复杂的、有效的、具体的数据

2,file

每打开一个文件,就会产生一个文件控制块,而文件控制块与文件描述符是一一对应的,通过对文件描述符的操作进而对文件进行操作

文件描述符的分配原则:编号的连续性(节省编号的资源)

通过文件系统对文件描述符的读/写控制,进程间一方对文件写,一方对文件读,达到文件之间的通信;可以是不相关进程间的通信

使用的API:write()和read()

为了能够实现两个进程通过文件进行有序的数据交流,还得借助于信号的处理机制

(1)、通过pause()等待对方发起一个信号,已确认可以开始执行下一次读/写操作;

pause():只要接受到任何的信号,立马就可以往下执行

(2)、通过kill(,SIGUSR1)方法向对方发出明确的信号:可以开始下一步执行(读、写)

缺点:i、文件通信没有访问规则,ii、(因为CPU > 内存 > 文件)是低速的

3,pipe

在通信的进程间构建一个单向的数据流动的通道,数据通过管道从一个进程流向另一个进程是具有时间先后顺序的,所以是半双工通信;管道文件是一种临时文件,不是磁盘上真真正正的文件,是一块内存区域

分为:

fd[0]:读出数据

fd[1]:写入数据

无名管道:只能用于亲缘关系的父子进程,fd = pipe(),得到的是管道文件描述符,通过fd,用的是write()和read()读写数据;

达到双方通信:得用2个管道,达到可以发多句话,的fork()子进程处理

有名管道:非父子进程间通信mkfifo()

mkfifo会在文件系统中创建一个管道文件,然后使其映射内存的一个特殊区域,凡是能够打开mkfifo创建的管道文件进程(通过这个文件描述符),都可以使用该文件实现FIFO的数据流动

mkfifo(文件名, O_CREAT | O_EXCL | 0755);创建了2个管道文件,在客户端创建一个读的管道文件,在服务器创建一个写的管道文件,然后当做文件操作即可

socketpair可以创建双向管道,fd[0]、fd[1]都是同时具有读和写的属性;

优点:(1)、有强制的访问规则FIFO,(2)、用内存模仿文件,也就是用文件的方式操作内存

管道通信的特点:

(1)、如果管道为空,从管道读取数据的一方会阻塞。直到管道中有新的数据为止

(2)、管道的数据通信具有FIFO特性,这样可以避免数据的混乱

(3)、管道数据的读取与发送并没有次数限制,而是管道是否为空时最重要的指标

(4)、这种管道的使用具有一个最大的局限性:只适用于父子进程之间。从程序的设计中可以看到,管道的创建是父进程完成的,而且是在创建子进程之前,从而才使得子进程拥有了管道文件描述符,才能够使得父子进程约定持有管道的入口或出口

(5)、一个管道只能实现单向的数据流

使用ipcs可以查看当前系统中IPC资源的情况。ipcrm -m shmid ipcrm -s semid

ipcrm -q msgid

4,shm

各个进程都能够共同访问的共享的内存区域;是独立于所有的进程空间之外的地址区域; (不相关)进程之间的通信

进程对于共享内存的操作与管理主要是:

(1)、申请创建一个共享内存区域(操作系统内核是不可能主动为进程创建共享内存的!),操作系统内核得到申请然后创建

(2)、申请使用一个已存在的共享内存区域

(3)、申请释放共享内存区域(操作系统内核也是不可能主动释放共享内存区域的!),操作系统内核得到申请然后释放

说明key_t key

i>、key_t是一个long类型,是IPC资源外部约定的key(关键)值,通过key值映射对应的唯一存在的某一个IPC资源

ii>、通过key_t的值就能够判断某一个对应的共享内存区域在哪,是否已经创建等等。

iii>、一个key值只能映射一个共享内存区域,但同时还可以映射一个信号量,而且还能同时映射一个消息队列资源,于是就可以使用一个key值管理三种不同的资源

在这里插入图片描述
key_t值的产生,有两种方式:

i>、把key值写死; //自己直接写一个数字即可

ii>、根据文件的inode编号生成。需要调用的API:ftok("./tmp/a.c", 3)方法,该方法是获取指定文件的inode编号在根据第二个参数计算得到最终的一个整型量。

shm的使用:

i>、建立进程与共享内存的映射关系

ii>、读/写(直接使用指针即可

iii>、如果对于共享内存的使用结束,此时就要断开与共享内存的映射

对于第一步来说,需要使用的API:shmat()方法。

对于第三步来说,需要使用的API:shmdt()方法。

被映射正在使用共享内存是否此时可以执行删除操作呢

是,虽然可以执行删除操作,却不能将其直接删除掉。而是做了2个操作

i>、将其状态置为dest(可回收状态)

ii>、将其key值置为0x00000000,IPC_PRIVATE值

当共享内存处于dest(待回收状态),则将其资源设为"私有"(只能将该共享资源分享给其子进程,其它进程无法创建于该资源的使用),当所有的使用该共享内存的进程都退出,此时操作系统才回收共享内存

共享内存的控制

共享内存的控制信息可以通过shmctl()方法获取,会保存在struct_shmid_ds结构体中

共享内存的控制主要是shmid_ds,即就是共享内存的控制信息

cmd:看执行什么操作(1、获取共享内存信息;2、设置共享内存信息;3、删除共享内存)

API:int shmctl(int shmid, int cmd, struct shmid_ds *buf)

5,sem

原因:进程在访问共享资源是存在冲突的,必须的有一种强制手段说明这些共享资源的访问规则------>信号量

sem:表示的是一种共享资源(空闲)的个数,对共享资源的访问规则

i>、用一种数量去标识某一种共享资源的个数(空闲)

ii>、当有进程需要访问对应的共享资源的时候,则需要先查看(申请),根据资源对应的当前可用数量进行申请。(申请所需要使用的资源个数)

iii>、资源的管理者(操作系统内核),就使用当前的资源个数减去要申请的资源个数,结果 >=0,表示有可用资源,允许该进程继续访问;否则表示资源不可用,则告诉进程(暂停或者立即返回)

iv>、资源数量的变化就表示资源的占用和释放。占用:使得可用资源减少;释放:使得可用资源增加

创建信号量集:int semid = semget(key_t key, int nsems, int semflg)

初始化信号量:

信号量ID事实上是信号量集合的ID,一个ID对应的是一组信号量。此时就使用信号量ID设置整个信号量集合,这种操作分为2种大的可能性

i>、针对信号量集合中的一个信号量进行设置;信号量集合中的信号量是按照数组的方式被管理起来的,从而可以直接使用信号的数组下标来进行访问

ii>、针对整个信号量集和进行统一的设置。

需要使用的API:semctl()方法。

int semctl(int semid, int semnum, int cmd, …);

cmd参数

在这里插入图片描述

第四个参数 可变参

如果cmd是GETALL、SETALL、GETVAL、SETVAL…的话,则需要提供第四个参数。第四个参数是一个共用体,这个共用体在程序中必须的自己定义(作用:初始化资源个数),定义格式如下:
在这里插入图片描述
信号量的操作:

API:semop()方法。 (op:operator操作)

int semop(int semid, struct sembuf *sops, unsigned nsops);

第二个参数需要借助结构体struct sembuf:
在这里插入图片描述
通过下标直接对其信号量sem_op进行加减即可

信号量的特征:

如果有进程通过信号量申请共享资源,而且此时资源个数已经小于0,则此时对于该进程,有两种可能性:等待资源,不等待。

如果此时进程选择等待资源,则操作系统内核会针对该信号量构建进程等待队列,将等待的进程加入到该队列之中。

如果此时有进程释放资源,则会:(1)、先将资源个数增加;(2)、从等待队列中抽取第一个进程;(3)、根据此时资源个数和第一个进程需要申请的资源个数进行比较,结果大于0,则唤醒该进程;结果小于0,则让该进程继续等待。

所以信号量的操作和共享内存一般联合使用来达到进程间的通信

6,msg

就是在进程间架起通道,从宏观上看是一样的,但是管道在字节流上是连续的,消息队列在发送数据时,分为一个一个独立的数据单元,也就是消息体,每个消息体都是固定大小的存储块,在字节流上不连续;

消息队列与管道不同的地方在于:管道中的数据并没有分割为一个一个的数据独立单位,在字节流上是连续的。然而,消息队列却将数据分成了一个一个独立的数据单位,每一个数据单位被称为消息体。每一个消息体都是固定大小的存储块儿,在字节流上是不连续的。

创建消息队列

int msgget(key_t key, int msgflg); //创建0个消息队列

消息的发送和消息的接收

在发送消息的时候动态的创建消息队列;
在这里插入图片描述

(1)、msgsnd()方法在发送消息的时候,是在消息体结构体中指定,当前的消息发送到消息队列集合中的哪一个消息队列上。

(2)、消息体结构体中就必须包含一个type值,type值是long类型,而且还必须是结构体的第一个成员。而结构体中的其他成员都被认为是要发送的消息体数据。

(3)、无论是msgsnd()发送还是msgrcv()接收时,只要操作系统内核发现新提供的type值对应的消息队列集合中的消息队列不存在,则立即为其创建该消息队列

总结:为了能够顺利的发送与接收,发送方与接收方需要约定:i>、同样的消息体结构体;(2)、发送方与接收方在发送和接收的数据块儿大小上要与消息结构体的具体数据部分保持一致! 否则:将不会读出正确的数据

重点注意:

消息结构体被发送的时候,只是发送了消息结构体中成员的值,如果结构体成员是指针,并不会将指针所指向的空间的值发送,而只是发送了指针变量所保存的地址值。数组作为消息体结构体成员是可以的。因为整个数组空间都在消息体结构体中

在这里插入图片描述
long mtype制定消息队列编号,下面的数组才是要发送的数据,计算大小,也是这个数组所申请的空间大小。接收方倒数第二个参数为:mtype的值(制定的消息队列编号)。

均是指针连接;

在接收的时候的指明是哪个消息队列进行接收(比发送多了一个参数);

7,socket

网络之间不同进程间通信,相当于网络编程部分了

线程

(1)、线程的API函数:
在这里插入图片描述

(2)、线程间的协作

在一个进程中会出现多个线程会访问同一个内存区域,因此就需要使用一种线程间的协作手段来处理
线程的同步机制主要有:互斥量,信号量,条件变量

1)互斥量 :出现了mutex,就为互斥量,为锁机制
在这里插入图片描述

(1)、在一个lock(加锁)和unlock(解锁)之间,形成的叫做:临界区域

线程同步:阻塞别人而完成自己(是不是就是让别人等等)。利用互斥量达到同步,使封锁区域最小化

(2)、加锁后,没有解锁————>将发生阻塞(不能再进行加锁)

(3)、利用互斥量,将程序执行的不确定顺序变为了确定性的顺序

  1. 条件变量

i>静态初始化:
在这里插入图片描述

ii>、动态初始化

分别调用pthread_mutex/cond_init,pthread_mutex/cond_destroy()初始化;

条件变量针对死锁情况(就是没有出现unlock),此时调用pthread_cond_wait()方法也可以进行解锁;也就是说wait()函数会在阻塞之时进行解锁。

pthread_cond_wait()方法是:在阻塞之时,自动解锁。

该方法在遇到pthread_cond_signal()时,唤醒等待的wait()方法,但是不直接执行wait()其后的语句,而是接着原先pthread_cond_signal()其后的方法继续执行,直到遇到pthread_mutex_lock()锁时,此时,转到wait()其后的方法执行。

在这里利用的是pthread_cond_wait()和pthread_cond_signal()方法

(3)线程间同步

锁机制:互斥锁、条件变量、信号量、读写锁

互斥锁:提供了以排他方式数据结构被并发修改的方法

读写锁:写锁优先抢占资源,读锁允许多个线程共同读共享数据,而写锁操作是互斥的

条件变量:以原子方式阻塞进程,直到某个特定条件为真为止

一般情况下:互斥锁起保护作用,条件变量和互斥锁一起使用

总结:线程间通信的目的主要用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制

猜你喜欢

转载自blog.csdn.net/yangxiaodong88/article/details/86522189