zoukankan      html  css  js  c++  java
  • linux多线程学习

    首先,编写一个耗时的单线程程序:

    #include<cstdio>
    #include<unistd.h>
    int main()
    {
        sleep(5);
        printf("program exited.
    ");
    }

    编译并运行这段程序,该程序5秒后输出,sleep期间不再响应其它消息或执行其他操作。为了更好地处理这种耗时的操作,我们需要使用多线程编程。

    先从书上抄些东西:

      进程和线程都是操作系统的概念。进程是应用程序的执行实例,每个进程是由私有的虚拟地址空间、代码、数据和其它各种系统资源组成,进程在运行过程中创建的资源随着进程的终止而被销毁,所使用的系统资源在进程终止时被释放或关闭。

      线程是进程内部的一个执行单元。系统创建好进程后,实际上就启动执行了该进程的主执行线程,主执行线程以函数地址形式,比如说main函数,将程序的启动点提供给操作系统。主执行线程终止了,进程也就随之终止。

      每一个进程至少有一个主执行线程,它无需由用户去主动创建,是由系统自动创建的。用户根据需要在应用程序中创建其它线程,多个线程并发地运行于同一个进程中。一个进程中的所有线程都在该进程的虚拟地址空间中,共同使用这些虚拟地址空间、全局变量和系统资源,所以线程间的通讯非常方便,多线程技术的应用也较为广泛。

      多线程可以实现并行处理,避免了某项任务长时间占用CPU时间。要说明的一点是,目前部分的计算机是单处理器(CPU)的,为了运行所有这些线程,操作系统为每个独立线程安排一些CPU时间,操作系统以轮换方式向线程提供时间片,这就给人一种假象,好象这些线程都在同时运行。由此可见,如果两个非常活跃的线程为了抢夺对CPU的控制权,在线程切换时会消耗很多的CPU资源,反而会降低系统的性能。这一点在多线程编程时应该注意。

    拥有下述特性的程序可以使用线程:

    • 工作可以被多个任务同时执行,或者数据可以同时被多个任务操作。
    • 阻塞与潜在的长时间I/O等待。
    • 在某些地方使用很多CPU循环而其他地方没有。
    • 对异步事件必须响应。
    • 一些工作比其他的重要(优先级中断)。

    多线程也可以用于串行程序,模拟并行执行。很好例子就是经典的web浏览器,运行在单CPU的电脑上,许多东西可以同时“显示”出来。

    使用线程编程的几种常见模型:

    • 管理者/工作者(Manager/worker):一个单线程,作为管理器将工作分配给其它线程(工作者),典型的,管理器处理所有输入和分配工作给其它任务。至少两种形式的manager/worker模型比较常用:静态worker池和动态worker池。

    • 管道(Pipeline):任务可以被划分为一系列子操作,每一个被串行处理,且是被不同的线程并发处理。汽车装配线可以很好的描述这个模型。比如IDM等下载软件的文件分块同时下载。

    • Peer:和manager/worker模型相似,但是主线程在创建了其它线程后,自己也参与工作。

    接下来看看实现多线程编程的接口pthread。

    一、线程管理

    1.创建和结束线程

    函数

    int pthread_create(pthread_t *tidp,const pthread_attr_t *attr,(void*)(*start_rtn)(void*),void *arg);
    void pthread_exit(void* retval);
    int pthread_attr_init(pthread_attr_t *attr);
    int pthread_attr_destroy(pthread_attr_t *attr);

    创建线程:

      最初,main函数包含了一个缺省的线程。其它线程则需要程序员显式地创建。pthread_create创建一个新线程并使之运行起来。该函数可以在程序的任何地方调用。

    pthread_create的参数与返回值:

        第一个参数为指向线程标识符的指针。不能设置为NULL。

        第二个参数用来设置线程属性。可设置NULL为缺省值。

        第三个参数是线程运行函数的起始地址,即线程将会执行一次的C函数。

        最后一个参数是传递给运行函数的参数。传递时必须转换成指向void的指针类型。没有参数传递时,可设置为NULL。

             若成功则返回0,否则返回出错编号。

      一个进程可以创建的线程最大数量取决于系统实现。

      一旦创建,线程就称为peers,可以创建其它线程。线程之间没有指定的结构和依赖关系。

      有这样一对问答:

        Q:一个线程被创建后,怎么知道操作系统何时调度该线程使之运行?

        A:除非使用了线程的调度机制,否则线程何时何地被执行取决于操作系统的实现。强壮的程序应该不依赖于线程执行的顺序。

      也就是说,多线程程序的运行结果可能是不确定的,因为不知道系统会何时运行该线程。

    线程属性:

      线程具有属性,用pthread_attr_t表示,在对该结构进行处理之前必须进行初始化,在使用后需要对其去除初始化。我们用pthread_attr_init函数对其初始化,用pthread_attr_destroy对其去除初始化。还有其它的一些函数用于查询和设置线程属性结构的指定属性。

      线程属性结构如下:

    typedef struct
    {
        int                   etachstate;      //线程的分离状态
        int                   schedpolicy;     //线程调度策略
        structsched_param     schedparam;      //线程的调度参数
        int                   inheritsched;    //线程的继承性
        int                   scope;           //线程的作用域
        size_t                guardsize;       //线程栈末尾的警戒缓冲区大小
        int                   stackaddr_set;   //线程的栈设置
        void*                 stackaddr;       //线程栈的位置
        size_t                stacksize;       //线程栈的大小
    }pthread_attr_t;

      调用pthread_attr_init之后,pthread_t结构所包含的内容就是操作系统实现支持的线程所有属性的默认值。

        如果要去除对pthread_attr_t结构的初始化,可以调用pthread_attr_destroy函数。如果pthread_attr_init实现时为属性对象分配了动态内存空间,pthread_attr_destroy还会用无效的值初始化属性对象,因此如果经pthread_attr_destroy去除初始化之后的pthread_attr_t结构被pthread_create函数调用,将会导致其返回错误。

      线程属性的其他特性和用法之后再讨论。

    结束终止:

      结束线程的方法有一下几种:

    • 线程从主线程(main函数的初始线程)返回。
    • 线程调用了pthread_exit函数。
    • 其它线程使用 pthread_cancel函数结束线程。
    • 调用exec或者exit函数,整个进程结束。

      pthread_exit用于显式退出线程。典型地,pthread_exit()函数在线程完成工作时或不再需要时候被调用,退出线程。如果main函数在调用了pthread_exit()后将退出,尽管在main中已经没有可执行的代码了,进程和所有线程将保持存活状态,其他线程将会继续执行。否则,它们会随着main的结束而终止。对于正常退出,可以免于调用pthread_exit(),除非你想得到一个返回值。程序员可以可选择地指定终止状态,当任何线程连接(join)该线程时,该状态就返回给连接(join)该线程的线程。pthread_exit()函数并不会关闭文件,任何在线程中打开的文件将会一直处于打开状态,直到线程结束。

     

    现在我们使用多线程来使我们的程序能在sleep期间执行其他操作。

    #include<cstdio>
    #include<pthread.h>
    #include<unistd.h>
    
    void* printhello(void*)
    {
        for(int i=1;i<=4;i++)
        {
            sleep(1);
            printf("hello! %d sec has past
    ",i);
        }
    }
    int main()
    {
        pthread_t tid;
        pthread_create(&tid,NULL,printhello,NULL);
        sleep(5);
        printf("program exited.
    ");
    }

    该程序利用多线程,在main函数sleep的时候还能进行输出。

    如果我们在main中使用pthread_exit()退出

    #include<cstdio>
    #include<pthread.h>
    #include<unistd.h>
    
    void* printhello(void*)
    {
        for(int i=1;i<=4;i++)
        {
            sleep(1);
            printf("hello! %d sec has past
    ",i);
        }
    }
    int main()
    {
        pthread_t tid;
        pthread_create(&tid,NULL,printhello,NULL);
        pthread_exit(NULL);  //退出main的线程
        printf("program exited.
    ");  //这段代码将不被执行
    }

    会发现子线程仍然能继续执行,如果将

        pthread_exit(NULL);  //退出main的线程

    改为

        return 0;

    程序将会直接终止,不输出。

    向线程传递参数

      pthread_create()函数允许程序员向线程的start_rtn函数传递一个参数。当多个参数需要被传递时,可以通过定义一个结构体包含所有要传的参数,然后用pthread_create()传递一个指向改结构体的指针,来打破传递参数的个数的限制。 所有参数都应该传引用传递并转化成(void*)。

      要安全地向一个新创建的线程传递数据应确保所传递的数据是线程安全的(不能被其他线程修改)。

      下面的代码片段演示了如何向一个线程传递一个简单的整数。

    #include<cstdio>
    #include<pthread.h>
    #include<unistd.h>
    
    void* printid(void* id)
    {
        printf("my thread id is %d
    ",*(int*)id);
    
    }
    int main()
    {
        pthread_t tid;
        pthread_create(&tid,NULL,printid,(void*)tid);
        pthread_exit(NULL);//不使用return是因为主函数退出太快以至于子线程没输出就被终止了
    }

      下面的代码片段演示了如何向一个线程传递结构体参数。

    #include<cstdio>
    #include<pthread.h>
    #include<unistd.h>
    
    struct point
    {
        int x,y;
    };
    void* print(void* p)
    {
        printf("I got a point (%d,%d).
    ",((point*)p)->x,((point*)p)->y);
    
    }
    int main()
    {
        pthread_t tid[10];
        point p[10];
        for(int i=0;i<10;i++)
        {
            p[i].x=p[i].y=i;
            pthread_create(&tid[i],NULL,print,(void*)&p[i]);
        }
        pthread_exit(NULL);
    }

      而下面的代码是错误的,循环会使线程访问传递给线程的地址前改变参数的值,输出的结果会有一样的。

    #include<cstdio>
    #include<pthread.h>
    #include<unistd.h>
    
    void* print(void* i)
    {
        printf("i got a number %d
    ",*(int*)i);
    }
    int main()
    {
        pthread_t tid[10];
        for(int i=0;i<10;i++)
        {
            pthread_create(&tid[i],NULL,print,(void*)&i);
        }
        pthread_exit(NULL);
    }

       main函数中调用pthread_exit的时候,进程不会退出,所以main创建的线程就不会退出,但是main的局部变量存储的堆栈会被释放。

    #include<cstdio>
    #include<pthread.h>
    #include<unistd.h>
    
    void* print(void* i)
    {
        printf("i got a number %d
    ",*(int*)i);
        sleep(10);
        printf("i got a number %d
    ",*(int*)i);
    }
    int main()
    {
        pthread_t tid[10];
        for(int i=0;i<10;i++)
        {
            pthread_create(&tid[i],NULL,print,(void*)&i);
        }
        pthread_exit(NULL);
    }

      这段程序的子线程第一次输出和10s后的输出结果是不同的,局部变量会在pthread_exit执行完后被释放。

    2.连接(Joining)和分离(Detaching)线程

    函数:

    int pthread_join (pthread_t tid, void **thread_return);
    int pthread_detach (pthread_t tid);
    int pthread_attr_setdetachstate (pthread_attr_t *attr,int detachstate);
    int pthread_attr_getdetachstate (const pthread_attr_t *attr,int *detachstate);

    连接:

      “连接”是一种在线程间完成同步的方法。例如:

      pthread_join()函数阻塞调用线程直到tid所指定的线程终止。

      代码中如果没有pthread_join()函数,主函数会很快结束从而使整个进程结束,从而使创建的线程没有机会开始执行就结束了。加入pthread_join()函数后,主线程会一直等待直到等待的线程结束自己才结束,使创建的线程有机会执行。

      如果在目标线程中调用pthread_exit(),程序员可以在主线程中获得目标线程的终止状态。连接线程只能用pthread_join()连接一次。若多次调用就会发生逻辑错误。

      还有两种同步方法,互斥量(mutexes)和条件变量(condition variables),稍后讨论。

      线程具有可连接性:

      当一个线程被创建,它有一个属性定义了它是可连接的(joinable)还是分离的(detached)。只有是可连接的线程才能被连接(joined),若果创建的线程是分离的,则不能连接。

      使用pthread_create()的attr参数可以显式的创建可连接或分离的线程,典型四步如下:

    • 声明一个pthread_attr_t数据类型的线程属性变量
    • 用pthread_attr_init()初始化改属性变量
    • 用pthread_attr_setdetachstate()设置可分离状态属性
    • 之后,用pthread_attr_destroy()释放属性所占用的库资源

    分离

      pthread_detach()可以显式用于分离线程,尽管创建时是可连接的。

      创建一个线程默认的状态是joinable, 如果一个线程结束运行但没有被join,则它的状态类似于进程中的Zombie Process,即还有一部分资源没有被回收(退出状态码),所以创建线程者应该使用pthread_join()来等待线程运行结束,并可得到线程的退出代码,回收其资源(类似于wait,waitpid)。但是调用pthread_join()后,如果该线程没有运行结束,调用者会被阻塞,在有些情况下我们并不希望如此,比如在Web服务器中当主线程为每个新来的链接创建一个子线程进行处理的时候,主线程并不希望因为调用pthread_join而阻塞(因为还要继续处理之后到来的链接),这时可以在子线程中加入代码
      
    pthread_detach(pthread_self());

    或在主线程中调用

    pthread_detach(thread_id);//非阻塞,可立即返回

      这将使该子线程的状态设置为detached,则该线程运行结束后会自动释放所有资源。

      建议:

    • 若线程需要连接,考虑创建时将线程显式设置为可连接的。因为并非所有创建线程的实现都是默认将线程创建为可连接的。
    • 若事先知道线程从不需要连接,考虑创建线程时将其设置为可分离状态。

      下面的代码演示了使用pthread_join()

    #include <pthread.h>
    #include <cstdio>
    #include <unistd.h>
    void* print1 (void*)
    {
        printf("x");
    }
    void* print2 (void*)
    {
        printf("y");
    }
    int main ()
    {
        pthread_t tid1,tid2;
        pthread_attr_t attr;
        pthread_attr_init(&attr);
        pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_JOINABLE);
        pthread_create(&tid1,&attr,&print1,NULL);
        pthread_create(&tid2,&attr,&print2,NULL);
        pthread_join(tid1,NULL);
        pthread_join(tid2,NULL);
        return 0;
    }

       输出结果是不能确定的,pthread_join阻塞的是调用join函数的所在main线程,其目的是等待并确认t1、t2线程执行结束,但它无法保证多线程的先后执行顺序。它只确定被join的线程已经结束”,和线程执行顺序没关系,类似于进程的waitpid函数。能控制线程执行顺序的是线程的Mutex和Condition Variables。

      每个进程创建以后都应该调用pthread_join或pthread_detach函数,只有这样在线程结束的时候资源(线程的描述信息和stack)才能被释放。在新线程里面没有调用pthread_join或pthread_detach会导致内存泄漏, 如果你创建的线程越多,你的内存利用率就会越高, 直到你再无法创建线程,最终只能结束进程。

      解决方法有三个:
    • 线程里面调用 pthread_detach(pthread_self()) 这个方法最简单
    • 在创建线程的设置PTHREAD_CREATE_DETACHED属性
    • 创建线程后用 pthread_join() 一直等待子线程结束。
    #include<pthread.h>
    #include<cstdio>
    #include<unistd.h>
    void* print(void *)
    {
        static int g=0;
        printf("%d
    ", g++);
        pthread_exit(0);
    }
    
    int main()
    {
        pthread_t tid;
        while(1)
        {
           pthread_create(&tid,NULL,print,NULL);
           pthread_detach(tid);
        }
        return 0;
    }

    还剩下一些内容以后再写......

  • 相关阅读:
    为初学者解释下命名空间
    面向对象的思想
    SELECT查询结果集INSERT到数据表
    SQL Server事务
    Sql Server中的谓词和运算符
    SQL查询语句执行的逻辑顺序
    浏览器中的流
    CSS盒子模型
    ArcGIS提取水系并进行生态敏感性分析
    ENVI提取水系并进行生态敏感性分析
  • 原文地址:https://www.cnblogs.com/CodeMIRACLE/p/5491583.html
Copyright © 2011-2022 走看看