进程与线程介绍

发布时间:2024年01月02日

进程与线程介绍

一、介绍

1, 定义

进程:是操作系统分配和调度系统内存资源、CPU时间片等资源的基本单位,为正在运行的应用程序提供运行环境;
线程:是操作系统/CPU能够进行运算调度的最小单位,它被包含在进程之中,进程包含一个或者多个线程。

2,通讯方式

2.1 进程间的通讯方式,以及优缺点

操作系统层面直观的看一些进程通信:我们知道,为了保证安全,每个进程的用户地址空间都是独立的,一般而言一个进程不能直接访问另一个进程的地址空间,不过内核空间是每个进程都共享的,所以进程之间想要进行信息交换就必须通过内核。
在这里插入图片描述

2.1.1 管道
(1)无名管道(PIPE)

介绍
一种半双工的通信方式,只能在具有亲缘关系的进程间使用(父子进程)
优点:简单方便
缺点:a、局限于单向通信;
b、只能创建在它的进程以及其有亲缘关系的进程之间;
c、缓冲区有限
说明
linux中Linux 管道使用竖线 | 连接多个命令,这被称为管道符。
$ command1 | command2
以上这行代码就组成了一个管道,它的功能是将前一个命令(command1)的输出,作为后一个命令(command2)的输入
示例
管道的原型:

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

代码:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
/*使用匿名管道实现进程间通信*/
int main()
{
        int fd[2];//fd[0]为读端 fd[1]为写端
        pid_t pid; // 进程id
        char buf[128]; // 读数据的缓冲区
        if(pipe(fd) == -1)//创建管道
        {
                printf("管道创建失败\n");
                perror("Fail");
        }
        pid = fork();
        if(pid < 0 )
        {
                printf("子进程开辟失败\n");
                perror("Fail");
        }else if(pid > 0){
                sleep(3);//让子进程先执行
                printf("这是一个父进程\n");//父进程完成写操作
                close(fd[0]);
                write(fd[1],"hello from father",strlen("hello from father"));
        }else{
                printf("这是一个子进程\n");//子进程完成读操作
                close(fd[1]); // 关闭写端
                read(fd[0],buf,sizeof(buf));//没有数据来时,阻塞在这
                printf("buf = %s\n",buf);
        }
        return 0;
}
(2)有名管道(FIFO)

介绍
也叫做命名管道,它是一种文件类型。
一种半双工的通信方式,它允许无亲缘关系进程间的通信
优点:可以实现任意关系的进程间的通信
缺点:a、长期存于系统中,使用不当容易出错;b、缓冲区有限
说明
FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。
FIFO的原型

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);

其中的 mode 参数与open函数中的mode相同。一旦创建了一个 FIFO,就可以用一般的文件 I/O 函数操作它。
当 open 一个 FIFO 时,是否设置非阻塞标志(O_NONBLOCK)的区别:
若没有指定 O_NONBLOCK(默认),只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写 open 要阻塞到某个其他进程为读而打开它。
若指定了 O_NONBLOCK,则只读 open 立即返回。而只写 open 将出错返回 -1 如果没有进程已经为读而打开该 FIFO,其 errno 置 ENXIO。
代码示例:
read.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
        int nread;
        char buf[30] = {'\0'};
        if(mkfifo("myfifo",0600) == -1 && errno != EEXIST)//创建命名管道
        {
                printf("mkfifo failed\n");
                perror("why");
        }
        int fd = open("./myfifo",O_RDONLY);//以只读的形式打开管道,程序阻塞在这,直到有另一个进程对其执行写操作
        if(fd < 0)
        {
                printf("read open failed\n");
        }else
        {
                printf("read open successn\n");
        }
        while(1)
        {
                nread = read(fd,buf,sizeof(buf));
                printf("read %d byte,context is:%s\n",nread,buf);
        }
        close(fd);
        return 0;
}

write.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main()
{
        int nread;
        char buf[30] = "message from myfifo";
        if(mkfifo("myfifo",0600) == -1 && errno != EEXIST)//创建命名管道
        {
                printf("mkfifo failed\n");
                perror("why");
        }
        int fd = open("./myfifo",O_WRONLY);//打开管道,程序阻塞在这,直到其他进程为读而打开它
        if(fd < 0)
        {
                printf("write open failed\n");
        }
        else
        {
                printf("write open success\n");
        }
        while(1)
        {
                sleep(1);
                write(fd,buf,strlen(buf));
        }
        close(fd);
        return 0;
}
2.1.2 消息队列(Message Queue)

介绍
消息队列(Message Queue):是消息的链表,存放在内核中并由消息队列标识符标识
优点:可以实现任意进程间的通信,并通过系统调用函数来实现消息发送和接收之间的同步,无需考虑同步问题,方便
缺点:信息的复制需要额外消耗 CPU 的时间,不适宜于信息量大或操作频繁的场合
在这里插入图片描述
说明
消息队列的本质就是存放在内存中的消息的链表,而消息本质上是用户自定义的数据结构。如果进程从消息队列中读取了某个消息,这个消息就会被从消息队列中删除。
1>消息队列允许一个或多个进程向它写入或读取消息。
2>消息队列可以实现消息的随机查询,不一定非要以先进先出的次序读取消息,也可以按消息的类型读取。
3>对于消息队列来说,在某个进程往一个队列写入消息之前,并不需要另一个进程在该消息队列上等待消息的到达。
4>消息队列的生命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列就会一直存在。而匿名管道随进程的创建而建立,随进程的结束而销毁。
5>需要注意的是,消息队列对于交换较少数量的数据很有用,因为无需避免冲突。但是,由于用户进程写入数据到内存中的消息队列时,会发生从用户态拷贝数据到内核态的过程;同样的,另一个用户进程读取内存中的消息数据时,会发生从内核态拷贝数据到用户态的过程。因此,如果数据量较大,使用消息队列就会造成频繁的系统调用,也就是需要消耗更多的时间以便内核介入

消息队列函数的原型:

// 创建或打开消息队列:成功返回队列ID,失败返回-1
int msgget(key_t key, int flag);
// 添加消息:成功返回0,失败返回-1
int msgsnd(int msqid, const void *ptr, size_t size, int flag);
// 读取消息:成功返回消息数据的长度,失败返回-1
int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
// 控制消息队列:成功返回0,失败返回-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

代码示例
msgSend.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct msgbuf{
        long mtype;       /* message type, must be > 0 */
        char mtext[128];    /* message data */
};

int main()
{
        struct msgbuf sendbuf={888,"message from send"};
        struct msgbuf readbuf;
        // 函数生成键值
        key_t key;
        if((key = ftok(".",'z')) < 0){
                printf("ftok error\n");
        }
        
        int msgId = msgget(key,IPC_CREAT|0777);
        if(msgId == -1){
                printf("get quen failed\n");
        }
 
        msgsnd(msgId,&sendbuf,strlen(sendbuf.mtext),0);
        printf("send over\n");
        msgrcv(msgId,&readbuf,sizeof(readbuf.mtext),999,0);
        printf("read from get is:%s\n",readbuf.mtext);
 
        msgctl(msgId,IPC_RMID,NULL);
        return 0;
}

msgGet.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct msgbuf{
        long mtype;       /* message type, must be > 0 */
        char mtext[128];    /* message data */
};
 
int main()
{
        struct msgbuf readbuf;
        memset(readbuf.mtext, '\0', sizeof(readbuf.mtext));
        struct msgbuf sendbuf={999,"thank for your reach"};
 
        key_t key;
        //获取key值
        if((key = ftok(".",'z')) < 0){
                printf("ftok error\n");
        }
 
        int msgId = msgget(key,IPC_CREAT|0777);
        if(msgId == -1){
                printf("get quen failed\n");
                perror("why");
        }
 
        msgrcv(msgId,&readbuf,sizeof(readbuf.mtext),888,0);
        printf("read from send is:%s\n",readbuf.mtext);
        msgsnd(msgId,&sendbuf,strlen(sendbuf.mtext),0);
        
        msgctl(msgId,IPC_RMID,NULL);
        return 0;
}
2.1.3 共享内存(Shared Memory)

介绍
共享内存:映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问
ipcs -m 查看系统下已有的共享内存;ipcrm -m shmid可以用来删除共享内存。
因为多个进程可以同时操作,所以需要进行同步。信号量 + 共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。
优点:无须复制,快捷,信息量大
缺点:
a、通信是通过将共享空间缓冲区直接附加到进程的虚拟地址空间中来实现的,因此进程间的读写操作的同步问题;
b、利用内存缓冲区直接交换信息,内存的实体存在于计算机中,只能同一个计算机系统中的诸多进程共享,不方便网络通信
说明
不同于消息队列频繁的系统调用,对于共享内存机制来说,仅在建立共享内存区域时需要系统调用,一旦建立共享内存,所有的访问都可作为常规内存访问,无需借助内核。这样,数据就不需要在进程之间来回拷贝,所以这是最快的一种进程通信方式。
在这里插入图片描述
共享内存函数的原型:

// 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1
int shmget(key_t key, size_t size, int flag);
// 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1
void *shmat(int shm_id, const void *addr, int flag);
// 断开与共享内存的连接:成功返回0,失败返回-1
int shmdt(void *addr); 
// 控制共享内存的相关信息:成功返回0,失败返回-1
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);

代码示例:
shmw.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main()
{
        int shmId;
        key_t key;
        char *shmaddr;
        // 函数生成键值
        if((key = ftok(".",1)) < 0){
                printf("ftok error\n");
        }
 
        shmId = shmget(key, 1024*4, IPC_CREAT|0666);//内存大小必须得是MB的整数倍
        if(shmId == -1){
                printf("shmget error\n");
                exit(-1);
        }
 
        /*第二个参数一般写0,让linux内核自动分配空间,第三个参数也一般写0,表示可读可写*/
        shmaddr = shmat(shmId, 0, 0);
        printf("shmat OK\n");
        strcpy(shmaddr,"I am so cool");
 
        sleep(5);//等待5秒,让别的进程去读
 
        shmdt(shmaddr);
        shmctl(shmId, IPC_RMID, 0);//写0表示不关心
        printf("quit\n");
         return 0;
}

shmr.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main()
{
        int shmId;
        key_t key;
        char *shmaddr;
        // 函数生成键值
        if((key = ftok(".",1)) < 0){
                printf("ftok error\n");
        }
 
        shmId = shmget(key, 1024*4, 0);//内存大小必须得是MB的整数倍
        if(shmId == -1){
                printf("shmget error\n");
                exit(-1);
        }
 
        /*第二个参数一般写0,让linux内核自动分配空间,第三个参数也一般写0,表示可读可写*/
        shmaddr = shmat(shmId, 0, 0);
        printf("shmat OK\n");
        printf("data : %s\n",shmaddr);
        shmdt(shmaddr);
        return 0;
}
2.1.4 信号量(Semaphore)

介绍
信号量:一个计数器,可以用来控制多个线程对共享资源的访问
信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
优点:可以同步进程
缺点:信号量有限
在这里插入图片描述

信号量的特点:
信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
支持信号量组
信号量的函数原型:

// 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
int semget(key_t key, int num_sems, int sem_flags);
// 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
int semop(int semid, struct sembuf semoparray[], size_t numops);  
// 控制信号量的相关信息
int semctl(int semid, int sem_num, int cmd, ...);

当 semget 创建新的信号量集合时,必须指定集合中信号量的个数(即 num_sems),通常为 1; 如果是引用一个现有的集合,则将 num_sems 指定为 0 。
在 semop 函数中,sembuf 结构的定义如下:

struct sembuf 
{
    short sem_num; // 信号量组中对应的序号,0~sem_nums-1
    short sem_op;  // 信号量值在一次操作中的改变量
    short sem_flg; // IPC_NOWAIT, SEM_UNDO
}

在 semctl 函数中的命令有多种,这里就说两个常用的:
SETVAL:用于初始化信号量为一个已知的值。
IPC_RMID:删除一个信号量集合。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,它可能在你下次运行此程序时引发问题,而且信号量是一种有限的资源。
代码示例:

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
union semun{
        int              val;    /* Value for SETVAL */
        struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
        unsigned short  *array;  /* Array for GETALL, SETALL */
        struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                    (Linux-specific) */
};
 
//P操作,拿钥匙
void PGetKey(int semid)
{
        struct sembuf sops;
        sops.sem_num = 0;
        sops.sem_op = -1;
        sops.sem_flg = SEM_UNDO;
        semop(semid, &sops, 1);
}
//V操作,放回钥匙
void VPutBackKey(int semid)
{
        struct sembuf sops;
        sops.sem_num = 0;
        sops.sem_op = 1;
        sops.sem_flg = SEM_UNDO;
        semop(semid, &sops, 1);
}
int main()
{
        key_t key;
        int semid;
        // 函数生成键值
        if((key = ftok(".",6)) < 0)
        {
                printf("ftok error\n");
        }
        semid = semget(key , 1,  IPC_CREAT|0666);//创造钥匙,数量为1
        union semun sem;
        sem.val = 0;//初始状态为没有钥匙
        semctl(semid, 0, SETVAL, sem);//SETVAL初始化信号量为一个已知的值,这时就需要第四个参数
        //0表示操作第一把钥匙
        int pid = fork();
        if(pid < 0)
        {
                printf("fork failed\n");
        }else if(pid == 0)
        {
                printf("this is child\n");
                VPutBackKey(semid);//首先把钥匙放回     
        }else
        {
                PGetKey(semid);//等子进程将钥匙放回后才会进行操作,保证子进程先执行
                printf("this is father\n");
                VPutBackKey(semid);
                semctl(semid, 0, IPC_RMID);//销毁钥匙
        }
 
        return 0;
}
2.1.5 信号(Signal)

介绍
信号:一种比较复杂的通信方式,用于通知接收进程某个事件已经发生
kill -l 来查看信号的名字以及序号

详细介绍在下方链接:
https://blog.csdn.net/Bossking321/article/details/135156806?spm=1001.2014.3001.5501

信号的处理:
信号的处理有三种方法,分别是:忽略、捕捉和默认动作。
忽略信号,大多数信号可以使用这个方式来处理,但是有两种信号不能被忽略(分别是 SIGKILL和SIGSTOP);
捕捉信号,需要告诉内核,用户希望如何处理某一种信号,说白了就是写一个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理。
系统默认动作,对于每个信号来说,系统都对应由默认的处理动作,当发生了该信号,系统会自动执行。具体的信号默认动作可以使用man 7 signal来查看系统的具体定义。

函数原型

//接收函数,第二个参数指向信号处理函数
sighandler_t signal(int signum, sighandler_t handler);
//发送函数
int kill(pid_t pid, int sig); 

代码示例:
接收端:

#include <stdio.h>
#include <signal.h> 
/*接受到信号后,让信号处理该函数*/
void handler(int signum)
{
        printf("signum = %d\n",signum);
        switch(signum){
                case 2:
                        printf("SIGINT\n");
                        break;
                case 9:
                        printf("SIGKILL\n");
                        break;
                case 10:
                        printf("SIGUSR1\n");
                        break;
        }
}
 
int main()
{
        signal(SIGINT,handler);
        signal(SIGKILL,handler);
        signal(SIGUSR1,handler);
		
        while(1);

        return 0;
}

发送端:

#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
 
int main(int argc,char **argv)
{
        int signum;
        int pid;
 
        signum = atoi(argv[1]);//将字符型转为整型
        pid = atoi(argv[2]);
 
        kill(pid,signum);
 
        printf("signum = %d,pid = %d\n",signum,pid);
 
        return 0;
}

高级版:
函数原型:

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
 
struct sigaction {
   void (*sa_handler)(int); //信号处理程序,不接受额外数据,SIG_IGN 为忽略,SIG_DFL 为默认动作
   void (*sa_sigaction)(int, siginfo_t *, void *); //信号处理程序,能够接受额外数据和sigqueue配合使用
   sigset_t sa_mask;//阻塞关键字的信号集,可以再调用捕捉函数之前,把信号添加到信号阻塞字,信号捕捉函数返回之前恢复为原先的值。
   int sa_flags;//影响信号的行为SA_SIGINFO表示能够接受数据
 };
//回调函数句柄sa_handler、sa_sigaction只能任选其一

我们只需要配置 sa_sigaction以及sa_flags即可。

siginfo_t {
               int      si_signo;    /* Signal number */
               int      si_errno;    /* An errno value */
               int      si_code;     /* Signal code */
               int      si_trapno;   /* Trap number that caused
                                        hardware-generated signal
                                        (unused on most architectures) */
               pid_t    si_pid;      /* Sending process ID */
               uid_t    si_uid;      /* Real user ID of sending process */
               int      si_status;   /* Exit value or signal */
               clock_t  si_utime;    /* User time consumed */
               clock_t  si_stime;    /* System time consumed */
               sigval_t si_value;    /* Signal value */
               int      si_int;      /* POSIX.1b signal */
               void    *si_ptr;      /* POSIX.1b signal */
               int      si_overrun;  /* Timer overrun count; POSIX.1b timers */
               int      si_timerid;  /* Timer ID; POSIX.1b timers */
               void    *si_addr;     /* Memory location which caused fault */
               int      si_band;     /* Band event */
               int      si_fd;       /* File descriptor */
}
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
union sigval {
   int   sival_int;
   void *sival_ptr;
 };

接收端:

#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
void handler(int signum, siginfo_t *info, void *context)
{
        printf("get signum is:%d\n",signum);
        if(context != NULL)
        {
                printf("get data = %d\n",info->si_int);
                printf("get data = %d\n",info->si_value.sival_int);
                printf("get pid is = %d\n",info->si_pid);
        }
}

int main()
{
        struct sigaction act;
        printf("pid = %d\n",getpid());
        act.sa_sigaction = handler;
        act.sa_flags = SA_SIGINFO;
 
        sigaction(SIGUSR1,&act,NULL);
        while(1);

        return 0;
}

发送端:

#include <signal.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc,char **argv)
{
        int signum;
        int pid;
 
        signum = atoi(argv[1]);
        pid = atoi(argv[2]);
 
        union sigval value;
        value.sival_int = 100;
 
        sigqueue(pid,signum,value);
        printf("pid = %d,done\n",getpid());
        return 0;
}

注意:信号发送字符串,只有在父子进程或者是共享内存下才可发送。

2.1.6 套接字(Socket)

套接字(Socket):可用于不同计算机间的进程通信
优点:
a、传输数据为字节级,传输数据可自定义,数据量小效率高;
b、传输数据时间短,性能高;
c、适合于客户端和服务器端之间信息实时交互;
d、可以加密,数据安全性强;
缺点:需对传输的数据进行解析,转化成应用级的数据。

(1)TCP服务端

在这里插入图片描述
头文件如下

#include<stdio.h>
#include<iostream>
#include<cstring>
#include<sys/fcntl.h>
#include<sys/socket.h>
#include<unistd.h>
#include<netinet/in.h>
#include<errno.h>
#include<sys/types.h>
#include <arpa/inet.h>
第一步 为服务端创建socket
 int listenfd;
    listenfd=socket(AF_INET,SOCK_STREAM,0);//在socket编程中,AF_INET是必须的,等同于固定搭配
    //socket创建成功后如果返回值是-1的话,说明创建失败,为0的时候就是创建成功
    if(listenfd==-1)
    {
        printf("socket create fail\n");
        return -1;
    }
第二步 将socket绑定到提供的ip的地址和对应的端口上
    struct sockaddr_in serveraddr;//定义一个用来处理网络通信的数据结构,sockaddr_in分别将端口和地址存储在两个结构体中
    //sin_family协议族
    memset(&serveraddr,0,sizeof(serveraddr));
    serveraddr.sin_family=AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    //serveraddr.sin_addr.s_addr=atoi(argv[1]);// specify ip address
    serveraddr.sin_port=htons(atoi(argv[1]));//specify port
    //printf("%s %s\n",argv[1],argv[2]);
    if(bind(listenfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr))!=0)
    {
        printf("bind failed \n");
        return -1;
    }
第三步 利用listen函数使用主动连接套接口变为被连接套接口

使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接。

 if(listen(listenfd,5)!=0)
 {
        printf("Listen failed\n");
        close(listenfd);
        return -1;
 }

函数声明

int listen(int sockfd, int backlog);//返回0=成功 -1则失败

参数sockfd是已经被bind过的socket。socket函数返回的socket是一个主动连接的socket,在服务端的编程中,程序员希望这个socket可以接受外来的连接请求,也就是被动等待客户端来连接。由于系统默认时认为一个socket是主动连接的,所以需要通过某种方式来告诉系统,程序员通过调用listen函数来完成这件事。
参数backlog,这个参数涉及到一些网络的细节,比较麻烦,填5、10都行,一般不超过30。当调用listen之后,服务端的socket就可以调用accept来接受客户端的连接请求。

第四步 接受客户端的请求

服务端接受客户端的请求

	int clintfd;//socket for client
    int socklen=sizeof(struct sockaddr_in);
    struct sockaddr_in client_addr;
    clintfd=accept(listenfd,(struct sockaddr*)&client_addr,(socklen_t *)&socklen);
    if(clintfd==-1)
        printf("connect failed\n");
    else
        printf("client %s has connnected\n",inet_ntoa(client_addr.sin_addr));

其中accept函数如下

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数sockfd是已经被listen过的socket.
参数addr用于存放客户端的地址信息,用sockaddr结构体表达,如果不需要客户端的地址,可以填0。
参数addrlen用于存放addr参数的长度,如果addr为0,addrlen也填0
accept函数等待客户端的连接,如果没有客户端连上来,它就一直等待,这种方式称之为阻塞。
accept等待到客户端的连接后,创建一个新的socket,函数返回值就是这个新的socket,服务端使用这个新的socket和客户端进行报文的收发。
返回值: 成功则返回0,失败返回-1,错误原因存于errno 中
accept在等待的过程中,如果被中断或其它的原因,函数返回-1,表示失败,如果失败,可以重新accept.

第五步 同客户端连接,接受数据

int iret;
memset(buf,0,sizeof(buf));
iret=recv(clintfd,buf,strlen(buf),0);
if(iret<=0)
{
     perror("send");
     break;
}
printf("receive %s\n",buf);

recv函数用于server端socket传送过来的数据,函数声明如下:

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

这时候这里的sockfd其实是clientfd,buf中存储接受的数据,如果client的socket没有发送数据,recv函数就会一直等待,如果发送了数据,函数返回接受到的字符数,如果socket关闭那么就会返回0

   char buffer[1024];
    while (1)
    {
    int iret;
    memset(buffer,0,sizeof(buffer));
    iret=recv(clintfd,buffer,sizeof(buffer),0);
    if (iret<=0) 
    {
       printf("iret=%d\n",iret); break;  
    }
    printf("receive :%s\n",buffer);
 
   strcpy(buffer,"ok");//reply cilent with "ok"
    if ( (iret=send(clintfd,buffer,strlen(buffer),0))<=0) 
    { 
        perror("send"); 
        break; 
    }
    printf("send :%s\n",buffer);
  }

send函数用于把数据通过socket发送给对端。不论是客户端还是服务端,应用程序都用send函数来向TCP连接的另一端发送数据。

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

返回值是0的时候,通信已经中断。

第六步 关闭连接
close(listenfd); 
close(clintfd);
完整 server端
#include<stdio.h>
#include<iostream>
#include<cstring>
#include<stdlib.h>
#include<sys/fcntl.h>
#include<sys/socket.h>
#include<unistd.h>
#include<netinet/in.h>
#include<errno.h>
#include<sys/types.h>
#include <arpa/inet.h>
using namespace std;
int main(int argc,char *argv[])
{
    // first step ->create socket for server
    int listenfd;
    listenfd=socket(AF_INET,SOCK_STREAM,0);// in socket code,it must be AF_INET(protocol) 
    if(listenfd==-1)
    {
        printf("socket create fail\n");
        return -1;
    }
    //second step bind ->server's ip&port for communication to socket created in fist step
    struct sockaddr_in serveraddr;
    memset(&serveraddr,0,sizeof(serveraddr));
    serveraddr.sin_family=AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    //serveraddr.sin_addr.s_addr=atoi(argv[1]);// specify ip address
    serveraddr.sin_port=htons(atoi(argv[1]));//specify port
    //printf("%s %s\n",argv[1],argv[2]);
    if(bind(listenfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr))!=0)
    {
        printf("bind failed \n");
        return -1;
    }
    //Third step ->Set socket to listening mode
    /*
    The listen function changes the active connection socket interface into the connected socket interface, 
    so that a process can accept the requests of other processes and become a server process. 
    In TCP server programming, the listen function changes the process into a server and specifies that the corresponding socket becomes a passive connection.
    */
    if(listen(listenfd,5)!=0)
    {
        printf("Listen failed\n");
        close(listenfd);
        return -1;
    }
    // 4th step -> receive client's request
    int clintfd;//socket for client
    int socklen=sizeof(struct sockaddr_in);
    struct sockaddr_in client_addr;
    clintfd=accept(listenfd,(struct sockaddr*)&client_addr,(socklen_t *)&socklen);
    if(clintfd==-1)
        printf("connect failed\n");
    else
        printf("client %s has connnected\n",inet_ntoa(client_addr.sin_addr));
   
    // 5th step ->connect with client,receive data and reply OK
    char buffer[1024];
    while (1)
    {
    int iret;
    memset(buffer,0,sizeof(buffer));
    iret=recv(clintfd,buffer,sizeof(buffer),0);
    if (iret<=0) 
    {
       printf("iret=%d\n",iret); break;  
    }
    printf("receive :%s\n",buffer);
 
    strcpy(buffer,"ok");//reply cilent with "ok"
    if ( (iret=send(clintfd,buffer,strlen(buffer),0))<=0) 
    { 
        perror("send"); 
        break; 
    }
    printf("send :%s\n",buffer);
  }
    // 6th close socket
    close(listenfd); close(clintfd);
}
(2)TCP服务端

在这里插入图片描述
所需头文件如下

#include <unistd.h>
#include <stdlib.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
第一步 为客服端创建socket
int sockfd;
sockfd = socket(AF_INET,SOCK_STREAM,0))//同服务端操作相同
第二步 向server发起连接请求

// 第一种

  struct hostent* h;
  if ( (h = gethostbyname(argv[1])) == 0 )   // 指定服务端的ip地址。
  {
   printf("gethostbyname failed.\n"); 
   close(sockfd);
   return -1;
   }
  struct sockaddr_in servaddr;
  memset(&servaddr,0,sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_port = htons(atoi(argv[2])); // 指定服务端的通信端口。
  memcpy(&servaddr.sin_addr,h->h_addr,h->h_length);
  if (connect(sockfd, (struct sockaddr *)&servaddr,sizeof(servaddr)) != 0) 
   // 向服务端发起连接清求。
  {
   perror("connect"); 
   close(sockfd); 
   return -1;
  }

// 第二种

  struct sockaddr_in servaddr;
  memset(&servaddr,0,sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_port = htons(atoi(argv[2])); // server's port
  servaddr.sin_addr.s_addr=inet_addr(argv[1]);//server's ip
  if (connect(sockfd, (struct sockaddr *)&servaddr,sizeof(servaddr)) != 0)  // send request to server for connection
  { 
      perror("connect"); 
      close(sockfd); 
      return -1; 
  }

其中用到的结构体和函数介绍如下

struct hostent
{
	char *h_name;         //正式主机名
	char **h_aliases;     //主机别名
	int h_addrtype;       //主机IP地址类型:IPV4-AF_INET
	int h_length;		  //主机IP地址字节长度,对于IPv4是四字节,即32位
	char **h_addr_list;	  //主机的IP地址列表
};
#define h_addr h_addr_list[0]   //保存的是IP地址

这里面使用的gethostbyname函数,只能用在客户端上,主要作用就是把字符串的IP地址转换成结构体的ip地址。

####### 第三步 向server发送数据

while(1){
   char buffer[1024];
   int iret;
   int choice;
   printf("if you want to continue to chat please input 1,or input 2\n");
   scanf("%d\n",&choice);
   if(choice==2)
   	break;
   memset(buffer,0,sizeof(buffer));
   printf("please input what you want to say\n");
   scanf("%s",buffer);
   if ( (iret=send(sockfd,buffer,strlen(buffer),0))<=0) // 向服务端发送请求报文。
   {
     perror("send");
     break; 
   }
   printf("send:%s\n",buffer);
  memset(buffer,0,sizeof(buffer));
   if ( (iret=recv(sockfd,buffer,sizeof(buffer),0))<=0) // 接收服务端的回应报文。
   {
      printf("iret=%d\n",iret); 
      break;
   }
   printf("receive:%s\n",buffer);
 }
第四步 关闭连接释放资源
close(sockfd);
完整clent端代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include<string>
#include<iostream>
using namespace std;
int main(int argc,char *argv[])
{
   // first step create socket 
  int sockfd;
  if ( (sockfd = socket(AF_INET,SOCK_STREAM,0))==-1) 
  { 
      perror("socket"); 
      return -1; 
  }
 
  // second step: send request to server for connection
  struct sockaddr_in servaddr;
  memset(&servaddr,0,sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_port = htons(atoi(argv[2])); // server's port
  servaddr.sin_addr.s_addr=inet_addr(argv[1]);//server's ip
  if (connect(sockfd, (struct sockaddr *)&servaddr,sizeof(servaddr)) != 0)  // send request to server for connection
  { 
      perror("connect"); 
      close(sockfd); 
      return -1; 
  }
  char buffer[1024];
  // 3th step connect with server 
  while(1)
  {
    int iret;
    memset(buffer,0,sizeof(buffer));
    int choice;
    printf("if you want to continue to chat please input 1 or input else\n");
    scanf("%d",&choice);
    getchar();
    if(choice!=1)
    break;
    else
    {
        printf("please input what you want to say\n");
        cin.getline(buffer,1024);
    }

    if ( (iret=send(sockfd,buffer,strlen(buffer),0))<=0) // send data to server
    { 
        perror("send"); 
        break; 
    }
    printf("send: %s\n",buffer);
    memset(buffer,0,sizeof(buffer));
    if ( (iret=recv(sockfd,buffer,sizeof(buffer),0))<=0) // receive server's reply
    {
       printf("iret=%d\n",iret); 
       break;
    }
    printf("receive: %s\n",buffer);
  }
  close(sockfd);
}
2.1.7 总结

简单总结一下上面六种 Linux 内核提供的进程通信机制:

1)首先,最简单的方式就是管道,管道的本质是存放在内存中的特殊的文件。也就是说,内核在内存中开辟了一个缓冲区,这个缓冲区与管道文件相关联,对管道文件的操作,被内核转换成对这块缓冲区的操作。管道分为匿名管道和有名管道,匿名管道只能在父子进程之间进行通信,而有名管道没有限制。

2)虽然管道使用简单,但是效率比较低,不适合进程间频繁地交换数据,并且管道只能传输无格式的字节流。为此消息队列应用而生。消息队列的本质就是存放在内存中的消息的链表,而消息本质上是用户自定义的数据结构。如果进程从消息队列中读取了某个消息,这个消息就会被从消息队列中删除。

3)消息队列的速度比较慢,因为每次数据的写入和读取都需要经过用户态与内核态之间数据的拷贝过程,共享内存可以解决这个问题。所谓共享内存就是:两个不同进程的逻辑地址通过页表映射到物理空间的同一区域,它们所共同指向的这块区域就是共享内存。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。

对于共享内存机制来说,仅在建立共享内存区域时需要系统调用,一旦建立共享内存,所有的访问都可作为常规内存访问,无需借助内核。这样,数据就不需要在进程之间来回拷贝,所以这是最快的一种进程通信方式。

4)共享内存速度虽然非常快,但是存在冲突问题,为此,我们可以使用信号量和 PV 操作来实现对共享内存的互斥访问,并且还可以实现进程同步。

5)信号和信号量是完全不同的两个概念!信号是进程通信机制中唯一的异步通信机制,它可以在任何时候发送信号给某个进程。通过发送指定信号来通知进程某个异步事件的发送,以迫使进程执行信号处理程序。信号处理完毕后,被中断进程将恢复执行。用户、内核和进程都能生成和发送信号。

6)上面介绍的 5 种方法都是用于同一台主机上的进程之间进行通信的,如果想要跨网络与不同主机上的进程进行通信,就需要使用 Socket 通信。另外,Socket 也能完成同主机上的进程通信。

2.2 线程之间的通信方式

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

2.2.1 锁机制

包括互斥锁/量(mutex)、读写锁(reader-writer lock)、自旋锁(spin lock)、条件变量(condition)
a. 互斥锁/量(mutex):提供了以排他方式防止数据结构被并发修改的方法。
b. 读写锁(reader-writer lock):允许多个线程同时读共享数据,而对写操作是互斥的。
c. 自旋锁(spin lock)与互斥锁类似,都是为了保护共享资源。互斥锁是当资源被占用,申请者进入睡眠状态;而自旋锁则循环检测保持者是否已经释放锁。
d. 条件变量(condition):可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。

2.2.2 信号量机制(Semaphore)

(1)无名线程信号量
(2)命名线程信号量
信号机制(Signal):类似进程间的信号处理
屏障(barrier):屏障允许每个线程等待,直到所有的合作线程都达到某一点,然后从该点继续执行。

3,对比

3.1 维度对比

对比维度进程线程
数据共享.同步数据共享复杂,需要用IPC;数据是分开的,同步简单因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂
内存、CPU占用内存多,切换复杂,CPU利用率低占用内存少,切换简单,CPU 利用率高
创建销毁、切换创建销毁、切换复杂,速度慢创建销毁、切换简单,速度很快
编程、调试编程简单,调试简单编程复杂,调试复杂
可靠性进程间不会互相影响一个线程挂掉将导致整个进程挂掉
分布式适应于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单适应于多核分布式

3.2 优劣

优劣进程线程
优点编程、调试简单,可靠性较高创建、销毁、切换速度快,内存、资源占用小
缺点创建、销毁、切换速度慢,内存、资源占用大编程、调试复杂,可靠性较差

4,选择

(1)需要频繁创建销毁的优先用线程
(2)需要进行大量计算的优先使用线程
(3)强相关的处理用线程,弱相关的处理用进程
(4)可能要扩展到多机分布的用进程,多核分布的用线程
(5)都满足需求的情况下,用你最熟悉、最拿手的方式

5,创建进程方式

5.1、fork()

fork() 函数被调用一次,但返回两次,可能会有三个不同的返回值。
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值;
故我们可以通过判断返回值来区分父子进程。如果不做判断,fork() 后的代码父子进程会各执行一遍。

#include<stdio.h> 
#include<sys/types.h> 
#include<unistd.h> 
#include<errno.h> 

int main() 
{ 
 int a = 5; 
 int b = 2; 
 pid_t pid; 
 pid = fork(); 
 if( pid == 0)
  { // 子进程
    a = a - 4; 
	  printf("I'm a child process with PID [%d],the value of a: %d,the value of b:%d.\n",pid,a,b); 
 }else if(pid < 0) { 
     perror("fork"); 
 }else { // 父进程
      printf("I'm a parent process, with PID [%d], the value of a: %d, the value of b:%d.\n", pid, a, b); 
 } 
 return 0; 
} 

#gcc –o fork fork.c 
#./fork 
//运行结果: 
  I’m a child process with PID[0],the value of a:1,the value of b:2. 
  I’m a parent process with PID[19824],the value of a:5,the value of b:2.

注:
fork()系统调用会创建一个子进程,这个子进程是父进程的一个副本。***这也就意味着,系统在创建子进程成功后,会将父进程的文本段、数据段、堆栈都复制一份给子进程,但子进程有一份自己独立的空间。***子进程对这些内存的修改并不会影响父进程空间的相应内存。 这时系统中出现两个基本完全相同的进程(父、子进程),这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的调度策略。 如果需要确保让父进程或子进程先执行。则需要程序员在代码中通过进程间通信的机制来实现。

5.2、vfork()

#include <sys/types.h> 
#include <unistd.h> 
pid_t vfork(void);

正确返回:在父进程中返回子进程的进程号,在子进程中返回0 ,错误返回:-1
vfork系统调用不同于fork,用vfork创建的子进程与父进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,如果这时子进程修改了某个变量,这将影响到父进程。
因此,上面的例子如果改用vfork()的话,那么两次打印a,b的值是相同的,所在地址也是相同的。
但此处有一点要注意的是用vfork()创建的子进程必须先调用exit()来结束,否则子进程将不能结束,而fork()则不存在这个情况。
Vfork也是在父进程中返回子进程的进程号,在子进程中返回0。 用 vfork创建子进程后,父进程会被阻塞直到子进程调用exec(exec,将一个新的可执行文件载入到地址空间并执行之。)或exit。vfork的好处是在子进程被创建后往往仅仅是为了调用exec执行另一个程序,因为它就不会对父进程的地址空间有任何引用,所以对地址空间的复制是多余的 ,因此通过vfork共享内存可以减少不必要的开销。下面这个例子可以验证子进程调用exec时父进程是否真的已经结束阻塞:

#include<stdlib.h> 
#include<sys/types.h> 
#include<sys/wait.h> 
#include<unistd.h> 
#include<stdio.h> 
#include<errno.h> 
#include<string.h> 
int main() 
{ 
 int a = 1; 
 int b = 2; 
 pid_t pid; 
 int status; 
  pid = vfork(); 
  if(pid == -1) { 
     perror("Fork failed to creat a process"); 
     exit(1); 
   }
   else if(pid == 0) 
   { //子进程 
     // sleep(3);
    /*调用exec后父进程才可能调度运行,因此sleep(3)函数必须放在example程序中才能生效。*/
      if(execl("/bin/example","example",NULL)<0) 
      { 
          perror("Exec failed"); 
          exit(1); 
       } 
       exit(0); 
   }// else // if(pid != wait(&status)) { 
              // perror("A Signal occured before the child exited"); }
   else 
   {	
       printf("parent process,the value of a :%d, b:%d, addr of a: %p,b: %p\n",a,b,&a,&b); 
       exit(0);
   }
}

/bin/example 程序源文件
Example.c

#include<stdio.h> 
int main() 
{ 
   int a = 1;
   int b = 2;
   sleep(3);
   printf("Child process,the value of a is %d,b is %d,the address a %p,b %p\n",a,b,&a,&b); 
   return 0; 
} 
#gcc –o execl execl.c 

#./execl 运行结果:

 Child process ,The value of a is 1,b is 2,the address a 0xbfb73d90,b 0xbfb73d8c

5.3、clone函数

#include <sched.h> 
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);

正确返回:返回所创建进程的PID,函数中的flags标志用于设置创建子进程时的相关选项,错误返回:-1
系统调用fork()和vfork()是无参数的,而clone()则带有参数。fork()是全部复制,vfork()是共享内存,而clone() 是则可以将父进程资源有选择地复制给子进程,而没有复制的数据结构则通过指针的复制让子进程共享,具体要复制哪些资源给子进程,由参数列表中的 clone_flags来决定。另外,clone()返回的是子进程的pid。

#include <stdio.h> 
#include <stdlib.h> 
#include <sched.h> 
#include <unistd.h> 
#include <fcntl.h> 
#include <sys/types.h> 
#include <sys/stat.h> 
int variable,fd; 
int do_something() 
{ 
// 关闭文件指针
  variable = 42; 
  printf("in child process\n"); 
  close(fd); 
  // _exit(0); 
  return 0; 
} 
 
int main(int argc, char *argv[]) 
{ 
  void *child_stack; 
  char tempch; 
  variable = 9; 
  fd = open("/test.txt",O_RDONLY); 
  child_stack = (void *)malloc(16384); 
  printf("The variable was %d\n",variable); 
  clone(do_something, child_stack+10000, CLONE_VM |CLONE_FILES,NULL); 
  sleep(3); /* 延时以便子进程完成关闭文件操作、修改变量 */ 
  printf("The variable is now %d\n",variable); 
  if(read(fd,&tempch,1) < 1) { 
  	perror("File Read Error"); 
  	exit(1); 
  } 
  printf("We could read from the file\n"); 
  return 0; 
} 
 
#gcc –o clone clone.c 
#./clone 

运行结果:

   the value was 9 
   in child process 
   The variable is now 42 
   File Read Error

5.4、system函数

system()函数可以在一个子进程中执行一个shell命令。它会创建一个新的进程来执行指定的命令,然后等待命令执行完毕。当进程被创建之后,当前进程不再执行(处于挂起状态);待被创建进程执行结束之后当前进程继续执行

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
int main()
{
	system("sh build.sh");
	printf("休息!\n");
	return 0;
}

5.5、使用exec簇

exec 族函数的特征:调用 exec 族函数会把新的程序装载到当前进程中. 在调用过 exec 族函数后,进程中执行的代码就与之前完全不同了,所以 exec 函数调用之后的代码是不会被执行的。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() 
{
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程
        execl("/bin/ls", "ls", "-l", NULL);
        printf("如果exec()调用成功,这行代码不会被执行\n");
    } else if (pid > 0) {
        // 父进程
        wait(NULL); // 等待子进程执行完毕
        printf("子进程执行完毕\n");
    } else {
        // fork()调用失败
        printf("无法创建子进程\n");
    }
    return 0;
}

5.6、fork,vfork,clone的区别

5.6.1.子进程获取数据段方式

fork出来的子进程是父进程的一个拷贝,即,子进程从父进程得到了数据段和堆栈段的拷贝,这些需要分配新的内存;而对于只读的代码段,通常使用共享内存的方式访问;
vfork则是子进程与父进程共享内存空间, 子进程对虚拟地址空间任何数据的修改同样为父进程所见;

clone则由用户通过参clone_flags 的设置来决定哪些资源共享,哪些资源拷贝。

5.6.2.父子进程运行时间

fork不对父子进程的执行次序进行任何限制,fork返回后,子进程和父进程都从调用fork函数的下一条语句开始行,但父子进程运行顺序是不定的,它取决于内核的调度算法;
vfork调用中,子进程先运行,父进程挂起,直到子进程调用了exec或exit之后,父子进程的执行次序才不再有限制;
clone中由标志CLONE_VFORK来决定子进程在执行时父进程是阻塞还是运行,若没有设置该标志,则父子进程同时运行,设置了该标志,则父进程挂起,直到子进程结束为止。

6,特殊进程

6.1 僵尸进程

僵尸进程:子进程先结束,而父进程没有回收子进程,没有释放子进程占用的空间,此时子进程将成为一个僵尸进程。
详细介绍在下方链接:
https://blog.csdn.net/Bossking321/article/details/135195725?spm=1001.2014.3001.5501

6.2 孤儿进程

孤儿进程:父进程先结束,而子进程仍然存活,此时子进程成为孤儿进程,将由系统的init进程负责回收相关资源。

7,创建线程的三种方式

通过函数指针
通过函数对象
通过lambda函数
使用std::thread类创建对象,必须包含头文件

#include <thread>

创建的形式是

std::thread thobj(<CALL_BACK>)

新线程将在创建新对象后立即启动,并将与启动该线程的线程并行执行传递的回调。而且,任何线程都可以通过在该线程的对象上调用join()函数来等待另一个线程退出。

7.1 通过函数指针创建线程

#include <iostream>
#include <thread>
#include <chrono>
using namespace std;

void thread_func()
{
	for(int i= 0; i< 10; ++i){
		cout<<" thread thread_func is running..."<< endl;
		std::this_thread::sleep_for(std::chrono::seconds(1));
	}
}
int main()
{
	thread threadobj(thread_func);
	cout<<"main Thread is running..."<<endl;
	threadobj.join();
	cout<<" exit from main Thread"<<endl;
	return 0;
}

7.2通过函数对象创建线程

#include <iostream>
#include <thread>
#include <chrono>
using namespace std;

class Thread_Test
{
public:
	void operator()()
	{ // 仿函数
		for(int i = 0; i < 10; i++)
		{
			cout<<" Thread_Test is running..."<<endl;
			std::this_thread::sleep_for(std::chrono::seconds(1));
		}
	}	
};

int main()
{
	thread threadobj((Thread_Test()));
	cout<<"main Thread is running..."<<endl;
	threadobj.join();
	cout<<" exit from main Thread"<<endl;
	return 0;
}

7.3 lambda函数创建线程

#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
int main()
{
	cout<<"main Thread is running..."<<endl;
	thread threadobj([]{
		for (int i = 0; i < 10; i++)
		{
			cout<<"lambda thread is running ..." <<endl;
			::this_thread::sleep_for(::chrono::seconds(1));
		}
	});
	threadobj.join();
	cout<<" exit from main Thread"<<endl;
	return 0;
}

8,线程下std::thread和std::this_thread的详细说明

头文件包含了std::thread类和std::this_thread命名空间的声明。C++开发中include 头文件,就可以使用std:thread线程类std::this_thread命名空间,std::this_thread这个命名空间包含了对当前线程的一些基本操作,如获取当前线程id、休眠当前线程、让渡当前线程的时间片给其他线程等。

8.1 std::thread类 (C++11)

std::thread类来表示执行的各个线程。
执行线程是实际上是执行一系列指令,可以在多线程环境中与其他此类序列同时执行,同时共享相同的地址空间。
一个初始化的线程(std::thread)对象表示活动的执行线程,即初始化后std::thread立即运行;这样的线程对象是可连接的(joinable),并且具有唯一的线程id。
默认构造的(未初始化的)线程对象是不可连接的(not joinable),其线程id对于所有不可连接线程都是通用的。
如果move线程,或者对它们调用join或detach,则可连接线程将变得不可连接。

8.1.1 thread成员变量

id
Thread id (public member type)
native_handle_type
Native handle type (public member type)

8.1.2 thread成员函数:
8.1.2.1 joinable

判断线程是否可连接
bool joinable() const noexcept;
检查线程是否可连接,返回线程对象是否可连接。
(1)如果线程对象表示执行线程,则它是可连接的。
(2)在以下任何情况下,线程对象都不可连接:
如果它是默认构造的。
如果它已被move(构造另一个线程对象,或分配给另一个线程)。
如果已调用其成员join或detach。

8.1.2.2 join

连接线程,阻塞调用线程
void join();
连接(join)线程,当线程执行完成时,函数返回。
join()函数能够保证调用线程和被调用线程同步,join()函数将阻塞调用该join()函数线程的执行,直到被调用线程的函数返回。调用join()函数后,线程对象变得不可连接,可以安全销毁。

8.1.2.3 detach

分离线程,调用线程和被调用线程各自独立运行
void detach();
分离(detach)线程将对象表示的线程与调用线程分离,允许它们彼此独立地执行。这两个线程都继续运行,不会以任何方式阻塞或同步。请注意,当其中一方结束执行时,其资源将被释放。调用此函数后,线程对象变得不可连接,可以安全销毁。

8.1.2.4 swap

交换线程
void swap (thread& x) noexcept;
交换线程,将对象的状态与x的状态交换。
swap函数与thread的移动构造函数和移动赋值函数作用一样。

8.1.2.5 thread

1> 拷贝构造函数
thread (const thread&) = delete;
由以上可知,拷贝构造函数已经delete,构造函数中只有三种构造函数可用:
(1) 默认构造函数
thread() noexcept;
构造一个不表示任何执行线程的线程对象。
(2) 初始化构造函数
template <class Fn, class… Args>
explicit thread (Fn&& fn, Args&&… args);
构造一个线程对象,该对象表示新的可连接执行线程。新的执行线程调用fn函数传递args 作为参数。此构造的完成与fn函数的调用开始同步。
(3) 移动构造函数
thread (thread&& x) noexcept;
构造一个线程对象,该对象获取由x表示的执行线程(如果有)。此操作不会以任何方式影响已移动线程的执行,它只是传输其处理程序。之后x对象不再表示任何执行线程。

8.1.2.6 thread 赋值函数
1,  thread& operator= (thread&& rhs) noexcept;
2,  thread& operator= (const thread&) = delete;

thread赋值函数的拷贝赋值函数已经delete,只有移动赋值函数可用,参数为右值thread,也叫移动分配线程函数。
如果对象当前不可连接,它将获取由rhs表示的执行线程(如果有的话)。
如果它是可连接的,则调用terminate()。
在调用之后,rhs不再表示任何执行线程(就像默认构造的一样)。

8.1.2.7 join()用法示例
#include <iostream>       // std::cout
#include <thread>         // std::thread
void foo() 
{  // do stuff... }
 
void bar(int x)
{  // do stuff... }
 
int main() 
{
  std::thread first (foo);    
  std::thread second (bar,0);  
  first.join();                // pauses until first finishes
  second.join();               // pauses until second finishes 
  return 0;
}

8.1.2.8 detach()用法示例

#include <iostream>       // std::cout
#include <thread>         // std::thread, std::this_thread::sleep_for
#include <chrono>         // std::chrono::seconds
 
void pause_thread(int n) 
{
  std::this_thread::sleep_for (std::chrono::seconds(n));
  std::cout << "pause of " << n << " seconds ended\n";
}
 
int main() 
{
  std::cout << "Spawning and detaching 3 threads...\n";
  std::thread (pause_thread,1).detach();
  std::thread (pause_thread,2).detach();
  std::thread (pause_thread,3).detach();
  std::cout << "Done spawning threads.\n";
 
  std::cout << "(the main thread will now pause for 5 seconds)\n";
  // give the detached threads time to finish (but not guaranteed!):
  pause_thread(5);
  return 0;
}

输出(5秒后):

Spawning 5 threads...
Done spawning threads. Now waiting for them to join:
pause of 1 seconds ended
pause of 2 seconds ended
pause of 3 seconds ended
pause of 4 seconds ended
pause of 5 seconds ended
All threads joined!

8.2 std::this_thread命名空间

此命名空间提供了访问当前线程的一组函数。
8.2.1 this_thread命名空间的函数介绍
get_id 获得当前线程id
Yield 将当前线程时间片让渡给其他线程
sleep_until 当前线程休眠直到某个时间点
sleep_for 当前线程休眠一段时间

8.2.2 sleep_for示例
int main() 
{
  std::cout << "countdown:\n";
  for (int i=10; i>0; --i) {
    std::cout << i << std::endl;
    std::this_thread::sleep_for (std::chrono::seconds(1));
  }
  std::cout << "Lift off!\n";
  return 0;
}

8.3 pthread函数 (C++98)

pthread_create 线程创建

函数原型

int pthread_create(pthread_t *restrict tidp,const pthread_attr_t *restrict attr,void *(*start_rtn)(void),void *restrict arg);

返回值:若是成功建立线程返回0,否则返回错误的编号。
  形式参数:pthread_t *restrict tidp要创建的线程的线程id指针;const pthread_attr_t restrict attr创建线程时的线程属性;void (start_rtn)(void)返回值是void类型的指针函数;void *restrict arg start_rtn的形参。

pthread_join线程挂起

该函数的作用使得当前线程挂起,等待另一个线程返回才继续执行。也就是说当程序运行到这个地方时,程序会先停止,然后等线程id为thread的这个线程返回,然后程序才会断续执行。
  函数原型:

int pthread_join( pthread_t thread, void **value_ptr);

参数说明如下:thread等待退出线程的线程号;value_ptr退出线程的返回值。

pthread_exit线程退出

函数原型

void pthread_exit(void *rval_ptr);
pthread_self获取当前线程id

函数原型

pthread_t pthread_self(void);
代码示例
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void *thread(void *ptr)
{
int i;
for(i=0;i<3;i++){
sleep(1);
printf("This is a pthread.\n");}
}
int main(void)
{
pthread_t id;
int i,ret;
ret=pthread_create(&id,NULL,thread,NULL);
if(ret!=0){
printf ("Create pthread error!\n");
exit (1);
}
for(i=0;i<3;i++){
printf("This is the main process.\n");
sleep(1);
}
pthread_join(id,NULL);
return (0);
}
 
编译链接命令 g++ -o example2 example.c -lpthread
文章来源:https://blog.csdn.net/Bossking321/article/details/135293443
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。