前言

在前文的学习之后,如愿来到了多线程编程


提示:以下是本篇文章正文内容,下面案例可供参考

一、daemon进程

daemon进程是Unix/Linux中的守护进程,类似于windows中的后台服务进程,一直在后台长时间运行的程序。它通常在系统启动后就运行,没有控制终端,也无法和前台的用户交互,在系统关闭时才结束。守护终端一旦脱离了终端,退出就成了问题,这时需要使用ps命令查出进程ID再使用kill命令停止。

提出daemon进程是因为有时候我们需要长时间的运行或者编译某一个程序,但是如果不小心关闭了终端,程序也就终止了。
在学习过一些Linux基本命令之后,我们知道在命令后加上后台运行符(&)可以让命令在后台运行

make &

该命令会让编译命令到后台执行,但是这样只是造成了make 在后台一直运行的假象,依然没有脱离和terminal之间的父子关系,当terminal退出后,make依然会退出。而作为daemon进程,我们希望一旦启动就能在后台一直运行,就算terminal退出了之后,daemon程序还是不会退出。

Linux系统下专门提供一个用来创建daemon进程的库函数,函数原型如下:

#include <unistd.h>
int daemon(int nochdir, int noclose)

参数nodhdir: 指定是否要切换当前工作路径到跟目录(“/”);
参数noclose:指定是否要关闭标准输入、标准输出和标准出错
//在创建守护进程的时候,往往需要将进程的工作目录修改“/”根目录,并将标准输入、标准输出和标准出错关闭,所以这两个参数一般都是传0.

二、信号

软中断信号(signal,又简称信号)用来通知进程发生了异步事件,在软件层次上是对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求是一样的。信号是进程间通信中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的达到,事实上,进程也不知道信号到底什么时候到达,进程之间可以相互通过系统调用kill()发送软中断信号,内核也可以因为内部事件而给进程发送信号,通知进程发生了某事件。信号机制处理基本通知功能外,还可以传递附加信息,收到信号的进程对各种信号又不同的处理方法,处理方法有以下三种:

  1. 类似于中断的处理程序,对于需要处理的信号,进程可以指定处理函数,也就是说,当该信号发生时,就调用该信号来处理;
  2. 对信号不作处理;
  3. 对信号的处理以一种保留系统的默认值,对大部分的信号的缺省操作使得程序终止,进程通过系统调用signal来指定进程对某个信号的处理行为。

Linux下有signal()和sigaction()两种信号安装函数
signal()函数原型如下::

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

sigaction()的函数说明:sigaction()会依照四个怒骂指定的信号编号来设置该信号的处理函数,参数signum可以指定SIGKILL和SIGSTOP以外的所有信号。
sigaction()函数原型如下
代码如下(示例):

#include <signal.h>
int sigaction (
int signum, const struct sigaction *act, struct sigaction *oldact);

Signum:指出要捕获的信号类型;
Act:指定新的信号处理方式;
Oldact:输出之前信号的处理方式。


Struct sigaction结构体介绍
struct sigaction {
    void (*sa_handler)(int);
    void (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
}

sa_handler此参数和signal()的参数handler相同,代表新的信号处理函数
sa_mask 用来设置在处理该信号时暂时将sa_mask 指定的信号集搁置
sa_flags 用来设置信号处理的其他相关操作,下列的数值可用。 

SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号

三、多线程编程

操作系统原理的术语中,线程是进程的一条执行路径。线程在unix系统下,通过被称为轻量级的进程。所有的线程都是在同一个进程空间运行,也意味着多条线程共享该进程中的全部资源,如虚拟地址空间,文件描述符和信号处理等等。但是同一个进程中的多个线程有各自的调用栈(call back),自己的寄存器环境,自己的线程本地存储。一个线程可以并行执行不同的任务。
在同一进程空间中,子线程们平等,且共享该进程中的全部资源:
在这里插入图片描述

创建线程
pthread_create()函数
函数说明:用来创建一个线程,并执行start_routinue指针指向的函数,函数原型如下:

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine)(void *),void *arg);
  1. 第一个参数thread是一个pthread_t类型的指针,它用来返回线程的线程ID,每个线程都能通过pthread_self()来获得自己的线程ID(pthread_t类型
  2. 第二个参数是线程的属性,其类型是pthread_attr类型,该类型的定义如下:
typedef struct 
{ 
int detachstate; 线程的分离状态 
int schedpolicy; 线程调度策略 
struct sched_param schedparam; 线程的调度参数 
int inheritsched; 线程的继承性 
int scope; 线程的作用域 
size_t guardsize; 线程栈末尾的警戒缓冲区大小 
int stackaddr_set; 
void * stackaddr; 线程栈的位置 
size_t stacksize; 线程栈的大小 
}pthread_attr_t; 
  1. 第三个参数start_routinue是一个函数指针,它指向的函数原型是void *func(void *),这是所创建的子进程要执行的任务(函数)。
  2. 第四个参数arg就是传给了所调用的函数的参数,如果有多个参数需要传递给子线程则需要封装到一个结构体里传进去

需要特别说明的是:对于第二个参数的那些属性,需要设定的是线程的分离状态,有必要的话也需要修改每个线程栈的大小,每个线程创建后默认是joinable状态,该状态需要主线程调用pthread_join()等待它退出,否则子进程结束是,内存资源不能得到释放,造成内存泄漏,所以我们创建线程是一般会将线程设置为分离状态,具体有两种方法:

  1. 线程里面调用pthread_detach(pthread_self())(这个方法最简单);
  2. 在创建线程的属性里面设置PTHREAD_CREATE_DETACHED属性。

多进程的简单代码示例如下:

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

void *thread_worker1(void *args);
void *thread_worker2(void *args);

int main(int argc,char **argv)
{
        int             shared_var = 1000;
        pthread_t       tid;
        pthread_attr_t  thread_attr;

        if( pthread_attr_init(&thread_attr) )           //将该线程初始化
        {
                printf("pthraed_attr_init() failure: %s\n",strerror(errno));
                return -1;
        }
        if( pthread_attr_setstacksize(&thread_attr,120*1024) )  //设置线程栈的>大小
        {
                printf("pthread_attr_setstacksize() failure: %s\n",strerror(errno));
                return -1;
        }
        if( pthread_attr_setdetachstate(&thread_attr,PTHREAD_CREATE_DETACHED) ) //设置线程的属性为分离状态
        {
                printf("pthread_sttr_setdetachstate() failure: %s\n",strerror(errno));
                return -1;
        }

        printf("sucessfully excute!!!");

        pthread_create(&tid,&thread_attr,thread_worker1,&shared_var);
        //创建了一个线程,该线程的执行任务是去执行函数thread_worker1,shared_var是将要去执行的这个函数的参数
        //第二个参数中的thread_attr设置了属性PTHREAD_CREATE_DEtACHED,表示将线>程设置成了分离状态

        printf("thread worker1 tid[%ld] created ok\n",tid);
        pthread_create(&tid,NULL,thread_worker2,&shared_var);
        //创建了一个线程,该线程的任务是去执行函数thread_worker2,balbala同上
        //但是没有设置线程的属性

        printf("thread worker2 tid[%ld] created ok\n",tid);

        pthread_attr_destroy(&thread_attr);     //在线程属性使用完之后,我们应>该调用该函数把它摧毁释放


        //wait until thread worker2 exit()

        pthread_join(tid,NULL);         //以阻塞的方式等待thread指定的线程结束
                                        //当函数返回时,被等待的线程的资源被收>回
                                        //如果线程结束,该函数会立即返回
        while(1)
        {
                printf("main/control thread shared_var: %d\n",shared_var);
                sleep(10);
        }

        return 0;
}
void *thread_worker1(void *args)
{
        int             *ptr = (int *)args;
        if(!args)
        {
                printf("%s() get invalid arguments\n", __FUNCTION__);
                pthread_exit(NULL);
        }
        printf("thread worker 1 {%ld} start running...\n",pthread_self());

        while(1)
        {
                printf("+++: %s before shared_var++: %d\n",__FUNCTION__,*ptr);
                *ptr += 1;
                sleep(2);
                printf("+++: %s after sleep shared_var: %d\n",__FUNCTION__,*ptr);
        }
        printf("thread worker 1 exit...\n");
        return NULL;
}
void *thread_worker2(void *args)
{
        int             *ptr = (int *)args;

        if( !args )
        {
                printf("%s() get invalid arguments\n",__FUNCTION__);
                pthread_exit(NULL);
        }

        printf("thread worker 2 [%ld] start running...\n",pthread_self());
        while(1)
        {
                printf("---: %s before shared_var++: %d\n",__FUNCTION__,*ptr);
                *ptr += 1;
                sleep(2);
                printf("---:%s after sleep ahsred_var: %d\n",__FUNCTION__,*ptr);
        }
        printf("thread worker 2 exit...\n");
        return NULL;
}
                       

其运行结果如下图所示:
在这里插入图片描述
我们看见,在线程1去执行thread_worker1()函数的时候,在自加之前shared_var的值是1000,在“after sleep”之后 shared_var的值却变成了1002。由执行结果我们可以知道,这个结果的造成是因为线程2也去执行thread_worker2()函数之后,对该变量完成了加一。

由此我们引申出一个知识点(敲小黑板辣),“如果一个资源会被不同的线程访问修改,那么我们把这个资源叫做临界资源叫做临界资源,,对于该资源访问修改相关的代码就叫做临界区”。所以我们需要考虑的问题就是:我们要如何在多进程之间共享同一个共享资源的时候,保证数据的安全性和准确性呢?在此不得不讲的是:锁机制。

先看示例代码:

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

void *thread_worker1(void *args);
void *thread_worker2(void *args);

typedef struct worker_zxp_s             //给执行函数传递参数的时候,除了传递共>享的变量以外,还有一个互斥锁,所以定义了一个结构体
{
        int             shared_var;
        pthread_mutex_t lock;
}worker_zxp_t;

int main(int argc,char **argv)
{
        worker_zxp_t            worker_zxp;     //定义了传给子进程的变量参数
        pthread_t               tid;
        pthread_attr_t          thread_attr;

        worker_zxp.shared_var = 1000;
        pthread_mutex_init(&worker_zxp.lock,NULL);      //该函数用来初始化互斥>锁

        if(pthread_attr_init(&thread_attr))
        {
                printf("pthread_attr_init() failure: %s\n",strerror(errno));
                return -1;
        }
        if(pthread_attr_setstacksize(&thread_attr,120*1024))
        {
                printf("pthread_attr_setstacksize() failure: %s\n",strerror(errno));
                return -1;
        }
        if(pthread_attr_setdetachstate(&thread_attr,PTHREAD_CREATE_DETACHED))
        {
                printf("pthread_attr_setdetachstate() failure: %s\n",strerror(errno));
                return -1;
        } 
         
        pthread_create(&tid,&thread_attr,thread_worker1,&worker_zxp);
        printf("thread worker1 tid[%ld] create ok\n",tid);
         pthread_create(&tid,&thread_attr,thread_worker2,&worker_zxp);
        printf("thread worker2 tid[%ld] created ok\n",tid);

        //两个线程都设置了分离属性,这时主线程后面的while(1)就会执行了

        while(1)
        {
                printf("Main/control thread shared_var: %d\n",worker_zxp.shared_var);
                sleep(10);
        }
        pthread_mutex_destroy(&worker_zxp.lock);        //互斥锁在使用完毕之后>,需要该函数来释放锁
}
void *thread_worker1(void *args)
{
        worker_zxp_t            *zxp = (worker_zxp_t*)args;

        if(!args)
        {
                printf("%s() get invalid arguments\n",__FUNCTION__);
                pthread_exit(NULL);
        }

        printf("thread worker1[%ld] start running...\n",pthread_self());

        while(1)
        {
                pthread_mutex_lock(&zxp -> lock);       //这里调用该函数来来申>请锁,这里是阻塞锁,如过锁被别的线程持有,则该函数不会返回

                printf("+++: %s before shared_var++: %d\n",__FUNCTION__,zxp->shared_var);
                zxp->shared_var ++;
                sleep(2);
                printf("+++: %s after sleep shard_var: %d\n",__FUNCTION__,zxp->shared_var);

                pthread_mutex_unlock(&zxp->lock);       //在访问临界资源完毕之>后,需要释放锁,使其它的线程能再次访问
                sleep(1);
        }
        printf("thread worker 1 exit...\n");

        return NULL;
}
void *thread_worker2(void *args)
{
        worker_zxp_t            *zxp = (worker_zxp_t*)args;

        if(!args)
        {
                printf("%s() get invalid arguments\n",__FUNCTION__);
                pthread_exit(NULL);

        }
        printf("thread worker 2 [%ld] start running...\n",pthread_self());

        while(1)
        {
                if(0 != pthread_mutex_trylock(&zxp->lock))		//这里申请的锁是非堵塞锁,如果锁现在被别的线程占用则返回非0值,如果没有被占用则返回0
                {
                        continue;
                }
                printf("---:%s before shared_var++:%d\n",__FUNCTION__,zxp->shared_var);
                zxp->shared_var++;
                sleep(2);			//这里和下面休眠的原因是避免一个进程一直占有锁
                printf("---:%s after sleep shared_var:%d\n",__FUNCTION__,zxp->shared_var);
                pthread_mutex_unlock(&zxp->lock);
                sleep(1);
        }

        printf("thread worker 2 exit...\n");

        return NULL;
}   

其程序的运行结果如图所示:
在这里插入图片描述
我分别用两种颜色(两个颜色有点相近这不是我的本意…)任意框了线程对共享变量的操作,可见在一个线程去修改共享资源的时候,我们通过锁机制实现了数据的安全性和正确性。(小锁不愧是你)

四、死锁的简单介绍

当多个线程需要获取多个资源的时候,就有可能会出现死锁的情况。本质情况如下图所示:
在这里插入图片描述
还有著名的哲学家问题:就是五个(到底几个我记不得了,反正是这个意思),大家坐在圆桌上吃饭,五个人,五支筷子,每个人的左右手边都有筷子,拿到两支筷子就可以吃饭,没拿到或者只拿到一支筷子就进入等待状态。
设想一种场景:哲学家们同时拿起了左手边的筷子,这时他们都进入了等待状态,并且这种状态是不会解除的,这就是死锁。

死锁产生的四个必要条件:
1.互斥:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其它进程就不能再访问,直到该进程访问结束;
2.占用且等待:一个进程本身占用资源(一种或多种),同时还有资源未得到满足,正在等待其它进程释放该资源;
3.不可抢占:别人已经占用了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来;
4.循环等待:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源
当以上四个条件均满足,必然会造成死锁,发生死锁无法进行下去,他们所持有的资源也无法释放。这样会导致CPU的吞吐量下降。所以死锁情况是会浪费系统资源和影响计算机的使用性能的

我们的解决办法是:
产生死锁需要四个条件,那么,只有这四个条件中至少有一个条件得不到满足,就不可能发生死锁了,由于互斥条件是非共享资源所必须的,不仅不能改变,还应该加以保证,所以,主要是破坏死锁的其它三个条件。
代码如下(示例):


总结

这是本篇多线程编程,有什么需要调整的我再整一整。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐