多线程编程之系统编程

来源:互联网 发布:聚合物水泥防水涂料js 编辑:程序博客网 时间:2024/05/16 02:02

一,线程的基本概念

1,线程的定义:

线程也被称为轻量进程(LWP)计算机科学术语,指运行中的程序的调度单位。

同一进程的多个线程共享同一地址空间。一般,线程具有就绪、阻塞和运行三种基本状态。

各线程还共享以下进程资源和环境:

  1. 文件描述符表
  2. 每种信号的处理方式(SIG_IGN、SIG_DFL或者自定义的信号处理函数)
  3. 当前工作目录
  4. 用户id和组id

但有些资源是每个线程各有一份的(独占):

  1. 线程id
  2. 上下文,包括各种寄存器的值、程序计数器和栈指针
  3. 栈空间
  4. errno变量
  5. 信号屏蔽字
  6. 调度优先级

2,线程与进程的对比

1>进程定义
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈,非共享),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.

形象比喻:

进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。一个车间里,可以有很多工人。他们协同完成一个任务。线程就好比车间里的工人。一个进程可以包括多个线程。车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。可是,每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫”互斥锁“(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。还有些房间,可以同时容纳n个人,比如厨房。也就是说,如果人数大于n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。这时的解决方法,就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做”信号量“(Semaphore),用来保证多个线程不会互相冲突。不难看出,mutex是semaphore的一种特殊情况(n=1时)。也就是说,完全可以用后者替代前者。但是,因为mutex较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。

2>关系
一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行。

相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。

3>区别
1) 包含关系:一个程序至少有一个进程,一个进程至少有一个线程.
2).地址空间和内存:进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。 进程在执行过程中拥有独立的内存单元(一个进程崩溃后,在保护模式下不会对其它进程产生影响);而多个线程共享进程提供的内存(拥有自己的私有栈空间只是作为运行需要的极少内存),从而极大地提高了程序的运行效率,但一个线程死掉就等于整个进程死掉。所以多进程的程序要比多线程的程序健壮。
3).通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
4).调度和切换:线程上下文切换比进程上下文切换要快得多。
5) 执行过程:进程独立执行;线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
6) 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

4>优缺点

多线程的优点:

无需跨进程边界; 程序逻辑和控制方式简单; 所有线程可以直接共享内存和变量等; 线程方式消耗的总资源比进程方式好;

多线程缺点:

每个线程与主程序共用地址空间,受限于2GB地址空间; 线程之间的同步和加锁控制比较麻烦; 一个线程的崩溃可能影响到整个程序的稳定性; 到达一定的线程数程度后,即使再增加CPU也无法提高性能; 线程能够提高的总性能有限,而且线程多了之后,线程本身的调度也是一个麻烦事儿,需要消耗较多的CPU

多进程优点:

每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系; 通过增加CPU,就可以容易扩充性能; 可以尽量减少线程加锁/解锁的影响,极大提高性能,就算是线程运行的模块算法效率低也没关系; 每个子进程都有2GB地址空间和相关资源,总体能够达到的性能上限非常大

多线程缺点

逻辑控制复杂,需要和主程序交互; 需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算多进程调度开销比较大; 最好是多进程和多线程结合,即根据实际的需要,每个CPU开启一个子进程,这个子进程开启多线程可以为若干同类型的数据进行处理。

进程的优点:

1)顺序程序的特点:具有封闭性和可再现性;
2)程序的并发执行和资源共享。多道程序设计出现后,实现了程序的并发执行和资源共享,提高了系统的效率和系统的资源利用率。

进程的缺点:

操作系统调度切换多个线程要比切换调度进程在速度上快的多。而且进程间内存无法共享,通讯也比较麻烦。

线程之间由于共享进程内存空间,所以交换数据非常方便;在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。

线程的优点:

1)它是一种非常”节俭”的多任务操作方式。在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种”昂贵”的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。当然,在具体的系统上,这个数据可能会有较大的区别;
2)线程间方便的通信机制,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便;
3)使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上;
4)改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

线程的缺点:

1.调度时, 要保存线程状态,频繁调度, 需要占用大量的机时;
2.程序设计上容易出错(线程同步问题)。

5). 多线程与单线程的区别

生活举例:
你早上上班,正要打卡的时候,手机响了。。你如果先接了电话,等接完了,在打卡,就是单线程。
如果你一手接电话,一手打卡。就是多线程。
2件事的结果是一样的。。你接了电话且打了卡。

3,多线程

这里写图片描述

多线程与信号处理函数对比:

多线程比信号处理函数更加灵活,信号处理函数的控制流程只是在信号递达时产生,在处理完信号之后就结束,而多线程的控制流程可以长期并存,操作系统会在各线程之间调度和切换,就像在 多个进程之间调度和切换一样。

注意:
   在Linux上线程函数位于libpthread共享库中,因此在编译时要加上 -lpthread 选项。

二,线程控制

1,线程创建

函数原型:

#include <pthread.h>int pthread_create(pthread_t *thread , const pthread_attr_t *attr , void *(*start_routine) (void *) ,  void *arg);

返回值:成功返回0,错误返回错误号
第一个参数thread:线程id(输出型参数)
第二个参数attr:线程属性,一般设置为NULL(表示线程属性取缺省值)
第三个参数start_routine:函数指针,指向新线程即将执行的代码
第四个参数arg:这个指针按什么类型解释由调用者自己定义,一般设置为NULL

在一个线程中调用pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行,而新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决定。

2,终止线程

1>如果需要只终止某个线程而不终止整个进程,可以有三种方法:
1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
2. 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。
3. 线程可以调用pthread_exit终止自己。

2>终止线程或执行流:

#include <pthread.h>void pthread_exit(void *retval);

retval是 void * 类型,和线程函数返回值的用法一样,其它线程可以调用pthread_join(稍后介绍)获得这个指针。

需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

3>取消线程

#include <pthread.h>int pthread_cancel(pthread_t thread);

线程是允许被取消的,退出结果为-1,终止同一进程中的另一个线程。线程自己取消自己(不推荐)退出结果为0。

3,线程等待

1>为什么要等待线程?

main函数执行的线程称为主线程,多线程的执行顺序由线程调度决定
主线程必须回收其他线程,否则就会产生类似僵尸进程的状况(内存泄漏)。
结论:线程必须被等待

2>获取当前线程的线程tid:pthread_self

 #include <pthread.h> pthread_t pthread_self(void);

注意:
   仅仅在当前进程内部有效,作为线程的唯一标识符。

3>线程等待函数:

#include <pthread.h>int pthread_join(pthread_t thread, void **value_ptr);

返回值:成功返回0,失败返回错误码。
参数thread:线程号,即要等待线程的tid
参数retval:要等待线程的退出码(输出型参数)

调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:

如果对thread线程的终止状态不感兴趣,可以传NULL给value_ptr参数。

  1. 如果thread线程通过return返回,value_ptr所指向的单元里存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调用pthread_cancel异常掉,value_ptr所指向的单元里存放的是常数PTHREAD_CANCELED。
  3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。

4>多线程的进行执行

多线程的进行执行时,只要有一个线程出错,整个进程就会挂掉(操作系统会向其发信号回收资源,其他线程都跟着退出)。线程运行时,线程只能正常的运行完,退出码表明了其运行状态

线程等待只有一种方式:阻塞式等待

4.上述函数举例:

pthread.c

#include<stdio.h>#include<pthread.h>#include<unistd.h>void* thread1(void* val1){    printf("thread1 is returning\n");    printf("%s:pid is %d,tid is %u\n",(char*)val1, getpid(),pthread_self());    return (void*)0;//线程终止方式1,用return返回}void* thread2(void* val2){    printf("thread2 exiting\n");    printf("%s:pid is %d,tid is %u\n",(char*)val2,getpid(),pthread_self());    pthread_exit((void*)2);//线程终止方式2,用pthread_exit退出}void* thread3(void* val3){    printf("%s:pid is %d,tid is %u\n",(char*)val3,getpid(),pthread_self());    while(1)    {        printf("thread3 is running,waiting for be canceled\n");//线程终止方式3,被其他线程cancel        sleep(1);    }}int main(){    pthread_t tid1;    pthread_t tid2;    pthread_t tid3;    void* ret;    pthread_create(&tid1,NULL,thread1,"thread1");    pthread_join(tid1,&ret);    printf("thread1 return ,return code is %d\n",(int)ret);    pthread_create(&tid2,NULL,thread2,"thread2");    pthread_join(tid2,&ret);    printf("thread2 exit,exit code is %d\n",(int)ret);    pthread_create(&tid3,NULL,thread3,"thread3");//线程3创建    sleep(3);    pthread_cancel(tid3);//线程3的终止方式,被其他线程用thread_cancel取消    pthread_join(tid3,&ret);//wait thread3    printf("thread3 cancel,cancel code is %d\n",(int)ret);    printf("main thread run:pid is %d,tid is %u\n",getpid(),pthread_self());    return 0;}

makefile:

pthread:pthread.c    gcc -o pthread pthread.c -lpthread.PHONY:cleanclean:    rm -f pthread

运行结果:

这里写图片描述

思考:
    为什么3个线程的pid和tid都是相同的呢?原因就是pid实际上是主线程的进程号,同为1514,那么既然是不同的线程,又因为线程号是确定一个线程的标识,为什么3个线程号仍然相同呢?
    回顾代码我们是验证3种线程终止的方式,而且我们在每个线程终止后就对其进行了等待,调用过线程等待函数之后主线程就会回收其资源,其中当然也包括线程id,当线程1回收之后,我们紧接着创建了线程2,系统会优先选择刚刚回收的1号线程id来分配给线程2,当线程2终止等待之后,此线程号再次被回收利用,进而分配给线程3,这就是为什么我们看到不同的3个线程拥有同一个tid了。

5.线程的两种属性

线程的两种属性:可结合性和可分离性。

在任何一个时间点上,线程是可结合的(joinable)或者是分离的(detached)。一个可结合的线程能够被其他线程收回其资源和杀死。在被其他线程回收之前,它的存储器资源(例如栈)是不释放的。相反,一个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统自动释放。

默认情况下,线程被创建成可结合的。
为了避免存储器泄漏,每个可结合线程都应该要么被显示地回收,即调用pthread_join;要么通过调用pthread_detach函数被分离。

由于调用pthread_join后,如果该线程没有运行结束,调用者会被阻塞,在有些情况下我们并不希望如此。改善方法:

1>可以在子线程中加入代码:

pthread_detach(pthread_self())

2>父线程调用:

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

这将该子线程的状态设置为分离的(detached),这样一来,该线程运行结束后会自动释放所有资源。

线程可以主动分离,也可以被分离。
一个进程设置为分离线程,则主线程不需要对其等待,待其运行完系统会自动回收。

原创粉丝点击