C++多核高级编程 - 06 并发任务的通信和同步(1) 通信和同步

来源:互联网 发布:c语言 根号 编辑:程序博客网 时间:2024/06/08 04:03

一, 依赖关系

当进程或线程相互之间要求通信或协作完成一个共同的目标时,他们之间便存在了依赖关系。


通信依赖:当TaskA要求来自TaskB的数据才能执行它的工作时,TaskA和TaskB之间便存在了通信依赖关系。


协作依赖:当TaskA要求的资源被TaskB所拥有,而且在TaskA使用该资源前TaskB必须释放它则他们之间存在协作依赖关系。


可以通过列举应用程序中进程或线程间存在的可能依赖的数目来理解总体的任务关系。

一旦列举了可能的依赖,便可以确定哪些线程必须为通信和同步进行编码。这是非常有用的描述程序总体结构和关系的方法。

例如:线程A B C,A通信依赖于B,B协作依赖于C,则可以得到依赖矩阵:

 ABCA0C0B00SC000

但这种方法并不适用于线程数量非常巨大的情况,此时矩阵的规模会非常的大。这种复杂度的增加并不利于对于程序的分析。


二,进程间通信

进程有着自己独立的地址空间,一个进程中的数据或是一进程中发生的事情并不能够被另一个进程所了解。如果一个进程想要了解另一个进程中发生的事情,则需要使用操作系统提供的某种通信手段。一个进程向另一个进程发送数据,或者通过操作系统API使另一个进程知道某个事件,这被称为IPC(进程间通信)。

操作系统中有着多种IPC方式,每种方式在系统中的持久性也不一样。(持久性是指在对象在创建它的程序,进程或线程的执行期间或执行期间之外的存在性)IPC实体驻留在文件系统,内核空间或用户空间,其持久性也采取这种定义方式:文件系统持久性,内核持久性和进程持久性。

文件系统持久性:IPC对象会一直存在,直到显示的删除该对象。如果内核重新启动,对象将保持它的值。

内核持久性:IPC对象会一直存在,直到内核重启,或对象被显示的删除。

进程持久性:IPC对象会一直存在,直到创建它的进程关闭。

下面根据不同的IPC类型定义了他们的持久性:

IPC 类型名   称   空   间持 久 性进   程管道未命名进程相关FIFO路径名进程双方互斥量未命名进程相关条件变量未命名进程相关读写锁未命名进程相关消息队列Posix IPC名称内核双方信号量(基于内存)未命名进程相关信号量(命名的)Posix IPC名称内核双方共享内存Posix IPC名称内核双方

●     下面将简要介绍几种不同的IPC的方式:

环境变量和命令行参数

父进程和子进程共享他们的资源,通过使用posix_spawn 或exec函数父进程可以创建有着它精确副本的子进程,也可以使用新的值来对子进程进行初始化。环境变量保存系统相关的信息。

int posix_spawn(pid_t *restict pid, 

                               const char * restrict path, 

                               const posix_spawn_file_actions_t *file_actions, 

                               const posix_spawnattr_t attrp, 

                               char* restrict argv[restrict]

                               char * const envp[restrict]);

posix_spawn 可以通过argv 和envp参数将信息传递给新的进程,这是单向的、一次性通信,一旦创建了子进程,子进程对这些变量的改变都不会反应到父进程中。


文件

使用文件在进程间传递数据是传输或共享数据最简单、最灵活的方法之一。文件具有系统持久性,使用文件传递数据不受系统重启的影响。

当使用文件传递数据时,应遵循以下7个步骤

(1) 必须沟通文件的名词

(2) 必须验证文件是否存在

(3) 确保得到了访问文件的正确权限

(4) 打开文件

(5) 同步对文件的访问

(6) 在读/写 文件时,检查流是否正常以及是否位于文件的结尾处

(7) 关闭文件


文件描述符
文件描述符是进程用来标识一个打开文件的无符号整数。他们在父进程和子进程中间共享。文件描述符是在文件描述符表中的索引,该表是内核为每个进程维护的块。当创建一个子进程时,会为该子进程复制该描述符表,使得子进程和父进程有相同的文件访问。


共享内存

共享内存块可用来在进程间传递信息。内存块不属于共享该内存的任何进程。内存块同进程的地址空间是分隔的。进程通过临时性地将共享内存块同他自己的内存块连接起来,获得对共享内存的访问。如果进程 A B C使用共享内存块,任何进程对共享内存的改动都能被其他进程看到。共享内存可以被一个进程读写,并且为该进程保持打开。其他进程可以根据需要附加到共享内存或同共享内存分开。这使得大数据块的传递要比使用管道或FIFO要快得多。但当访问保存在共享内存中的数据时,要进行同步,并且注意控制。

POSIX 使用共享内存

#include <sys/mman.h>
void* mmap(void * addr, size_t len, int prot, int flags, int fd, off_t offset);
int mumap(void* addr, size_t len);
int shm_open(const char* name, int oflag, mode_t mode)
int shm_unlink(const char * name)


管道

用来在进程之间传送数据的通道,在使用时要求进程同时为活动的。

使用管道的一般规则

管道用于2个或多个进程之间;

一个进程(写入方)打开或创建管道,然后阻塞,直到另一个进程(读取方)打开相同的管道进行读或写。


管道分为匿名管道命名管道

匿名管道:用于在相关进程(父子进程)之间传送数据。使用fork()创建的进程。

命名管道:用于在相关进程或不相关进程间进行通信。使用posix_spawn()创建的进程。它是内核对象,有着内核持久性。

                    命名管道是特殊类型的文件,存在于文件系统中,当创建它的进程终止后,命名管道仍可保持,直到进程显示式的删除它。

                    命名管道中的数据并不保持。


命名管道的使用举例:

#include <sys/types.h>

#include <sys/stat.h>

int mkfifo(const char* pathname, mode_t mode);

int unlink(const char* pathname);

下面两段程序 pipe_writer.cpp 和 pipe_reader.cpp 摘自书中,但在redhat 9 中运行时管道可以被创建,运行时无法传送数据。但程序本身可以作为管道实现的一种参考。


pipe_writer.cpp

using namespace std;#include <iostream>#include <fstream>#include <sys/wait.h>#include <sys/types.h>#include <sys/stat.h>#include <iostream>#include <unistd.h>int main(int argc, char* argv[], char* envp[]){    fstream MyPipe;    cout << "make FIFO Pipe" << endl;    if (mkfifo("C1", 0666) ==-1)    {        cout<<"could not make pipe" << endl;    }    cout << "Pipe open" << endl;    MyPipe.open("C1",  ios::out);    // if using ios::out the program will be blocked at MyPipe << msg;                                     // if using ios::out | ios::in the program will not block any more    if (MyPipe.bad())    {        cout << "could not open pipe" << endl;        return -1;    }    else    {        cout << "Writting message" << endl;        string msg = "1 2 3 4 5 6 7 ";        for (int i = 0; i < 10; i++)        {            sleep(1);            cout << "Write msg " << i << endl;            MyPipe << msg;        }        cout << "Message writting complete" << endl;    }    return 0;}


pipe_reader.cpp

using namespace std;#include <iostream>#include <fstream>#include <sys/wait.h>#include <sys/types.h>#include <sys/stat.h>#include <string>#include <unistd.h>int main(int argc, char* argv[]){    fstream MyPipe;    int nType;    string strInput = "";    cout << "Open FIFO Pipe" << endl;    MyPipe.open("C1", ios::in);    if (MyPipe.bad())    {        cout << "could not open pipe" << endl;    }        while(!MyPipe.eof() && MyPipe.good())    {        sleep(1);        cout << "Reading message" << endl;        getline(MyPipe, strInput);        cout << "Msg: " << strInput << endl;    }    cout << "Close pipe" << endl;    MyPipe.close();    unlink("C1");    return 0;}


消息队列
消息队列是字符串或消息的链表。一个进程可以将数据写入队列中,然后终止,数据会保留在队列中,其他进程也可以对队列进行读写操作。消息队列具有内核的持久性。


消息队列的相关函数:
#include <myqueue.h>
mqd_t mq_open(const char* name, int oflag, mode_t mode, struct mq_attr *attr);
int mq_close(mqd_t mqdes);
int mq_unlink(const char* name);


struct mq_attr描述了消息队列的属性
struct mq_attr
{
    long mq_flag;
    long mq_maxmsg;  //maximum number of message allowed.
    long mq_msgsize; // maximum size of message
    long mq_curmsgs; //number of message currently in queue
}

int mq_getattr(mqd_t mqdes, struct mq_attr *attr)

int mq_setattr(mqd_t mqdes, struct mq_attr *attr, struct mq_attr* oattr);

int mq_send(mqd_t mqdes, const char* ptr, size_t len, unsigned int prio);

ssize_t mq_receive(mqd_t mqdes, const char* ptr, size_t len, unsigned int priop);



三,线程间通信

线程驻留在进程的空间内,所以线程间的通信方式相对简单。进行线程间通信(ITC) 需要处理的最重要的问题是同步,最可能的问题是数据竞争和无限延迟。

线程间通信主要用于:

-    共享数据

-    发送消息


多线程的目的是使并发执行的处理成为流线型的。每个线程在数据流上执行不同的处理,或直接使用运算结果。

ITC 的成本要低于IPC。


线程间通信方式:


○     全局数据,全局变量和全局数据结构

同IPC相比,ITC的一个重要的优点在于线程可以共享全局数据,全局变量和全局数据结构。线程可以平等的访问他们。一个线程对他们进行的更改可以马上反应到其他的线程上。但在使用的过程中同步是最重要的。否则结果难以预料。


○     线程间通信参数

线程的参数可以用于线程之间或主线程与对等线程间的通信。线程创建的API支持此种方式。


○     文件句柄

作为ITC的一种方式,使用文件句柄要和使用全局变量一样小心。如果ThreadA移动了文件指针,ThreadB将会从新的位置开始读/写文件。如果一个线程关闭了文件,其他线程再次操作文件时则会有怎样的结果。



原创粉丝点击