当前位置: 首页 > news >正文

Linux的多任务编程-线程

Linux的多任务编程-线程

1.线程的基本概念

Linux操作系统很早就具备这些多进程功能了.但有时人们认为用fork()来创建一个新进程的代价还是太大,如在Web服务器中,通常采取的多进程方案是一旦接收到访问请求后,即创建一个新的进程,由该进程执行任务,当任务执行完毕后,该进程退出.如果有大量的请求在很短的时间中频繁的访问该服务器,那么服务器耗费在创建进程,销毁进程中的机时便十分可观.如果能用线程来完成这个工作,则情况会好很多.

线程的出现使得程序有了多个控制流程,能够在单进程的环境中执行多个任务.有了线程之后,一个进程就能够做不止一件事情.这使得程序复杂化了,但是这样做有很多优点:

  • 同一进程内的线程共享该进程的资源,线程创建时无需复制这些资源,因此大大减少了上下文切换的开销;
  • 由于地址空间的共享,使得内存和文件描述符的共享比多进程环境下要容易实现;
  • 通过为每种事务的类型分配单独的处理线程,能够简化处理复杂事件的代码.
  • 在交互式程序中使用多线程能很好的改善操作的响应时间,有更好的交互友好性.
就像每个进程有一个PID一样,每个线程有一个线程ID.但线程ID只是在它所属的进程环境中有效,因此在不同的进程中可能会找到两个具有相同线程ID的线程.线程ID通过数据类型pthread_t来表示.这个类型通常采用结构体来实现,因此在使用时与PID有较大的不同.例如必须使用下面的函数来比较两个线程ID.
#include <pthread.h>
int pthread_equal (pthread_t tid1, pthread_t tid2); 
与getpid函数类似,在线程中获取自身的线程ID的函数是:
#include <pthread.h>
pthread_t pthread_self (void); 
上面给出的这两个函数都有一个前缀"pthread_".事实上,与线程有关的函数库构成了一个完整的系列,绝大多数函数的名字都是以"pthread_"打头的.为了使用这些函数库,首先必须包含头文件<pthread.h>,其次在链接这些线程函数库时要使用编译器命令的"-pthread"选项.

2.线程的创建

创建一个新的线程可以通过调用pthread_create()函数来创建.该函数的原型是:
#include <pthread.h>
int pthread_create (pthread_t *thread, pthread_attr_t *attr, void *(*start_rtn)(void), void *restrict arg); 
pthread_create()函数的第二个参数(pthread_attr_t *attr)表示线程的属性.如果该值设为NULL,就是采用默认属性,线程的多项属性都是可以更改的.这些属性主要包括绑定属性,离属性,堆栈地址,堆栈大小以及优先级.其中系统默认的属性为非绑定,非分离,缺省1M的堆栈以及与父进程同样级别的优先级.
最后的两个参数分别是线程将要启动执行的函数以及将要传递给这个函数的参数.如果传递的参数不止一个,那么需要把这些参数放在一个结构体中,然后把这个结构体的地址作为arg参数传入.
下面是一个简单的例子.
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

void print_ids (const char *s)
{
    pid_t       pid;
    pthread_t   tid;
    pid = getpid();
    tid = pthread_self();
    
    printf("%s pid %u tid %u (0x%x)\n", s,\
        (unsigned int)pid,(unsigned int)tid,\
        (unsigned int)tid);
}

void *thread_func(void *arg)
{
    print_ids("new thread: ");
    printf("thread param is :%d\n",*(int *)arg);
    return NULL;
}

int main(void)
{

    int err;
    pthread_t newtid;
    int myarg;
    myarg=1;
    err = pthread_create (&newtid, NULL, thread_func, &myarg);
    if (err != 0)
    {
        perror("create thread error");
        exit(1);
    }
    print_ids("main thread:");
    sleep(1);
    exit(0);
}
可以看到新创建的线程和主进程拥有相同的线程ID(PID均为1785),而线程ID显示不同的.

3.线程的终止

单个线程可以通过三种方式退出执行:

  • 从启动例程中返回;
  • 在线程内调用pthread_exit()函数;
  • 被同一进程中的其他线程取消.
与在进程中使用exit和_exit函数退出类似,使用pthread_exit函数退出线程将返回一个退出状态码,并将保存对对线程调用pthread_join函数(类似于waitpid函数).这两个函数的原型如下:
#include <pthread.h>
void pthread_exit (void *rval_ptr);
int pthread_join (pthread_t thread , void **rval_ptr);

参数rval_ptr是一个无类型的指针,如果线程只是从它的启动例程返回,rval_ptr将包含返回码,如果线程被取消(在返回前被中止),由rval_ptr指定的内存单元就置为PTHREAD_CANCELLED.
pthread_join函数的第一个参数指定了将要等待的线程ID,它就是pthread_creat函数返回的标识符.这个函数在成功时返回0,失败时返回一个错误代码,与pthread_creat函数类似.下面是一个简单的例子,用来熟悉函数的用法.

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

void *thread1(void *arg)
{
    printf("thread1 returning\n");
    return ((void *)1);
}

void *thread2(void *arg)
{
    printf("thread2 exiting\n");
    pthread_exit ((void *)2);
}

int main(void)
{
    int err;
    pthread_t tid1,tid2;
    void *tret;

    err = pthread_create (&tid1, NULL, thread1, NULL);
    if (err != 0)
    {
        perror("create thread1 error");
        exit(1);
    }
    err = pthread_create (&tid2, NULL, thread2, NULL);
    if (err != 0)
    {
        perror("create thread2 error");
        exit(1);
    }
    err = pthread_join(tid1, &tret);
    if (err != 0)
    {
        perror("join thread1 error");
        exit(1);
    }
    printf("The exit code of thread1 :%d\n",(int)tret);
    err = pthread_join(tid2, &tret);
    if (err != 0)
    {
        perror("join thread2 error");
        exit(1);
    }
    printf("The exit code of thread2 :%d\n",(int)tret);
    exit(0);
}
从运行结果可以看出,当一个线程通过pthread_exit函数退出或者简单地使用return从启动例程中返回,进程中的其他线程可以通过pthread_join函数获得该线程的退出状态.需要注意的是参数rval_ptr所使用的内存在函数调用完后应该仍能是有效的,否则会出现无效或者非法内存访问错误.
线程还可以通过pthread_cancel函数来请求同一进程中的其他线程,该函数的原型如下:
#include <pthread.h>
int pthread_calcel ( pthread_ t tid);
该函数并不像pthread_exit函数那样,强迫线程退出,而只是提出一个要求,至于被要求的线程,既可以接受请求退出执行,也可以忽略该请求继续执行.这个问题涉及到线程属性的设置,在后面我们会详细讲解.
线程和进程的函数有很多相似之处,下图总结了这些相似的函数.


到目前为止,我们所讨论的线程都是一种被称为"非分离状态"的线程.所谓"非分离线程",就是线程的一种属性,用来决定线程与什么杨的方式终止自己."非分离状态"的线程结束时,它所占用的系统资源并没有被释放,只有当pthread_join函数返回时,创建的线程才能释放自己占用的系统资源;而"分离状态"的线程,在线程结束时则立即释放所占用的系统资源.
下面我们来详细线程的属性设置.

4.线程的基本属性


对大多数程序来说,使用默认的属性就可以了,但有的属性还是十分有用的,所以有必要了解一些线程的这些属性的意义.
分离(Detachedstate)属性:如果不需要理解线程的终止状态,则可以修改这个属性为PTHREAD_CREATE_DETACHED,让线程以分离状态启动,当线程退出时收回它所占有的资源.如果没有同步措施,一个刚创建的分析线程有可能在pthread_creat函数返回之前就终止了,而它的线程标识符则被另一个新线程所使用.默认的PTHREAD_CREATE_JOIN表示非分离状态,可以用pthread_join函数来检查线程的退出状态.
绑定(Scope)属性:linux中采用"一对一"的线程机制,也就是一个用户线程对应一个内核线程.默认情况下,启动多少内核线程,哪些内核线程是由系统控制的,这种状况即非绑定的.而绑定状况是指一个用户线程固定的分配给一个内核线程,被绑定的线程具有较高的响应速度.通过设置绑定属性可以使得绑定的线程满足诸如实时之类的要求.
堆栈大小(Stack Size)属性:该属性定义了线程的堆栈大小,在大多数情况下,默认值是最合适的.
调度策略(Schedpolicy)属性:这个属性控制着由POSIX标准定义的调度策略,包括SCHED_FIFO(先入先出),SCHED_RR(循环),SCHED_OTHER(默认调度策略).
并发度(Concurrency)属性:并发度控制着用户线程可以映射到的内核线程的数目.在一个进程中,可能需要多个非绑定的线程被同时激活,默认情况下,线程库保证有足够多的线程处在激活状态下,以确保进程的运行.尽管这样可以节省系统资源,但那并不一定能达到最有效的并发性.

线程的属性大多通过pthread_attr_t结构来描述,通过修改pthread_attr_t结构来修改想成的默认属性.设置线程属性需要用到一对函数,它们是:
#include <pthread.h>
int pthread_attr_init (pthread_attr_t *attr);
int pthread_attr_destroy (pthread_attr_t *attr);
使用pthread_attr_init函数初始化pthread_attr_t结构,之后pthread_attr_t结构中所包含的内容就是OS实现支持线程的所有属性的默认值,如果要修改其中个别属性的值,则需要调用其他的函数.
如果要去除对pthread_attr_t结构的初始化,可以调用pthread_attr_destroy函数,该函数将释放该空间的内存结构,同时用无效的值初始化该属性对象,因此如果该属性对象被无用,将会导致pthread_create函数返回错误.
不要尝试直接操作pthread_attr_t结构的内部,由于考虑了线程的可移植性,该结构在不同的系统中实现有所差别,因此为了增强程序的可移植性,应尽量采用标准可函数提供的属性修改参数.
修改线程所使用的函数都是成对出现的,带有"get"前缀的函数都是获取某属性的当前值,而带有"set"前缀的函数则是设置该属性的值.下面我们列出部分函数.

4.1分离属性

分离属性是用来决定一个线程以什么样的方式来终止自己 .修改分离属性的函数的函数原型如下:
#include <pthread.h>
int pthread_attr_getdetachstate ( pthread_attr_t *attr, int *detachstate );
int pthread_attr_setdetachstate ( pthread_attr_t *attr, int detachstate );
返回值:函数成功返回0.任何其他值表示错误.参数detachstate的值为:PTHREAD_CREATE_DETACHED,PTHREAD_CREATE_JOINABLE.

4.2绑定属性

绑定属性就是指一个用户线程固定地分配给一个内核线程,因为CPU时间片的调度是面向内核线程(也就是轻量级进程)的,因此具有绑定属性的线程可以保证在需要的时候总有一个内核线程与之对应.而与之对应的非绑定属性就是指用户线程和内核线程的关系不是始终固定的,而是由系统来控制分配的.修改绑定属性的函数的函数原型如下:
#include <pthread.h>
int pthread_attr_getscope( pthread_attr_t *tattr, int *scope );
int pthread_attr_setscope( pthread_attr_t *tattr, int scope );
返回值:函数成功返回0.任何其他值表示错误.PTHREAD_SCOPE_SYSTEM指定将来创建的线程是绑定的,PTHREAD_SCOPE_PROCESS指定将来创建的线程是非绑定的.在一个进程中可以有这两种类型的线程.

4.3堆栈属性

修改堆栈属性的函数的函数原型如下:
#include <pthread.h>
int pthread_attr_getstacksize( pthread_attr_t *tattr, size_t *size );
int pthread_attr_setstacksize( pthread_attr_t *tattr, int size );
返回值:函数成功返回0.任何其他值表示错误.参数size表示线程所需堆栈的大小.如果size的值为0,则使用默认大小.在大多数情况下,默认值是最合适的.堆栈的大小不能小于系统定义的最小堆栈容量.

4.4调度策略属性

修改调度策略属性的函数的函数原型如下:
#include <pthread.h>
int pthread_attr_getschedpolicy( pthread_attr_t *tattr, int *policy );
int pthread_attr_setschedpolicy( pthread_attr_t *tattr, int policy );
返回值:函数成功返回0.任何其他值表示错误.参数policy的取值有SCHED_FIFO,SCHED_RR,SCHED_OTHER.

4.5发度属性

修改并发度属性的函数的函数原型如下:
#include <pthread.h>
int pthread_getconcurrency( void );
int pthread_setconcurrency( int new_level );
返回值:函数成功返回0.任何其他值表示错误.该函数允许一个应用程序通知线程库它所需的并发级别(即同一时刻可以有多少个非绑定线程处于激活状态下).但只是通知系统它所需要的并发级别,系统将它作为一个提示,而不是命令.如果参数new_level的值为0,并行级别将保持不变,就像该函数从来没有被调用过.
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

void *thread1()
{
    printf("this is thread1.\n");
    sleep(1);
    printf("thread1 return\n");

}
/*线程2*/
void *thread2()
{
    printf("this is thread2.\n");
    sleep(2);
    printf("thread2 exiting\n");
    pthread_exit (0);
}

int main(void)
{
    int err;
    pthread_t tid1,tid2;
    pthread_attr_t attr;
    
    pthread_attr_init(&attr);
    
    pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
    
    pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
    
    err = pthread_create (&tid1, &attr, thread1, NULL);
    if (err != 0)
    {
        perror("create thread1 error");
        exit(1);
    }
    err = pthread_create (&tid2, NULL, thread2, NULL);
    if (err != 0)
    {
        perror("create thread2 error");
        exit(1);
    }
    
    err = pthread_join(tid1, NULL);
    if (err != 0)
    {
        printf("can`t join thread1\n ");
    }
    err = pthread_join(tid2, NULL);
    if (err != 0)
    {
        perror("join thread2 error");
        exit(1);
    }
    pthread_attr_destroy(&attr);
    exit(0);
}

4.5线程的扩展属性

有两个线程属性没有包含在pthread_attr_t结构中,它们是可取消状态和可取消类型.这两个属性影响着线程在响应pthread_cancel()函数调用时所呈现的行为.
可取消状态属性有两个取值:PTHREAD_CANCEL_ENABLE和PTHREAD_CANCEL_DISABLE.线程启动时默认的可取消状态是 PTHREAD_CANCEL_ENABLE ,当状态设为 PTHREAD_CANCEL_DISABLE 时,对pthread_cancel函数的调用不会杀死线程.线程可以通过调用pthread_setcancelstate()函数修改它的可取消状态,该函数的原型为:
#include <pthread.h>
int pthread_setcancelstate (int state, int *oldstate);
此函数把当前的可取消状态置为变量state,把原来的可取消状态存放在oldstate只想的内存单元中,若执行成功则返回0,否则返回错误编码.
此外,还可以修改线程取消动作的执行时机,函数pthread_setcanceltype修改线程的取消类型,有PTHREAD_CANCEL_DEFERRED和PTHREAD_CANCEL_ASYNCHRONOUS两个取值,分别表示线程收到取消信号后继续运行至下一个取消点和立即执行取消动作.该函数的原型为:

#include <pthread.h>
int  pthread_setcanceltype (int type, int *oldtype);
参数type的取值可以是PTHREAD_CANCEL_DEFERRED或PTHREAD_CANCEL_ASYNCHRONOUS,原来的值保存在oldstype中.
注意:可取消点是线程检查是否被取消并按照请求进行动作的一个位置.
线程还可以调用pthread_testcancel函数自己添加取消点,该函数的原型为:

#include <pthread.h>
int  pthread_testcancel (void);
该函数检查本线程是否处于"待取消"状态(已经收到pthread_cancel的信号,但还没有到达取消点),若可取消状态没有被取消,则线程执行取消动作,否则返回.
下面我们来看一个例子.

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

void *thread1(void *arg) {
    int i, err, j;
    err = pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
    if (err != 0) {
        perror("Thread1 pthread_setcancelstate failed");
        exit(EXIT_FAILURE);
    }
    err = pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);
    if (err != 0) {
        perror("Thread1 pthread_setcanceltype failed");
        exit(EXIT_FAILURE);
    }
    printf("thread1 is running\n");
    for(i = 0; i < 5; i++) {
        printf("Thread1 is still running (%d)...\n", i);
        sleep(1);
    }
    pthread_exit((void *)2);
}

void *thread2(void *arg) {
    int i, err, j;
    err = pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
    if (err != 0) {
        perror("Thread2 pthread_setcancelstate failed");
        exit(EXIT_FAILURE);
    }
    err = pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);
    if (err != 0) {
        perror("Thread2 pthread_setcanceltype failed");
        exit(EXIT_FAILURE);
    }
    printf("thread2 is running\n");
    for(i = 0; i < 5; i++) {
        printf("Thread2 is still running (%d)...\n", i);
        sleep(1);
    }
    pthread_exit((void *)2);
}

int main() {
    int err;
    pthread_t tid1,tid2;
    void *thread_result;

    err = pthread_create(&tid1, NULL, thread1, NULL);
    if (err != 0) {
        perror("Thread1 creation failed");
        exit(EXIT_FAILURE);
    }
    err = pthread_create(&tid2, NULL, thread2, NULL);
    if (err != 0) {
        perror("Thread2 creation failed");
        exit(EXIT_FAILURE);
    }
    sleep(2);
    
    printf("Cancelling thread1 ...\n");
    err = pthread_cancel(tid1);
    if (err != 0) {
        perror("Thread1 cancelation failed");
        exit(EXIT_FAILURE);
    }
    
    printf("Cancelling thread2 ...\n");
    err = pthread_cancel(tid2);
    if (err != 0) {
        perror("Thread2 cancelation failed");
        exit(EXIT_FAILURE);
    }


    printf("Waiting for thread1 to finish...\n");
    err = pthread_join(tid1, &thread_result);
    if (err != 0) {
        perror("Thread1 join failed");
        exit(EXIT_FAILURE);
    }
    printf("The exit code of thread1 :%d\n",(int)thread_result);

    printf("Waiting for thread2 to finish...\n");
    err = pthread_join(tid2, &thread_result);
    if (err != 0) {
        perror("Thread2 join failed");
        exit(EXIT_FAILURE);
    }
    printf("The exit code of thread2 :%d\n",(int)thread_result);

    exit(EXIT_SUCCESS);
}
线程1和线程2创建后即交替运行,由于线程1为可取消状态,当它接收到pthread_cancel的请求时便立即退出了(返回值为-1),而线程2为不可取消状态,因而一直运行到pthread_exit才退出(返回值为2).在上面的例子中,线程1的取消点为sleep函数,下表是常见的取消点.

当接到取消请求后,线程一旦执行到这些函数出现的位置就会退出.除此之外就需要通过pthread_testcancel函数人为的指定取消点.

相关文章:

  • 黑马程序员_String类
  • 洪水预报系统的读书笔记
  • android 关于闹钟
  • CSS中首字母大写怎么实现?
  • android环境下解决java.io.IOException: Malformed ipv6异常的方法
  • Scatter“秒传”的速度秒杀了云存储
  • 腾讯移动游戏平台发布了《天天酷跑》游戏
  • 黑马程序员_String练习
  • android下基本json串的生成与解析
  • FileZilla无法连接ftp服务器的解决方案
  • nginx编译安装报错src/os/unix/ngx_files.c: In function 鈔gx_write_chain_to_file?
  • WiFi基本知识
  • nginx配置DocumentRoot
  • 黑马程序员_StringBuffer,包装类
  • 【Android病毒分析报告】 - ZooTiger “集恶意推广、隐私窃取、恶意吸费于一体”...
  • 《微软的软件测试之道》成书始末、出版宣告、补充致谢名单及相关信息
  • 【前端学习】-粗谈选择器
  • 【刷算法】从上往下打印二叉树
  • Git初体验
  • javascript 哈希表
  • JavaScript 事件——“事件类型”中“HTML5事件”的注意要点
  • Javascript设计模式学习之Observer(观察者)模式
  • Js实现点击查看全文(类似今日头条、知乎日报效果)
  • JS正则表达式精简教程(JavaScript RegExp 对象)
  • Laravel 菜鸟晋级之路
  • Magento 1.x 中文订单打印乱码
  • passportjs 源码分析
  • SegmentFault 社区上线小程序开发频道,助力小程序开发者生态
  • Spring Boot快速入门(一):Hello Spring Boot
  • Vue.js 移动端适配之 vw 解决方案
  • vue从创建到完整的饿了么(18)购物车详细信息的展示与删除
  • vue脚手架vue-cli
  • 不用申请服务号就可以开发微信支付/支付宝/QQ钱包支付!附:直接可用的代码+demo...
  • 基于web的全景—— Pannellum小试
  • 面试总结JavaScript篇
  • 前嗅ForeSpider采集配置界面介绍
  • 算法之不定期更新(一)(2018-04-12)
  • 微服务核心架构梳理
  • 我的zsh配置, 2019最新方案
  • No resource identifier found for attribute,RxJava之zip操作符
  • 组复制官方翻译九、Group Replication Technical Details
  • #我与Java虚拟机的故事#连载02:“小蓝”陪伴的日日夜夜
  • #我与Java虚拟机的故事#连载17:我的Java技术水平有了一个本质的提升
  • #在 README.md 中生成项目目录结构
  • (2)(2.4) TerraRanger Tower/Tower EVO(360度)
  • (9)YOLO-Pose:使用对象关键点相似性损失增强多人姿态估计的增强版YOLO
  • (Demo分享)利用原生JavaScript-随机数-实现做一个烟花案例
  • (附源码)ssm高校运动会管理系统 毕业设计 020419
  • (六)软件测试分工
  • (欧拉)openEuler系统添加网卡文件配置流程、(欧拉)openEuler系统手动配置ipv6地址流程、(欧拉)openEuler系统网络管理说明
  • (五)关系数据库标准语言SQL
  • (一)80c52学习之旅-起始篇
  • (一)Mocha源码阅读: 项目结构及命令行启动
  • (原創) 如何動態建立二維陣列(多維陣列)? (.NET) (C#)
  • .net core 微服务_.NET Core 3.0中用 Code-First 方式创建 gRPC 服务与客户端