这里说一下相关的基础知识:

线程概念

什么是线程

LWP:light weight process 轻量级的进程,本质仍是进程(在Linux环境下)

    进程:独立地址空间,拥有PCB

    线程:也有PCB,但没有独立的地址空间(共享)

    区别:在于是否共享地址空间。    独居(进程);合租(线程)。

    Linux下:    线程:最小的执行单位

                 进程:最小分配资源单位,可看成是只有一个线程的进程。

Linux内核线程实现原理

类Unix系统中,早期是没有"线程"概念的,80年代才引入,借助进程机制实现出了线程的概念。因此在这类系统中,进程和线程关系密切。

1. 轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone

2. 从内核里看进程和线程是一样的,都有各自不同的PCB,但是PCB中指向内存资源的三级页表是相同的

3. 进程可以蜕变成线程

4. 线程可看做寄存器和栈的集合

5. 在linux下,线程最是小的执行单位;进程是最小的分配资源单位

察看LWP号:ps –Lf pid 查看指定线程的lwp号。

三级映射:进程PCB --> 页目录(可看成数组,首地址位于PCB中) --> 页表 --> 物理页面 --> 内存单元

参考:《Linux内核源代码情景分析》 ----毛德操

对于进程来说,相同的地址(同一个虚拟地址)在不同的进程中,反复使用而不冲突。原因是他们虽虚拟址一样,但,页目录、页表、物理页面各不相同。相同的虚拟址,映射到不同的物理页面内存单元,最终访问不同的物理页面。

但!线程不同!两个线程具有各自独立的PCB,但共享同一个页目录,也就共享同一个页表和物理页面。所以两个PCB共享一个地址空间。

    实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数clone。

    如果复制对方的地址空间,那么就产出一个"进程";如果共享对方的地址空间,就产生一个"线程"。

    因此:Linux内核是不区分进程和线程的。只在用户层面上进行区分。所以,线程所有操作函数 pthread_* 是库函数,而非系统调用。

线程共享资源

    1.文件描述符表

    2.每种信号的处理方式

    3.当前工作目录

    4.用户ID和组ID

    5.内存地址空间 (.text/.data/.bss/heap/共享库)

线程非共享资源

    1.线程id

    2.处理器现场和栈指针(内核栈)

    3.独立的栈空间(用户空间栈)

    4.errno变量

    5.信号屏蔽字

    6.调度优先级

线程优、缺点

    优点:    1. 提高程序并发性    2. 开销小    3. 数据通信、共享数据方便

    缺点:    1. 库函数,不稳定    2. 调试、编写困难、gdb不支持    3. 对信号支持不好

    优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线

程差别不是很大。

线程控制原语

pthread_self函数

获取线程ID。其作用对应进程中 getpid() 函数。

    头文件:#include <pthread.h>

    pthread_t pthread_self(void);    返回值:成功:0;    失败:无!

pthread_t:当前Linux中可理解为:typedef unsigned long int pthread_t;//无符号长整形

    线程ID:pthread_t类型,本质:在Linux下为无符号整数(%lu),其他系统中可能是结构体实现

    线程ID是进程内部,识别标志。(两个进程间,线程ID允许相同)

    注意:不应使用全局变量 pthread_t tid,在子线程中通过pthread_create传出参数来获取线程ID,而应使用pthread_self。

pthread_create函数

创建一个新线程。        其作用,对应进程中fork() 函数。

    头文件:#include <pthread.h>

    int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

    返回值:成功:0;    失败:错误号    -----Linux环境下,所有线程特点,失败均直接返回错误号。

参数:    

    pthread_t:当前Linux中可理解为:typedef unsigned long int pthread_t;//无符号长整形

参数1:传出参数,保存系统为我们分配好的线程ID

    参数2:通常传NULL,表示使用线程默认属性。若想使用具体属性也可以修改该参数。

    参数3:函数指针,指向线程主函数(线程体),该函数运行结束,则线程结束。参数是函数指针,只能传递函数名,不能传递参数。所以就是只能有一个参数。

    参数4:线程主函数执行期间所使用的参数。

在一个线程中调用pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行,而新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决定。start_routine函数接收一个参数,是通过pthread_create的arg参数传递给它的,该参数的类型为void *,这个指针按什么类型解释由调用者自己定义。start_routine的返回值类型也是void *,这个指针的含义同样由调用者自己定义。start_routine返回时,这个线程就退出了,其它线程可以调用pthread_join得到start_routine的返回值,类似于父进程调用wait(2)得到子进程的退出状态,稍后详细介绍pthread_join。

pthread_create成功返回后,新创建的线程的id被填写到thread参数所指向的内存单元。我们知道进程id的类型是pid_t,每个进程的id在整个系统中是唯一的,调用getpid(2)可以获得当前进程的id,是一个正整数值。线程id的类型是thread_t,它只在当前进程中保证是唯一的,在不同的系统中thread_t这个类型有不同的实现,它可能是一个整数值,也可能是一个结构体,也可能是一个地址,所以不能简单地当成整数用printf打印,调用pthread_self(3)可以获得当前线程的id。

attr参数表示线程属性,本节不深入讨论线程属性,所有代码例子都传NULL给attr参数,表示线程属性取缺省值,感兴趣的读者可以参考APUE。

现在我们先预热:创建一个新线程,打印线程ID。注意:链接线程库 -lpthread。

#include
<stdio.h>

#include
<pthread.h>

#include
<unistd.h>

void *tfn(void *arg)

{

    printf("我是线程,我的ID = %lu\n", pthread_self());

    return
NULL;

}

int main(void)

{

    pthread_t tid;

    pthread_create(&tid, NULL, tfn, NULL);

    sleep(1);

    printf("我是进程,我的进程ID = %d\n", getpid());

    return 0;

}

结果:

由于pthread_create的错误码不保存在errno中,因此不能直接用perror(3)打印错误信息,可以先用strerror(3)把错误码转换成错误信息再打印。如果任意一个线程调用了exit或_exit,则整个进程的所有线程都终止,由于从main函数return也相当于调用exit,为了防止新创建的线程还没有得到执行就终止,我们在main函数return之前延时1秒,这只是一种权宜之计,即使主线程等待1秒,内核也不一定会调度新创建的线程执行,下一节我们会看到更好的办法。要这样写命令:gcc -pthread pthread_create.c -o pthread_create

现在进入主题:循环创建多个线程,每个线程打印自己是第几个被创建的线程。(类似于进程循环创建子进程)

#include
<pthread.h>

#include
<stdio.h>

#include
<unistd.h>

#include
<stdlib.h>

void *tfn(void *arg)

{

    int i;

    i = (int)arg;

    sleep(i); //通过i来区别每个线程

    printf("我是第%d个线程,我的线程ID = %lu\n", i + 1, pthread_self());

    return
NULL;

}

int main(int
argc, char *argv[])

{

    int n = 5, i;

    pthread_t tid;

    if (argc == 2)

        n = atoi(argv[1]);

    for (i = 0; i < n; i++) {

        pthread_create(&tid, NULL, tfn, (void *)i);

        //将i转换为指针,在tfn中再强转回整形。

    }

    sleep(n);

    printf("我是main函数,但是我不是进程,我的ID = %lu\n", pthread_self());

    return 0;

}

结果:

一切正常,现在我解释一些代码:  pthread_create(&tid, NULL, tfn, (void *)i);这里的 (void *)i参数应该是指针,但是我们这里是将其强转为void*类型了,并且编译过程中也给我警告了:

位机,如果是在32位机上编译是没有这样的错误的。这个警告是在说int和void转化中的长度不一致(在我的机器上)。void在64位机上是8位,int一般来说都是4位。这在第一次转化的时候是小变大,会发生补零,在高位上补零;第二次在i = (int)arg;这里发生大变小转化,会截取,截取高位。所以,实际上对于这个程序来说是没有影响的。所以那两个警告是没有问题的。其他的我相信是没有什么问题的。

修改为(void *)&i, 将线程主函数(tfn)内改为 i=*((int *)arg) 是否可以?

开始了,一会儿4个线程,一会儿5个线程。这很蛋疼啊:第四个参数应该是指针啊,没错啊。可就是不对。其实也很好理解的,线程之间共享一个用户空间,我们这样传递的是i的地址过去,然后在运行线程主函数的时候依据地址找i的值,那么,问题出现了,cpu是个很快的男人,从main到线程主函数这之间有时间差吧?所以,在那么点时间内,i的值发生改变了。那为什么有时候线程个数不足?上面只要main一结束,管你后面是不是还有线程的,统统杀死。

Linux 循环创建多个线程的更多相关文章

  1. Linux:回收循环创建的多个线程

    上午我说了循环创建多个线程,由于进程与线程是如此的相似,进程我们知道要回收,那么线程也自然要回收啦.我们接着看控制原语: 线程与共享 线程间共享全局变量! [牢记]:线程默认共享数据段.代码段等地址空 ...

  2. C++ Linux 多线程之创建、管理线程

    线程就是,在同一程序同一时间内同意运行不同函数的离散处理队列. 这使得一个长时间去进行某种特殊运算的函数在运行时不阻碍其它的函数变得十分重要. 线程实际上同意同一时候运行两种函数,而这两个函数不必相互 ...

  3. python 计算机发展史,线程Process使用 for循环创建 2种传参方式 jion方法 __main__的解释

    ########################总结################## #一 操作系统的作用: 1:隐藏丑陋复杂的硬件接口,提供良好的抽象接口 2:管理.调度进程,并且将多个进程对硬 ...

  4. Linux C 一个简单的线程池程序设计

    最近在学习linux下的编程,刚开始接触感觉有点复杂,今天把线程里比较重要的线程池程序重新理解梳理一下. 实现功能:创建一个线程池,该线程池包含若干个线程,以及一个任务队列,当有新的任务出现时,如果任 ...

  5. Linux多线程实践(9) --简单线程池的设计与实现

    线程池的技术背景 在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源.在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收.所以 ...

  6. LINUX操作系统知识:进程与线程详解

    当一个程序开始执行后,在开始执行到执行完毕退出这段时间内,它在内存中的部分就叫称作一个进程. Linux 是一个多任务的操作系统,也就是说,在同一时间内,可以有多个进程同时执行.我们大家常用的单CPU ...

  7. Linux下c开发 之 线程通信(转)

    Linux下c开发 之 线程通信(转) 1.Linux“线程” 进程与线程之间是有区别的,不过Linux内核只提供了轻量进程的支持,未实现线程模型.Linux是一种“多进程单线程”的操作系统.Linu ...

  8. 第六周——分析Linux内核创建一个新进程的过程

    "万子恵 + 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 &q ...

  9. 分析Linux内核创建一个新进程的过程

    一.原理分析 1.进程的描述 进程控制块PCB——task_struct,为了管理进程,内核必须对每个进程进行清晰的描述,进程描述符提供了内核所需了解的进程信息. struct task_struct ...

随机推荐

  1. centos7更改引导项等待时间

    centos7已经不用grub,改用grub2. [ root]# vi /boot/grub2/grub.cfg 找到并更改启动时间(timeout) [root]# grub2-mkconfig ...

  2. SCCM2012 R2实战系列之十:解决WDS服务无法启动问题(错误1067:进程意外终止)

    在操作系统分发(OSD)之前需要开启PXE服务,然后会自动在SCCM服务器安装Windows Deployment Service. 但是之前在一次项目过程当中发现启用PXE服务后WDS无法启动,本以 ...

  3. 动态材质实例(Dynamic Material Instance)

    转自:http://blog.csdn.net/panda1234lee/article/details/62041775 本例将通过 “靠近影响椅子的颜色” 来展示什么是 动态材质实例(Dynami ...

  4. scrapy框架之日志等级和请求传参-cookie-代理

    一.Scrapy的日志等级 - 在使用scrapy crawl spiderFileName运行程序时,在终端里打印输出的就是scrapy的日志信息. - 日志信息的种类: ERROR : 一般错误 ...

  5. Django中的ORM相关操作:F查询,Q查询,事物,ORM执行原生SQL

    一    F查询与Q查询: 1 . F查询: 在上面所有的例子中,我们构造的过滤器都只是将字段值与某个常量做比较.如果我们要对两个字段的值做比较,那该怎么做呢? Django 提供 F() 来做这样的 ...

  6. CF603EPastoral Oddities

    /* LCT管子题(说的就是你 水管局长) 首先能得到一个结论, 那就是当且仅当所有联通块都是偶数时存在构造方案 LCT动态加边, 维护最小生成联通块, 用set维护可以删除的边, 假如现在删除后不影 ...

  7. Linux安装vsftpd组件

    1 安装vsftpd组件 安装完后,有/etc/vsftpd/vsftpd.conf 文件,是vsftp的配置文件. [root@hadoop1 ~]# yum -y install vsftpd 2 ...

  8. puppeteer 的PDD反爬经历

    使用puppeteer 爬取PDD数据时出现要求登录,以前是没有这问题的. 尝试多种方式如果: 变更UA 变更代理IP 变更Chromium版本(当然最终就是该问题的原因,但是因为版本跨度太大没有测试 ...

  9. 介绍Collection框架的结构;Collection 和 Collections的区别

    介绍Collection框架的结构:Collection 和 Collections的区别 集合框架: Collection:List列表,Set集 Map:Hashtable,HashMap,Tre ...

  10. Java并发编程:Java Thread 的 run() 与 start() 的区别

    1. sleep 和 wait 方法解释 sleep()方法是Thread类里面的,主要的意义就是让当前线程停止执行,让出cpu给其他的线程,但是不会释放对象锁资源以及监控的状态,当指定的时间到了之后 ...