Linux 循环创建多个线程
这里说一下相关的基础知识:
线程概念
什么是线程
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 循环创建多个线程的更多相关文章
- Linux:回收循环创建的多个线程
上午我说了循环创建多个线程,由于进程与线程是如此的相似,进程我们知道要回收,那么线程也自然要回收啦.我们接着看控制原语: 线程与共享 线程间共享全局变量! [牢记]:线程默认共享数据段.代码段等地址空 ...
- C++ Linux 多线程之创建、管理线程
线程就是,在同一程序同一时间内同意运行不同函数的离散处理队列. 这使得一个长时间去进行某种特殊运算的函数在运行时不阻碍其它的函数变得十分重要. 线程实际上同意同一时候运行两种函数,而这两个函数不必相互 ...
- python 计算机发展史,线程Process使用 for循环创建 2种传参方式 jion方法 __main__的解释
########################总结################## #一 操作系统的作用: 1:隐藏丑陋复杂的硬件接口,提供良好的抽象接口 2:管理.调度进程,并且将多个进程对硬 ...
- Linux C 一个简单的线程池程序设计
最近在学习linux下的编程,刚开始接触感觉有点复杂,今天把线程里比较重要的线程池程序重新理解梳理一下. 实现功能:创建一个线程池,该线程池包含若干个线程,以及一个任务队列,当有新的任务出现时,如果任 ...
- Linux多线程实践(9) --简单线程池的设计与实现
线程池的技术背景 在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源.在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收.所以 ...
- LINUX操作系统知识:进程与线程详解
当一个程序开始执行后,在开始执行到执行完毕退出这段时间内,它在内存中的部分就叫称作一个进程. Linux 是一个多任务的操作系统,也就是说,在同一时间内,可以有多个进程同时执行.我们大家常用的单CPU ...
- Linux下c开发 之 线程通信(转)
Linux下c开发 之 线程通信(转) 1.Linux“线程” 进程与线程之间是有区别的,不过Linux内核只提供了轻量进程的支持,未实现线程模型.Linux是一种“多进程单线程”的操作系统.Linu ...
- 第六周——分析Linux内核创建一个新进程的过程
"万子恵 + 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 &q ...
- 分析Linux内核创建一个新进程的过程
一.原理分析 1.进程的描述 进程控制块PCB——task_struct,为了管理进程,内核必须对每个进程进行清晰的描述,进程描述符提供了内核所需了解的进程信息. struct task_struct ...
随机推荐
- [UE4]蓝图调试小心得
假设某个功能由10个蓝图步骤组成,在第10步的某个蓝图函数的其中一个参数为none,可以使用not equal(Object)和Print从第10步开始,一步一步倒退到第一步检查,看看是哪一个步骤参数 ...
- 2018年最新PHP面试题
面试之前多看看公司的资料,可以看出面试的公司主要做什么,电商,数据库,php函数,sql的优化,接口,session和cookie等经常会问到,都是必问之题,这其中有一部分题目摘抄自网络,回答也不错 ...
- 13-001 ViewComponents IViewComponentActivator
接口定义: /// <summary> /// Provides methods to activate an instantiated ViewComponent /// </su ...
- c#语言函数
class Program {访问修饰符 函数名(参数1,参数2){ 函数体 return 返回值} 无参数,无返回值 public static void abc() ...
- gulp 使用案例
1.gulp 配置: // 引入 gulp var gulp = require('gulp'); var watch = require('gulp-watch'), //导入所有gulp需要的模块 ...
- jq遍历list和object
<script> //----------------for用来遍历数组对象-- var i,myArr = [1,2,3]; for (var i = 0; i < myArr.l ...
- java中六个时间类的使用和区别
java.util.Date java.sql.Date java.sql.Time java.sql.Timestamp java.text.SimpleDateFormat java.ut ...
- java 日期排序。。。。
Collections.sort(list, new Comparator<Map<Object, Object>>() { public int compare(Map< ...
- uva-10282-枚举
题意:语言翻译, 直接map即可 #include "pch.h" #include <string> #include<iostream> #includ ...
- Windows下python库的常用安装方法
目录: 1.pip安装(需要pip) 2.通过下载whl文件安装(需要pip) 3.在pythn官网下载安装包安装(不需要pip) 方法一:pip安装. 这是最 ...