简 述: 前面几篇,学习了 Linux 下多进程使用 fork() 分析的其构造和原理;这里进一步,探究一下如何创建多线程,以及多线程和多进程之间的差异。最后写几个实例;验证分析。

[TOC]


本文初发于 “偕臧的小站“,同步转载于此。


编程环境:

  💻: uos20 📎 gcc/g++ 8.3 📎 gdb8.0

  💻: MacOS 10.14 📎 gcc/g++ 9.2 📎 gdb8.3


线程:

线程和进程的理论概念不再称述;这里主要是从虚拟地址空间和 PCB 的角度来看,主、子线程。

Linux 中,系统是不认识线程还是进程的,它们只认识 PCB;

Unix 和 Linux 的线程在系统的底层实现是不一样的,有区别的,但是它们在应用层是看起来没什么区别;Linux 中一开始没有线程,是 Windows 中先有的,后面被前辈们移植过来的,所以 Linux 中线程的实现,系统底层是由创建进程改了改。

  • 主线程和子线程:

    • 共享:
      • 用户区内,除了栈区是不共享的,其余都是不共享的。
    • 不共享:
      • 栈区(当有 1 主 + 4 子线程时候,栈区会被平分为 5 份)
  • 在 Linux 下:

    • 线程就是进程 – 轻量级的进程
    • 对于内核来说,线程就是进程(内核只会用)
  • 多线程和多进程的区别:

    • 多进程共享的资源:
      • 代码
      • 文件描述符
      • 内存映射区 –mmap
    • 多线程共享的资源:
      • 全局变量
      • 相比多进程,更加节省系统资源;对于系统 CPU 轮转时间片来说,不论是线程还是进程,它不认识,只认 PCB。

安装线程 man page:

  • 安装线程 man page 命令:

    sudo apt install manpages-posix-dev

查看指定线程的 LWP 号:

  • 线程号和线程 ID 是有区别的
  • 线程号是给内核看的
  • 查看方式:
    • 找到程序的进程 ID
    • ps -Lf pid

一个例子,查看火狐浏览器程序由多线程构成:

如: Linux 下 查看火狐浏览器,发现其是由多线程构成的

ps ajx | grep "firefox"

ps -Lf 31102

pthread_create():

  • 作用: 创建子线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
  • 主线程退出,子线程一定会被强制结束退出。

  • 参数:

    • thread: 传出参数,线程创建成功后,会设置一个合适值,通常比较大。
    • attr: 线程属性,默认使用 NULL
    • start_routine: 子线程的处理函数(也被称为回调函数)
    • arg: 子线程的处理函数的参数
  • 返回值:

    • 函数调用成功返回 0,失败返回错误号;但此处的错误号不是之前那个能够用 perror() 打印的错误号,这两个不是一个东西,他们各自使用的库不通用。

    • 想要打印错误信息,需要使用标准库函数 strerror()

      int ret = pthread_creator(...)
      if (ret != 0) 
      	printf("error: %s\n", strerror(ret))

写一个例子:

例子一:

使用不带参数的,子线程的处理函数,验证子线程执行的代码,只有该回调函数里面的内容。

  • 代码示例:

    #include <stdio.h>
    #include <unistd.h>
    #include <pthread.h>
    
    void* myfun(void* arg);
    
    int main(int argc, char *argv[])
    {
        pthread_t pthread = 0;  //创建一个子线程
        int ret = pthread_create(&pthread, NULL, myfun, NULL);
        printf("parent thread id: %ld\n", pthread_self);
    
        sleep(2);  //避免主线程运行后,就死亡了,而子线程没机会
        for (int i = 0; i < 5; i++) {  //验证子线程,并不会执行这里面的代码,只会执行回调函数 muyfun 里面的
            printf("i = %d\n", i);
        }
        
        return 0;
    }
    
    void* myfun(void* arg)
    {
        printf("child thread id: %ld\n", pthread_self);
        return NULL;
    }
    
  • 运行结果:


例子二:

写一个能够创建多个子线程的函数,要使用到其参数。

注意其是地址传递的话,有这样一种可能,该变量是在主线程的地址空间中,其他几个子线程都是在该线程中读取改值;但是由于他们分别抢占 CPU 时间片,会导致结果可能和预期结果不符;此时可以考虑使用值传递。

  • 代码示例:

    #include <stdio.h>
    #include <unistd.h>
    #include <pthread.h>
    
    int num = 13; //设置为全局变量,在全局区域,共享
    
    void* myfun(void* arg);
    
    int main(int argc, char *argv[])
    {
        void* p = (void *)&num;  //传一个地址进去(voi* 也是 4 个字节)
        pthread_t id[5] = {0};
        for (int i = 0; i < 5; i++) {
            pthread_create(&(id[i]), NULL, myfun, p);
            printf("i = %d, thread id: %ld\n", i, id[i]);
        }
        
        return 0;
    }
    
    void* myfun(void* arg)
    {
        printf("num = %d, child thread id: %ld\n", (*((int *)arg))++, pthread_self());
        return NULL;
    }
  • 程序分析:

    可以看出,其其创建多个子线程成功了🤓🤓,和预期结果相符。

  • 运行结果:

  • 其它

    如果将全局变量 int num = 13; 放到 main() 里面,当做局部变量的话,就会出现如下结果!!! 有一处小的瑕疵,有点困惑?注意其 mun 的值,有时候会异常,怀疑是多个线程抢占 CPU 的时间片 + 是传递的地址,或者有其他系统或者数据修改过该值???所以有时候值得范围是非 13-17;异常变得巨大; 原因还在找,或者你知道的话,可以在下方留言告诉我,学习知识这种事,达者为先,相互指点一下后到“晚辈”。


下载地址:

17_thread

欢迎 star 和 fork 这个系列的 linux 学习,附学习由浅入深的目录。