第十一章 线程(一)

来源:互联网 发布:.net core java 编辑:程序博客网 时间:2024/04/28 04:07
这里首先提一下,并发和并行有什么区别:
    并行是指在同一时刻,有多条指令在多个处理器上同时执行。
    并发是指在同一时刻,只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。




线程的概念:
    典型的UNIX进程可以看成是一个线程,通过使用线程可以把进程设计成在某一时刻能够不止做一件事。

        1、通过为每种事件类型分配单独的处理线程,可以简化处理异步事件的代码。每个线程在进行事件处理时可以采用同步编程模式,同步编程模式要比异步编程模式简单多。
        2、多个进程必须使用操作系统提供的复杂机制才能实现内存和文件描述父的共享(以后会涉及)。而多个线程自动的可以访问相同的存储地址空间和文件描述符。
        3、有多个线程控制时,若多个处理线程不互相依赖,则可以为每个任务分配一个单独线程,从而实现交叉执行。
        4、交互的程序同样可以使用线程来改善响应时间,多线程可以把处理用户输入输出的部分与其他部分分开。

    是否需要创建多个线程取决于各种因素。在以下情况下,最适合采用多线程处理:
        (1)耗时或大量占用处理器的任务阻塞用户界面操作;
        (2)各个任务必须等待外部资源 (如远程文件或 Internet连接)。
    
    当然也有缺点:

        (1)等候使用共享资源时造成程序的运行速度变慢。这些共享资源主要是独占性的资源 ,如打印机等。    
        (2)对线程进行管理要求额外的 CPU开销。线程的使用会给系统带来上下文切换的额外负担。当这种负担超过一定程度时,多线程的特点主要表现在其缺点上,比如用独立的线程来更新数组内每个元素。
        (3)线程的死锁。即较长时间的等待或资源竞争以及死锁等多线程症状。
        (4)对公有变量的同时读或写。当多个线程需要对公有变量进行写操作时,后一个线程往往会修改掉前一个线程存放的数据,从而使前一个线程的参数被修改;另外 ,当公用变量的读写操作是非原子性时,在不同的机器上,中断时间的不确定性,会导致数据在一个线程内的操作产生错误,从而产生莫名其妙的错误,而这种错误是程序员无法预知的。

    每个线程包含有表示执行环境所必须的信息,其中包括进程中标识线程的线程ID,一组寄存器值,栈,调度优先级和策略,信号屏蔽字,errno变量以及线程私有数据。

    一个进程的所有信息对该进程所有的线程都是共享的,包括可执行程序的代码,程序的全局内存和堆内存,栈以及文件描述符。





进程与线程之间的区别:
    进程与线程有根本上的区别。每个进程有其独立的数据空间、文件描述符以及进程的ID。
               而线程共享一个数据空间,文件描述符以及进程的ID。
           一、共享数据空间
                这里考虑一个在存储器中存储了巨大而复杂的树结构数据库的数据库系统。多个线程可以轻易的读取到这个共享的数据集。客户的多个查询也可以由一个进程实现。如果变量不会被改变,共享这个数据空间不会导致任何问题。
                再考虑用到malloc和free的系统调用来管理内存的例子。一个线程分配了一块内存用于存放字符串,当此线程做其他事时,另一个线程使用free释放这块内存了,更糟糕的是,这块内存已经被指向其他用途了。。。
                在单线程环境中返回指向静态局部变量的指针的函数无法兼容多线程环境。因为同样的函数可能在多个线程中调用而导致出错
   
            二、共享文件描述符
                在fork原语调用后,文件描述符被自动复制,从而子进程得到新的文件描述符,在子进程关闭某个从父进程继承来的描述符后,此描述符对于父进程依旧是打开的。
                在多线程中,很可能统一个文件描述符被传给多个线程。显然如果一个线程中的函数关闭了这个文件,此文件描述符对此进程中任何线程来说都已经是关闭的,即使还有线程需要使用它。

            三、fork, exec, exit, signals
                所有线程共享同一个进程。如果一个线程调用了 exec,系统内核用一个新的程序取代当前程序从而所有正在运行的线程都会消失。 并且如果一个线程执行了exit, 那么整个进程都会结束。因此,要是某个线程导致了内存段异常或线程崩溃,瘫痪的是整个进程,而不是单单某个线程了。
                如果线程中某个函数调用了fork,那么只有调用fork的线程在新的进程中运行。



    


线程标识:
    每个线程也拥有一个线程ID。 进程ID在系统中是唯一的,但线程不同,线程ID只在它所属的进程上下文中才有意义
    线程ID是用 pthread_t来表示的,在可移植的操作系统实现不能把它作为整数处理,
        比较两个线程ID必须使用下面的函数:
            int pthread_equal(pthread_t tid1, pthread_t tid2);
                相等则返回非0; 否则返回0;
        获取自身的线程ID
            pthread_t pthread_self(viod);


线程创建:
    程序开始运行时,它是以单进程中的单个控制线程启动的。
        创建新的线程:
            int pthread_create(pthread_t *tidp, pthread_attr_t attr, void*(*start)(void *),void *)
        线程创建后可以访问进程的地址空间,并且继承调用线程的浮点环境和信号屏蔽字。


线程终止:
    若进程中任一线程调用了exit之类,那么整个进程就会终止。
    单个线程可以通过3种方式退出,因此可以在不终止整个进程的情况下,停止它的控制流。
        1、线程可以简单的从启动例程中返回,返回值是线程的退出码。
        2、线程可以被同一进程中的其他线程取消。
        3、线程调用 pthread_exit

            函数    pthread_exit(void *rval)
                与传给pthrad_create的单个参数类似
            进程中其他线程可以使用 pthread_join 函数访问到这个 pthread_exit中的参数。

              函数  pthread_join(pthread_t thread, void **rval_ptr)
            调用 pthread_join后线程将一直阻塞,直到指定的线程调用pthread_exit、从启动例程中返回或者被取消。
                若线程简单的从启动例程返回, rval_ptr即包含返回码;若是被取消的,则被设置为 PTHREAD_CANCELED
#include <stdio.h>#include <stdlib.h>#include <pthread.h>void *fun1(void *arg){        printf("thread 1 returning\n");        return ((void*)1);                    //普通的例程返回}void *fun2(void *arg){        printf("thread 2 exiting\n");        pthread_exit((void*)2);                //调用 pthread_exit函数退出}int main(){        pthread_t p1, p2;        void *tret;        if(pthread_create(&p1,NULL,fun1,NULL) != 0)        {                perror("pthread_create");                exit(1);        }        if(pthread_create(&p2,NULL,fun2,NULL) != 0)        {                perror("pthread_create");                exit(1);        }        if(pthread_join(p1,&tret) != 0)                //可以通过ptherad_join来得到退出码        {                perror("join ");                exit(2);        }        printf("thread 1 exit code : %ld\n",(long)tret);        if(pthread_join(p2,&tret) != 0)        {                perror("join ");                exit(2);        }        printf("thread 2 exit code : %ld\n",(long)tret);        return 0;}



运行结果:
$ ./PJthread 1 returningthread 2 exitingthread 1 exit code : 1thread 2 exit code : 2



    这个返回的无类型指针参数可以传递的值不止一个,可以传递包含复杂信息的结构的地址。但是要注意,一个线程的结束后,其栈上的内存可能被另做它用,不可以随随便便作为返回值(常识)

    线程可以通过 pthread_cancel(id)
        来请求取消同一进程中的其他线程, 注意该函数并不等待线程终止,仅仅提出请求

    线程可以建立多个清理处理函数,与进程在退出时用atexit函数类似,在退出时调用某些函数。
        执行顺序与它们的注册时间相反。
        函数    void pthread_cleanup_push(void (*rtn)(void *), void *arg);
                    void pthread_cleanup_pop(int execute)
;
    只有当线程执行以下几个动作的时候,清理函数由 pthread_cleanup_push调度,参数仍旧只有一个。
        1、调用 pthread_exit 的时候
        2、响应取消请求(pthread_cancel)的时候
        3、用非0的execute参数调用pthread_cleanup_pop的时候
            若execute参数为0, 某清理函数将不被调用(即删除上一次调用pthread_cleanup_push所建立的清理程序)

#include <stdio.h>#include <stdlib.h>#include <pthread.h>void cleanup(void *s){        printf("cleanup : %s\n",(char*)s);}void* fun1(void *arg ){        printf("thread 1 start\n");        pthread_cleanup_push(cleanup, "thred 1's 1st handler");        pthread_cleanup_push(cleanup, "thred 1's 2ed handler");        printf("thread 1 push complete\n");        if(arg)                return ((void *)1);                    //这里简单的启动例程的返回,因此不会导致清理函数的调用        pthread_cleanup_pop(0);                        //this excute value here         pthread_cleanup_pop(0);            return ((void *)1);                    }void *fun2(void *arg ){        printf("thread 2 start\n");        pthread_cleanup_push(cleanup, "thred 2's 1st handler");        pthread_cleanup_push(cleanup, "thred 2's 2ed handler");        printf("thread 2 push complete\n");        if(arg)                pthread_exit((void *)2);        pthread_cleanup_pop(0);        pthread_cleanup_pop(0);        pthread_exit ((void *)2);                        //这里调用pthread_exit}int main(){        pthread_t p1,p2;        void *tret;        if(pthread_create(&p1,NULL,fun1,(void*)1) != 0)                exit(1);        if(pthread_create(&p2,NULL,fun2,(void*)2) != 0)                exit(1);        if(pthread_join(p1,&tret) != 0)                exit(1);        printf("thread 1 return code : %ld\n",(long)tret);        if(pthread_join(p2,&tret) != 0)                exit(1);        printf("thread 2 return code : %ld\n",(long)tret);        return 0;}



结果为:
thread 2 startthread 2 push completethread 1 startthread 1 push completethread 1 return code : 1cleanup : thred 2's 2ed handlercleanup : thred 2's 1st handlerthread 2 return code : 2



    正如预料的,符合上面提到的3种情况我们就会执行清理函数,且按照注册顺序相反的顺序执行清理函数

    The excute arg in pthread_chleanup_pop() must be 0. We have known that if the excute is 0, clean functions won't be called.

     A better example:

void child(void *t){pthread_cleanup_push(pthread_mutex_unlock,&mutex);pthread_mutex_lock(&mutex);..............pthread_mutex_unlock(&mutex);pthread_cleanup_pop(0);}

    If this program calls pthread_exit or is canceled by other threads after it has gotten the lock, something must go wrong. So we use pthread_cleanup_push/pop to cover these codes.  After it gets the lock, though it might be canceled or exits, it will still calll unlock function to release the lock.

    the excute value must be 0 to make sure that if the lock is released successfully, we do not unlock again.

0 0
原创粉丝点击