进程和线程那点事儿

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

先在此申明。本文有部分内容来自网络,不过其中的代码都进行了一一验证,可以放心借鉴使用。

-------------------------------------------------------------------------

进程(process)和线程(thread)是操作系统的基本概念,但是它们比较抽象,不容易掌握。

进程与线程的一个简单解释:

了解一下进程和线程的宏观概念:
进程,是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竟争计算机系统资源的基本单位。每一个进程都有一个自己的地址空 间,即进程空间或(虚空间)。进程空间的大小 只与处理机的位数有关,一个 16 位长处理机的进程空间大小为 216 ,而 32 位处理机的进程空间大小为 232 。进程至少有 5 种基本状态,它们是:初始态,执行态,等待状态,就绪状态,终止状态。线程,在网络或多用户环境下,一个服务器通常需要接收大量且不确定数量用户的并发请求,为每一个请求都创建一个进程显然是行不通的,——无论是从系统资源开销方面或是响应用户请求的效率方面来看。因此,操作系统中线程的概念便被引进了。线程,是进程的一部分,一个没有线程的进程可以被看作是单线程的。线程有时又被称为轻权进程或轻量级进程,也是 CPU 调度的一个基本单位。
说到这里,我们对进程与线程都有了一个大体上的印象,现在开始说说二者大致的区别。
进程的执行过程是线状的, 尽管中间会发生中断或暂停,但该进程所拥有的资源只为该线状执行过程服务。一旦发生进程上下文切换,这些资源都是要被保护起来的。这是进程宏观上的执行过 程。而进程又可有单线程进程与多线程进程两种。我们知道,进
程有 一个进程控制块 PCB ,相关程序段 和 该程序段对其进行操作的数据结构集 这三部分,单线程进程的执行过程在宏观上是线性的,微观上也只有单一的执行过程;而多线程进程在宏观上的执行过程同样为线性的,但微观上却可以有多个执行 操作(线程),如不同代码片段以及相关的数据结构集。线程的改变只代表了 CPU 执行过程的改变,而没有发生进程所拥有的资源变化。除了 CPU 之外,计算机内的软硬件资源的分配与线程无关,线程只能共享它所属进程的资源。与进程控制表和 PCB 相似,每个线程也有自己的线程控制表 TCB ,而这个 TCB 中所保存的线程状态信息则要比 PCB 表少得多,这些信息主要是相关指针用堆栈(系统栈和用户栈),寄存器中的状态数据。进程拥有一个完整的虚拟地址空间,不依赖于线程而独立存在;反之,线程是进程的一部分,没有自己的地址空间,与进程内的其他线程一起共享分配给该进程的所有资源。
     线程可以有效地提高系统的执行效率,但并不是在所有计算机系统中都是适用的,如某些很少做进程调度和切换的实时系统。使用线程的好处是有多个任务需要处理 机处理时,减少处理机的切换时间;而且,线程的创建和结束所需要的系统开销也比进程的创建和结束要小得多。最适用使用线程的系统是多处理机系统和网络系统 或分布式系统。
线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。 
    从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。


1. 线程的执行特性。
    线程只有 3 个基本状态:就绪,执行,阻塞。
   线程存在 5 种基本操作来切换线程的状态:派生,阻塞,激活,调度,结束。

2. 进程通信。
    单机系统中进程通信有 4 种形式:主从式,会话式,消息或邮箱机制,共享存储区方式。
    主从式典型例子:终端控制进程和终端进程。
    会话式典型例子:用户进程与磁盘管理进程之间的通信。

关于多进程和多线程:

一.为何需要多进程(或者多线程),为何需要并发?

这个问题或许本身都不是个问题。但是对于没有接触过多进程编程的朋友来说,他们确实无法感受到并发的魅力以及必要性。
我想,只要你不是整天都写那种int main()到底的代码的人,那么或多或少你会遇到代码响应不够用的情况,也应该有尝过并发编程的甜头。就像一个快餐点的服务员,既要在前台接待客户点 餐,又要接电话送外卖,没有分身术肯定会忙得你焦头烂额的。幸运的是确实有这么一种技术,让你可以像孙悟空一样分身,灵魂出窍,乐哉乐哉地轻松应付一切状 况,这就是多进程/线程技术。并发技术,就是可以让你在同一时间同时执行多条任务的技术。你的代码将不仅仅是从上到下,从左到右这样规规矩矩的一条线执行。你可以一条线在main函数里跟你的客户交流,另一条线,你早就把你外卖送到了其他客户的手里。
所以,为何需要并发?因为我们需要更强大的功能,提供更多的服务,所以并发,必不可少。

二.多进程
什么是进程。最直观的就是一个个pid,官方的说法就:进程是程序在计算机上的一次执行活动。说得简单点,下面这段代码执行的时候:

#include"stdio.h"
#include"sys/types.h"
#include"unistd.h"

typedef void (*FUN)();

//进程退出时执行的函数
void print_exit()
{
        printf("exit process:%d\n",getpid());
}

void g_fork(pid_t pid,FUN chlidfoo,FUN parnetfoo)
{
        pid = fork();
        if(pid <0) 
        {
                printf("fork error!\n");
        }
        else if(pid == 0)
        {//子进程执行的函数==》子进程具有父进程所有属性和资源
                (*chlidfoo)();
                printf("child process,pid :%d\n",getpid());
        }
        else
        {// 父进程执行的函数
                (*parnetfoo)();
                printf("parent process,pid:%d\n",getpid());
                sleep(3);
                wait();
        }
}

void print_test()
{
        printf("call back test\n");
}


int main(void)
{
        //打印主进程的pid
        printf("main pid:%d\n",getpid());
        atexit(print_exit);//进程退出时执行的函数
        pid_t pid;

        g_fork(pid,print_test,print_test);
        return 0;
}
关于fork函数,功能就是产生子进程,由于前面说过,进程就是执行的流程活动。
那么fork产生子进程的表现就是它会返回2次,一次返回0,顺序执行下面的代码。这是子进程。
一次返回子进程的pid,也顺序执行下面的代码,这是父进程。
(为何父进程需要获取子进程的pid呢?这个有很多原因,其中一个原因:看最后的wait,就知道父进程等待子进程的终结后,处理其task_struct结构,否则会产生僵尸进程,扯远了,有兴趣可以自己google)。
如果fork失败,会返回-1.
额外说下atexit( print_exit ); 需要的参数肯定是函数的调用地址。
这里的print_exit 是函数名还是函数指针呢?答案是函数指针,函数名永远都只是一串无用的字符串。
某本书上的规则:函数名在用于非函数调用的时候,都等效于函数指针。

说到子进程只是一个额外的流程,那他跟父进程的联系和区别是什么呢?

我很想建议你看看linux内核的注解(有兴趣可以看看,那里才有本质上的了解),总之,fork后,子进程会复制父进程的task_struct结构,并为子进程的堆栈分配物理页。理论上来说,子进程应该完整地复制父进程的堆,栈以及数据空间,但是2者共享正文段。
关于写时复制:由于一般 fork后面都接着exec,所以,现在的 fork都在用写时复制的技术,顾名思意,就是, 数据段,堆,栈,一开始并不复制,由父,子进程共享,并将这些内存设置为只读。直到父,子进程一方尝试写这些区域,则内核才为需要修改的那片内存拷贝副 本。这样做可以提高 fork的效率。

三.多线程

    线程是可执行代码的可分派单元。这个名称来源于“执行的线索”的概念。在基于线程的多任务的环境中,所有进程有至少一个线程,但是它们可以具有多个任务。这意味着单个程序可以并发执行两个或者多个任务。
简而言之,线程就是把一个进程分为很多片,每一片都可以是一个独立的流程。这已经明显不同于多进程了,进程是一个拷贝的 流程,而线程只是把一条河流截成很多条小溪。它没有拷贝这些额外的开销,但是仅仅是现存的一条河流,就被多线程技术几乎无开销地转成很多条小流程,它的伟 大就在于它少之又少的系统开销。(当然伟大的后面又引发了重入性等种种问题,这个后面慢慢比)。
线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者。传统的Unix也支持线程的概念,但是在一个进程(process)中只允许有一个线程,这样多线程就意味着多进程。
  为什么有了进程的概念后,还要再引入线程呢?使用多线程到底有哪些好处?什么的系统应该选用多线程?我们首先必须回答这些问题。
  使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。
  使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。
  除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:
  1) 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。 
  2) 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。 
  3) 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
  下面我们先来尝试编写一个简单的多线程程序。

简单的多线程编程

  Linux系统下的多线程遵循POSIX线程接口,称为pthread。编写Linux下的多线程程序,需要使用头文件pthread.h,连接时需要使用库libpthread.a。顺便说一下,Linux下pthread的实现是通过系统调用clone()来实现的。clone()是Linux所特有的系统调用,它的使用方式类似fork,关于clone()的详细情况,有兴趣的读者可以去查看有关文档说明。

#include"stdio.h"
#include"pthread.h"
#include"stdlib.h"

void g_thread(void)
{
        int i =0;
        for(i=0;i<100;i++)
        {
                printf("this is a thread\n");
        }
}


int main(void)
{
	//创建线程
        pthread_t tid;
        int ret = pthread_create(&tid,NULL,(void*)g_thread,NULL);//默认属性,非分离状态
        if(ret != 0)
        {
                printf("create pthread error\n");
                exit(1);
        }

	//主进程执行函数
        int i=0;
        for(i = 0;i<100;i++)
        {
                printf("this is main process\n");
        }

        pthread_join(tid,NULL);//函数pthread_join用来等待一个线程的结束,并回收资源
        return 0;
}
上面的示例中,我们使用到了两个函数,  pthread_create和pthread_join,并声明了一个pthread_t型的变量。 
    pthread_t在头文件/usr/include/bits/pthreadtypes.h中定义: 
    typedef unsigned long int pthread_t; 
    它是一个线程的标识符。函数pthread_create用来创建一个线程,它的原型为: 
    extern int pthread_create __P ((pthread_t *__thread, __const pthread_attr_t *__attr, 
    void *(*__start_routine) (void *), void *__arg)); 
    第一个参数为指向线程标识符的指针,第二个参数用来设置线程属性,第三个参数是线程运行函数的起始地址,最后一个参数是运行函数的参数。这里,我们的函数thread不需要参数,所以最后一个参数设为空指针。第二个参数我们也设为空指针,这样将生成默认属性的线程。对线程属性的设定和修改我们将在下一节阐述。当创建线程成功时,函数返回0,若不为0则说明创建线程失败,常见的错误返回代码为EAGAIN和EINVAL。前者表示系统限制创建新的线程,例如线程数目过多了;后者表示第二个参数代表的线程属性值非法。创建线程成功后,新创建的线程则运行参数三和参数四确定的函数,原来的线程则继续运行下一行代码。
   函数pthread_join用来等待一个线程的结束。函数原型为: 
    extern int pthread_join __P ((pthread_t __th, void **__thread_return));
    第一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。一个线程的结束有两种途径,一种是象我们上面的例子一样,函数结束了,调用它的线程也就结束了;另一种方式是通过函数pthread_exit来实现。它的函数原型为: 
  extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__));
    唯一的参数是函数的返回代码,只要pthread_join中的第二个参数thread_return不是NULL,这个值将被传递给thread_return。

     最后要说明的是,一个线程不能被多个线程等待,否则第一个接收到信号的线程成功返回,其余调用pthread_join的线程则返回错误。

   在这一节里,我们编写了一个最简单的线程,并掌握了最常用的三个函数pthread_create,pthread_join和pthread_exit。下面,我们来了解线程的一些常用属性以及如何设置这些属性。


修改线程的属性

    属性结构为pthread_attr_t,它同样在头文件/usr/include/pthread.h中定义,喜欢追根问底的人可以自己去查看。属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。属性对象主要包括是否绑定、是否分离、堆栈地址、堆栈大小、优先级。默认的属性为非绑定、非分离、缺省1M的堆栈、与父进程同样级别的优先级。
  关于线程的绑定,牵涉到另外一个概念:轻进程(LWP:Light Weight Process)。轻进程可以理解为内核线程,它位于用户层和系统层之间。系统对线程资源的分配、对线程的控制是通过轻进程来实现的,一个轻进程可以控制一个或多个线程。默认状况下,启动多少轻进程、哪些轻进程来控制哪些线程是由系统来控制的,这种状况即称为非绑定的。绑定状况下,则顾名思义,即某个线程固定的"绑"在一个轻进程之上。被绑定的线程具有较高的响应速度,这是因为CPU时间片的调度是面向轻进程的,绑定的线程可以保证在需要的时候它总有一个轻进程可用。通过设置被绑定的轻进程的优先级和调度级可以使得绑定的线程满足诸如实时反应之类的要求。
    设置线程绑定状态的函数为pthread_attr_setscope,它有两个参数,第一个是指向属性结构的指针,第二个是绑定类型,它有两个取值:PTHREAD_SCOPE_SYSTEM(绑定的)和PTHREAD_SCOPE_PROCESS(非绑定的)。下面的代码即创建了一个绑定的线程

#include <pthread.h>
pthread_attr_t attr; pthread_t tid; /*初始化属性值,均设为默认值*/
pthread_attr_init(&attr);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);

pthread_create(&tid, &attr, (void *) my_function, NULL);


  线程的分离状态决定一个线程以什么样的方式来终止自己。在上面的例子中,我们采用了线程的默认属性,即为非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。而分离线程不是这样子的,它没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。程序员应该根据自己的需要,选择适当的分离状态。设置线程分离状态的函数为pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)。第二个参数可选PTHREAD_CREATE_DETACHED(分离线程)和 PTHREAD _CREATE_JOINABLE(非分离线程)。这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timewait函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。

#include <pthread.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <ctype.h>
#include <sched.h>
#include<sys/time.h>  //pthread_cond_timewait()

pthread_cond_t cond;//线程阻塞/解除(唤醒)条件
pthread_mutex_t mutex;//线程锁

void g_thread(void* arg)
{
        struct timeval now;
        struct timespec outtime;
        pthread_mutex_lock(&mutex);//给线程上锁

        int i =0;
        for(i=0;i<100;i++)
        {
                printf("this is a thread\n");
        }
      
        gettimeofday(&now,NULL);
        outtime.tv_sec = now.tv_sec + 5;
        outtime.tv_nsec = now.tv_usec*1000;
        //阻塞线程
        pthread_cond_timedwait(&cond,&mutex,&outtime);
        pthread_mutex_unlock(&mutex);//给线程解锁
        printf("thread exit\n");
}

int main(void)
{
        //设置线程是否绑定到轻进程的属性
        pthread_attr_t attr;
        pthread_attr_init(&attr);//初始化线程属性
        //PTHREAD_SCOPE_SYSTEM 绑定
        //PTHREAD_SCOPE_PROCESS 非绑定
        pthread_attr_setscope(&attr,PTHREAD_SCOPE_SYSTEM);

        //设置线程的分离状态
        //PTHREAD_CREATE_DETACHED 分离线程
        //PTHREAD_CREATE_JOINABLE 非分离线程

        pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);//分离线程

        pthread_mutex_init(&mutex,NULL);
        pthread_cond_init(&cond,NULL);

        pthread_t tid;
        int ret = pthread_create(&tid,&attr,(void*)g_thread,NULL);
        if(ret != 0)
        {
                printf("create pthread error\n");
                exit(1);
        }

        int i=0;
        for(i = 0;i<100;i++)
        {
                printf("this is main process\n");
        }

        pthread_mutex_lock(&mutex);
        pthread_cond_signal(&cond);
        pthread_mutex_unlock(&mutex);

        pthread_join(tid,NULL);
        return 0;
}

一个linux手册给出的多线程的例子


/*The program below demonstrates the use of pthread_create(), as well as a number of other functions in the pthreads API. In  the following run, on a system providing the NPTL threading imple‐ mentation, the stack size defaults to the value given  by  the  "stack size" resource limit:
           $ ulimit -s
           8192            # The stack size limit is 8 MB (0x800000 bytes)
           $ ./a.out hola salut servus
           Thread 1: top of stack near 0xb7dd03b8; argv_string=hola
           Thread 2: top of stack near 0xb75cf3b8; argv_string=salut
           Thread 3: top of stack near 0xb6dce3b8; argv_string=servus
           Joined with thread 1; returned value was HOLA
           Joined with thread 2; returned value was SALUT
           Joined with thread 3; returned value was SERVUS


       In  the  next  run,  the  program  explicitly sets a stack size of 1MB
       (using pthread_attr_setstacksize(3)) for the created threads:


           $ ./a.out -s 0x100000 hola salut servus
           Thread 1: top of stack near 0xb7d723b8; argv_string=hola
           Thread 2: top of stack near 0xb7c713b8; argv_string=salut
           Thread 3: top of stack near 0xb7b703b8; argv_string=servus
           Joined with thread 1; returned value was HOLA
           Joined with thread 2; returned value was SALUT
           Joined with thread 3; returned value was SERVUS


   Program source: 
#include <pthread.h>
       #include <string.h>
       #include <string.h>
       #include <stdio.h>
       #include <stdlib.h>
       #include <unistd.h>
       #include <errno.h>
       #include <ctype.h>


       #define handle_error_en(en, msg) \
               do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)


       #define handle_error(msg) \
               do { perror(msg); exit(EXIT_FAILURE); } while (0)


       struct thread_info {    /* Used as argument to thread_start() */
           pthread_t thread_id;        /* ID returned by pthread_create() */
           int       thread_num;       /* Application-defined thread # */
           char     *argv_string;      /* From command-line argument */
       };


       /* Thread start function: display address near top of our stack,
          and return upper-cased copy of argv_string */


       static void *
       thread_start(void *arg)
       {
           struct thread_info *tinfo = arg;
           char *uargv, *p;


           printf("Thread %d: top of stack near %p; argv_string=%s\n",
                   tinfo->thread_num, &p, tinfo->argv_string);


           uargv = strdup(tinfo->argv_string);
           if (uargv == NULL)
               handle_error("strdup");


           for (p = uargv; *p != '\0'; p++)
               *p = toupper(*p);


           return uargv;
       }


       int
       main(int argc, char *argv[])
       {
           int s, tnum, opt, num_threads;
           struct thread_info *tinfo;
           pthread_attr_t attr;
           int stack_size;
           void *res;


           /* The "-s" option specifies a stack size for our threads */


           stack_size = -1;
           while ((opt = getopt(argc, argv, "s:")) != -1) {
               switch (opt) {
               case 's':
                   stack_size = strtoul(optarg, NULL, 0);
                   break;


               default:
                   fprintf(stderr, "Usage: %s [-s stack-size] arg...\n",
                           argv[0]);
                   exit(EXIT_FAILURE);
               }
           }


           num_threads = argc - optind;


           /* Initialize thread creation attributes */


           s = pthread_attr_init(&attr);
           s = pthread_attr_init(&attr);
           if (s != 0)
               handle_error_en(s, "pthread_attr_init");


           if (stack_size > 0) {
               s = pthread_attr_setstacksize(&attr, stack_size);
               if (s != 0)
                   handle_error_en(s, "pthread_attr_setstacksize");
           }
           if (stack_size > 0) {
               s = pthread_attr_setstacksize(&attr, stack_size);
               if (s != 0)
                   handle_error_en(s, "pthread_attr_setstacksize");
           }


           /* Allocate memory for pthread_create() arguments */


           tinfo = calloc(num_threads, sizeof(struct thread_info));
           if (tinfo == NULL)
               handle_error("calloc");


           /* Create one thread for each command-line argument */


           for (tnum = 0; tnum < num_threads; tnum++) {
               tinfo[tnum].thread_num = tnum + 1;
               tinfo[tnum].argv_string = argv[optind + tnum];


               /* The pthread_create() call stores the thread ID into
                  corresponding element of tinfo[] */


               s = pthread_create(&tinfo[tnum].thread_id, &attr,
                                  &thread_start, &tinfo[tnum]);
               if (s != 0)
                   handle_error_en(s, "pthread_create");
           }


           /* Destroy the thread attributes object, since it is no
              longer needed */


           s = pthread_attr_destroy(&attr);
           if (s != 0)
               handle_error_en(s, "pthread_attr_destroy");


           /* Now join with each thread, and display its returned value */


           for (tnum = 0; tnum < num_threads; tnum++) {
               s = pthread_join(tinfo[tnum].thread_id, &res);
               if (s != 0)
                   handle_error_en(s, "pthread_join");


               printf("Joined with thread %d; returned value was %s\n",
                       tinfo[tnum].thread_num, (char *) res);
               free(res);      /* Free memory allocated by thread */
           }


           free(tinfo);
           exit(EXIT_SUCCESS);
       }

线程的数据处理:

  进程相比,线程的最大优点之一是数据的共享性,各个进程共享父进程处沿袭的数据段,可以方便的获得、修改数据。但这也给多线程编程带来了许多问题。我们必须当心有多个不同的进程访问相同的变量。许多函数是不可重入的,即同时不能运行一个函数的多个拷贝(除非使用不同的数据段)。在函数中声明的静态变量常常带来问题,函数的返回值也会有问题。因为如果返回的是函数内部静态声明的空间的地址,则在一个线程调用该函数得到地址后使用该地址指向的数据时,别的线程可能调用此函数并修改了这一段数据。在进程中共享的变量必须用关键字volatile来定义,这是为了防止编译器在优化时(如gcc中使用-OX参数)改变它们的使用方式。为了保护变量,我们必须使用信号量、互斥等方法来保证我们对变量的正确使用。下面,我们就逐步介绍处理线程数据时的有关知识。 

 线程数据

  在单线程的程序里,有两种基本的数据:全局变量和局部变量。但在多线程程序里,还有第三种数据类型:线程数据(TSD: 
Thread-Specific Data)。它和全局变量很象,在线程内部,各个函数可以象使用全局变量一样调用它,但它对线程外部的其它线程是不可见的。这种数据的必要性是显而易见的。例如我们常见的变量errno,它返回标准的出错信息。它显然不能是一个局部变量,几乎每个函数都应该可以调用它;但它又不能是一个全局变量,否则在A线程里输出的很可能是B线程的出错信息。要实现诸如此类的变量,我们就必须使用线程数据。我们为每个线程数据创建一个键,它和这个键相关联,在各个线程里,都使用这个键来指代线程数据,但在不同的线程里,这个键代表的数据是不同的,在同一个线程里,它代表同样的数据内容。
    和线程数据相关的函数主要有4个:创建一个键;为一个键指定线程数据;从一个键读取线程数据;删除键。 
    创建键的函数原型为: 
    extern int pthread_key_create __P ((pthread_key_t *__key, 
    void (*__destr_function) (void *)));

  第一个参数为指向一个键值的指针,第二个参数指明了一个destructor函数,如果这个参数不为空,那么当每个线程结束时,系统将调用这个函数来释放绑定在这个键上的内存块。

      这个函数常和函数pthread_once ((pthread_once_t*once_control, void  (*initroutine) (void)))一起使用,为了让这个键只被创建一次。函数pthread_once声明一个初始化函数,第一次调用 pthread_once 时它执行这个函数,以后的调用将被它忽略。

  在下面的例子中,我们创建一个键,并将它和某个数据相关联。我们要定义一个函数createWindow,这个函数定义一个图形窗口(数据类型为Fl_Window *,这是图形界面开发工具FLTK中的数据类型)。由于各个线程都会调用这个函数,所以我们使用线程数据。


/* 声明一个键*/
pthread_key_t myWinKey;
 
/* 函数 createWindow */
void createWindow ( void )
{
  Fl_Window * win;
  static pthread_once_t once= PTHREAD_ONCE_INIT;
  /* 调用函数createMyKey,创建键*/
  pthread_once ( &once, createMyKey) ;
  /*win指向一个新建立的窗口*/
  win=new Fl_Window( 0, 0, 100, 100, "MyWindow");
  /* 对此窗口作一些可能的设置工作,如大小、位置、名称等*/
  setWindow(win);
  /* 将窗口指针值绑定在键myWinKey上*/
  pthread_setpecific ( myWinKey, win);
}
 
/* 函数 createMyKey,创建一个键,并指定了destructor */
void createMyKey ( void )
{
  pthread_keycreate(&myWinKey, freeWinKey);
}
 
/* 函数 freeWinKey,释放空间*/
void freeWinKey ( Fl_Window * win)
{
  delete win;
}
 
  这样,在不同的线程中调用函数createMyWin,都可以得到在线程内部均可见的窗口变量,这个变量通过函数pthread_getspecific得到。在上面的例子中,我们已经使用了函数pthread_setspecific来将线程数据和一个键绑定在一起。这两个函数的原型如下: 
      extern int pthread_setspecific __P ((pthread_key_t __key,__const void *__pointer)); 
      extern void *pthread_getspecific __P ((pthread_key_t __key));
  这两个函数的参数意义和使用方法是显而易见的。要注意的是,用pthread_setspecific为一个键指定新的线程数据时,必须自己释放原有的线程数据以回收空间。这个过程函数pthread_key_delete用来删除一个键,这个键占用的内存将被释放,但同样要注意的是,它只释放键占用的内存,并不释放该键关联的线程数据所占用的内存资源,而且它也不会触发函数pthread_key_create中定义的destructor函数。线程数据的释放必须在释放键之前完成。
 
互斥锁
  互斥锁用来保证一段时间内只有一个线程在执行一段代码。必要性显而易见:假设各个线程向同一个文件顺序写入数据,最后得


到的结果一定是灾难性的。
  我们先看下面一段代码。这是一个读/写程序,它们公用一个缓冲区,并且我们假定一个缓冲区只能保存一条信息。即缓冲区只


有两个状态:有信息或没有信息。
 
void reader_function ( void );
void writer_function ( void );
char buffer;
int buffer_has_item=0;
pthread_mutex_t mutex;
struct timespec delay;
void main ( void )
{
  pthread_t reader;
  /* 定义延迟时间*/
  delay.tv_sec = 2;
  delay.tv_nec = 0;
  /* 用默认属性初始化一个互斥锁对象*/
  pthread_mutex_init (&mutex,NULL);
  pthread_create(&reader, pthread_attr_default, (void *)&reader_function), NULL);
  writer_function( );
}
 
void writer_function (void)
{
  while(1) {
    /* 锁定互斥锁*/
    pthread_mutex_lock (&mutex);
    if (buffer_has_item==0) {
      buffer=make_new_item( );
      buffer_has_item=1;
    }
    /* 打开互斥锁*/
    pthread_mutex_unlock(&mutex);
    pthread_delay_np(&delay);
  }
}
 
void reader_function(void)
{
  while(1) {
    pthread_mutex_lock(&mutex);
    if(buffer_has_item==1)
    {
      consume_item(buffer);
      buffer_has_item=0;
     }
    pthread_mutex_unlock(&mutex);
    pthread_delay_np(&delay);
  }
}
  这里声明了互斥锁变量mutex,结构pthread_mutex_t为不公开的数据类型,其中包含一个系统分配的属性对象。函数pthread_mutex_init用来生成一个互斥锁。NULL参数表明使用默认属性。如果需要声明特定属性的互斥锁,须调用函数pthread_mutexattr_init。函数pthread_mutexattr_setpshared和函数pthread_mutexattr_settype用来设置互斥锁属性。前一个函数设置属性pshared,它有两个取值,PTHREAD_PROCESS_PRIVATE和PTHREAD_PROCESS_SHARED。前者用来不同进程中的线程同步,后者用于同步本进程的不同线程。在上面的例子中,我们使用的是默认属性PTHREAD_PROCESS_ PRIVATE。后者用来设置互斥锁类型,可选的类型有PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、PTHREAD_MUTEX_RECURSIVE和PTHREAD _MUTEX_DEFAULT。它们分别定义了不同的上锁、解锁机制,一般情况下,选用最后一个默认属性。
  pthread_mutex_lock声明开始用互斥锁上锁,此后的代码直至调用pthread_mutex_unlock为止,均被上锁,即同一时间只能被一个线程调用执行。当一个线程执行到pthread_mutex_lock处时,如果该锁此时被另一个线程使用,那此线程被阻塞,即程序将等待到另一个线程释放此互斥锁。在上面的例子中,我们使用了pthread_delay_np函数,让线程睡眠一段时间,就是为了防止一个线程始终占据此函数。(如果用sleep(2)则是让进程睡眠了)
  上面的例子非常简单,就不再介绍了,需要提出的是在使用互斥锁的过程中很有可能会出现死锁:两个线程试图同时占用两个资源,并按不同的次序锁定相应的互斥锁,例如两个线程都需要锁定互斥锁1和互斥锁2,a线程先锁定互斥锁1,b线程先锁定互斥锁2,这时就出现了死锁。此时我们可以使用函数pthread_mutex_trylock,它是函数pthread_mutex_lock的非阻塞版本,当它发现死锁不可避免时,它会返回相应的信息,程序员可以针对死锁做出相应的处理。另外不同的互斥锁类型对死锁的处理不一样,但最主要的还是要程序员自己在程序设计注意这一点。


条件变量


  前一节中我们讲述了如何使用互斥锁来实现线程间数据的共享和通信,互斥锁一个明显的缺点是它只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条件变量被用来进行线承间的同步。
    条件变量的结构为pthread_cond_t,函数pthread_cond_init()被用来初始化一个条件变量。它的原型为: 

    extern int pthread_cond_init __P ((pthread_cond_t *__cond,__const pthread_condattr_t *__cond_attr));

    其中cond是一个指向结构pthread_cond_t的指针,cond_attr是一个指向结构pthread_condattr_t的指针。结构
pthread_condattr_t是条件变量的属性结构,和互斥锁一样我们可以用它来设置条件变量是进程内可用还是进程间可用,默认值是PTHREAD_ PROCESS_PRIVATE,即此条件变量被同一进程内的各个线程使用。注意初始化条件变量只有未被使用时才能重新初始化或被释放。释放一个条件变量的函数为pthread_cond_ destroy(pthread_cond_t cond)。 

    函数pthread_cond_wait()使线程阻塞在一个条件变量上。它的函数原型为: 
    extern int pthread_cond_wait __P ((pthread_cond_t *__cond,  pthread_mutex_t *__mutex));

    线程解开mutex指向的锁并被条件变量cond阻塞。线程可以被函数pthread_cond_signal和函数pthread_cond_broadcast唤醒,但是要注意的是,条件变量只是起阻塞和唤醒线程的作用,具体的判断条件还需用户给出,例如一个变量是否为0等等,这一点我们从后面的例子中可以看到。线程被唤醒后,它将重新检查判断条件是否满足,如果还不满足,一般说来线程应该仍阻塞在这里,被等待被下一次唤醒。这个过程一般用while语句实现。


    另一个用来阻塞线程的函数是pthread_cond_timedwait(),它的原型为: 
    extern int pthread_cond_timedwait __P ((pthread_cond_t *__cond, pthread_mutex_t *__mutex, __const struct timespec *__abstime));

    它比函数pthread_cond_wait()多了一个时间参数,经历abstime段时间后,即使条件变量不满足,阻塞也被解除。 
    函数pthread_cond_signal()的原型为: 
    extern int pthread_cond_signal __P ((pthread_cond_t *__cond));

    它用来释放被阻塞在条件变量cond上的一个线程。多个线程阻塞在此条件变量上时,哪一个线程被唤醒是由线程的调度策略所决定的。要注意的是,必须用保护条件变量的互斥锁来保护这个函数,否则条件满足信号又可能在测试条件和调用pthread_cond_wait函数之间被发出,从而造成无限制的等待。下面是使用函数pthread_cond_wait()和函数pthread_cond_signal()的一个简单的例子。

pthread_mutex_t count_lock;
pthread_cond_t count_nonzero;
unsigned count;
decrement_count()
{
  pthread_mutex_lock (&count_lock);
  while(count==0)
    pthread_cond_wait( &count_nonzero, &count_lock);
  count=count -1;
  pthread_mutex_unlock (&count_lock);
}
increment_count()
{
  pthread_mutex_lock(&count_lock);
  if(count==0)
    pthread_cond_signal(&count_nonzero);
  count=count+1;
  pthread_mutex_unlock(&count_lock);
}   
  count值为0时,decrement函数在pthread_cond_wait处被阻塞,并打开互斥锁count_lock。此时,当调用到函数increment_count时,pthread_cond_signal()函数改变条件变量,告知decrement_count()停止阻塞。读者可以试着让两个线程分别运行这两个函数,看看会出现什么样的结果。
  函数pthread_cond_broadcast(pthread_cond_t *cond)用来唤醒所有被阻塞在条件变量cond上的线程。这些线程被唤醒后将再次竞争相应的互斥锁,所以必须小心使用这个函数。


信号量


  信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。当公共资源增加时,调用函数sem_post()增加信号量。只有当信号量值大于0时,才能使用公共资源,使用后,函数sem_wait()减少信号量。函数sem_trywait()和函数pthread_ mutex_trylock()起同样的作用,它是函数sem_wait()的非阻塞版本。下面我们逐个介绍和信号量有关的一些函数,它们都在头文件/usr/include/semaphore.h中定义。
  信号量的数据类型为结构sem_t,它本质上是一个长整型的数。函数sem_init()用来初始化一个信号量。它的原型为: 
  extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));
  sem为指向信号量结构的一个指针;pshared不为0时此信号量在进程间共享,否则只能为当前进程的所有线程共享;value给出了信号量的初始值。
  函数sem_post( sem_t *sem )用来增加信号量的值。当有线程阻塞在这个信号量上时,调用这个函数会使其中的一个线程不在阻塞,选择机制同样是由线程的调度策略决定的。 
  函数sem_wait( sem_t *sem )被用来阻塞当前线程直到信号量sem的值大于0,解除阻塞后将sem的值减一,表明公共资源经使用后减少。函数sem_trywait ( sem_t *sem )是函数sem_wait()的非阻塞版本,它直接将信号量sem的值减一。 
  函数sem_destroy(sem_t *sem)用来释放信号量sem。
  下面我们来看一个使用信号量的例子。在这个例子中,一共有4个线程,其中两个线程负责从文件读取数据到公共的缓冲区,另两个线程从缓冲区读取数据作不同的处理(加和乘运算)。


/* File sem.c */
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#define MAXSTACK 100
int stack[MAXSTACK][2];
int size=0;
sem_t sem;
/* 从文件1.dat读取数据,每读一次,信号量加一*/
void ReadData1(void)
{
  FILE *fp=fopen("1.dat","r");
  while(!feof(fp)) {
    fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
    sem_post(&sem);
    ++size;
  }
  fclose(fp);
}
/*从文件2.dat读取数据*/
void ReadData2(void)
{
  FILE *fp=fopen("2.dat","r");
  while(!feof(fp)) {
    fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
    sem_post(&sem);
    ++size;
  }
  fclose(fp);
}
/*阻塞等待缓冲区有数据,读取数据后,释放空间,继续等待*/
void HandleData1(void)
{
  while(1) {
    sem_wait(&sem);
    printf("Plus:%d+%d=%d\n",stack[size][0],stack[size][1], stack[size][0]+stack[size][1]);
    --size;
  }
}
void HandleData2(void)
{
  while(1) {
    sem_wait(&sem);
    printf("Multiply:%d*%d=%d\n",stack[size][0],stack[size][1], stack[size][0]*stack[size][1]);
    --size;
  }
}
int main(void)
{
  pthread_t t1,t2,t3,t4;
  sem_init(&sem,0,0);
  pthread_create(&t1,NULL,(void *)HandleData1,NULL);
  pthread_create(&t2,NULL,(void *)HandleData2,NULL);
  pthread_create(&t3,NULL,(void *)ReadData1,NULL);
  pthread_create(&t4,NULL,(void *)ReadData2,NULL);
  /* 防止程序过早退出,让它在此无限期等待*/
  pthread_join(t1,NULL);
}
  在Linux下,我们用命令gcc -lpthread sem.c -o sem生成可执行文件sem。 我们事先编辑好数据文件1.dat和2.dat,假设它们的内容分别为1 2 3 4 5 6 7 8 9 10和 -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 ,我们运行sem,得到如下的结果: 
    Multiply:-1*-2=2 
    Plus:-1+-2=-3 
    Multiply:9*10=90 
    Plus:-9+-10=-19 
    Multiply:-7*-8=56 
    Plus:-5+-6=-11 
    Multiply:-3*-4=12 
    Plus:9+10=19 
    Plus:7+8=15 
    Plus:5+6=11 
  从中我们可以看出各个线程间的竞争关系。而数值并未按我们原先的顺序显示出来这是由于size这个数值被各个线程任意修改的缘故。这也往往是多线程编程要注意的问题。 


总结:

    关于c语言下的进程和线程就说到这儿了。接下来会抽时间整理下多进程和多线程用c++ 和Qt 下的实现方法及具体案例。


猜你喜欢

转载自blog.csdn.net/weixin_39568531/article/details/80067459
今日推荐