Linux 循环创建多个线程

时间:2024-01-19 09:19:56

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

线程概念

什么是线程

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

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号。

Linux 循环创建多个线程

三级映射:进程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;

}

结果:

Linux 循环创建多个线程

由于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*类型了,并且编译过程中也给我警告了:

Linux 循环创建多个线程

位机,如果是在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一结束,管你后面是不是还有线程的,统统杀死。