C++实战-Linux多线程(入门到精通)(一)

简介: C++实战-Linux多线程(入门到精通)(一)

线程的概念

1.与进程(process)类似,线程(thread)是允许应用程序并发执行多个任务的一种机制。一个进程可以包含多个线程。同一个程序中的所有线程均会独立执行相同的程序,并且共享同一份全局内存区域,其中包括初始化数据段(.data),未初始化数据段(.bss),栈内存段。

【注意:没有共享栈内存和代码段】

2.进程是CPU分配资源的最小单位,线程是操作系统调度执行的最小单位

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

4.查看指定进程的LWP:ps -lf pid   (注:lwp不是线程的id)

Linux内核线程实现原理

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

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

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

3.进程可以蜕变成线程(看上面的虚拟地址空间就能分析出来)

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

5.LWP和进程ID的区别:LWP号是Linux内核划分时间轮片给线程的依据,线程ID是在进程内部区分线程的

a

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

但,线程不同。两个线程虽然有独立的PCB,但是共享同一个页目录、页表、物理页面。所以两个PCB共享同一个地址空间。

7.无论是创建进程的fork()还是创建线程的pthread_creat(),底层实现都是调用一个内核函数clone()。如果是复制对方的地址空间,那么就产生一个“进程”,如果是共享对方的地址空间,就产生一个"线程"。因此,Linux内核是不区分进程和线程的。只是在用户层面上进行区分。所以,线程所有的操作函数pthrad_*是库函数,而非系统调用。

进程和线程区别总结

1.进程间的信息难以共享。由于除去只读代码段外,父子进程并未共享内存,因此必须采用一些进程间通信方式,在进程间进行信息交换。

2.调用fork()来创建进程的代价相对较高(复制一份地址空间),即便采用"写时复制"机制,仍然需要复制诸如内存页表和文件描述符表之类的多种进程属性,这意味着fork()调用在时间上的开销依然不菲

3.线程之间能够方便、快速地共享信息,只需要将数据复制到共享(栈不行,见上图)变量中即可

4.创建线程比创建进程通常要快10倍甚至更多。线程间是共享虚拟地址空间的,无需采用写时复制来复制内存,也无需复制页表(之前说过虚拟地址空间的实现,并不是真的去分配4G的内存,只需要实现相应的数据结构,页表、页目录)

线程之间共享和非共享资源

这个图太重要了,我们再看一下

共享资源 非共享资源
文件描述符表 线程ID
每种信号的处理方式 处理器现场和栈指针(内核栈)
当前工作目录、文件权限 独立的栈空间(用户空间栈)
用户ID和组ID和会话ID errno变量
虚拟地址空间(除栈  .txt) 信号屏蔽字
调度优先级

介绍下NPTL(第三方库):

       当Linux最初开发时,在内核中并不能正真支持线程。通过clone()系统调用进程作为可调度的实体。这个调用创建了调用进程的一个拷贝,这个拷贝于调用共享相同的地址空间。但这个符合POSIX的要求,在信号处理、调度间同步等方面存在问题。

       NPTL或称为Native POSIX Thread Library。是Linux线程的一个新实现,它克服了LinuxThreads的缺点,同时符合标准。

       查看当前进程库版本:getconf GNU_LIBPTHREAD_VERSION

线程优缺点

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

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

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

线程相关函数(线程控制原语)

一般情况下,main函数所在的线程称为主线程。其余创建的线程称为子线程。

fork()函数--->创建进程

程序中默认只有一个线程,pthread_create()函数调用-->2个线程


pthread_create函数

       int pthread_create(pthread_t *thread,const pthread_attr_t *attr,

                                       void*(*start_routine)(void*),void *arg);

       功能:创建一个子线程

       参数:

                  thread:传出参数,线程创建成功后,子线程的线程ID被写到该变量中

                  attr设置线程的属性,一般使用默认值 NULL

                  start_routine:函数指针,这个函数是子线程需要处理的逻辑代码

                  arg:给第三个参数使用

       返回值:

                  成功:0

                  失败:返回错误号

补充说明:因为线程是通过NPTL库是第三方库,跟我们的系统调用不一样哦。

                 之前我们一直使用perror(ret),现在可行不通. 使用 char *strerror(int errnum)

 

#include <iostream>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
using namespace std;
void *func(void *arg)
{
    printf("pthread:%lu\n",pthread_self());
    return NULL;
}
int main(void)
{
    //typedef unsigned long 
    pthread_t tid;
    int ret = pthread_create(&tid,NULL,func,NULL);
    if(ret)
    {
        printf("%s\n",strerror(ret));
        exit(-1);
    }
    sleep(1);
    printf("我是主线程:%d\n",getpid());
    return 0;
}

编译注意事项:线程椒通过第三方库实现的

g++ createPthread.cpp -o createPthread -lpthread(-pthread)


pthread_exit函数

       int pthread_exit(void *retval);

       功能:终止一个线程,在哪个线程中调用就代表终止哪个线程

       参数:

               retval:需要传递一个指针,作为一个返回值,在pthread_join()中可以获取到。

                          线程退出的状态,通常传NULL

      补充:当主线程退出时,不会影响其他正常运行的线程

                 在线程中禁止使用exit函数,会导致进程内所有线程全部退出

       

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
void *func(void *arg)
{
   int i = (int)arg;
   if(i == 2)
   {
        pthread_exit(NULL);
   }
   sleep(2);
    printf("我是第%d号线程,线程ID:%lu\n",i,pthread_self());
   return NULL;
}
int main(void)
{
    pthread_t tid;
    for(int i=0;i<5;++i)
    {
        pthread_create(&tid,NULL,func,(void *)i);
    }
    sleep(5);
    printf("我是主线程,线程ID:%lu\n",pthread_self());
    return 0;
}

pthread_self函数

       pthread_t pthread_self(void)

       功能:获取当前的线程的线程ID


pthread_equal函数

       int pthread_equal(pthread_t t1,pthread_t t2);

       功能:比较两个线程ID是否相等

       返回值:相等非0,不相等0

       补充:不同操作系统pthread_t类型的实现不一样,有的时无符号长整型,有的可能是结构体      

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_t tid_one;
void *func(void *arg)
{
    if(pthread_equal(tid_one,pthread_self()))
    {
        printf("相等\n");
    }
    else
    {
        printf("不相等\n");
    }
}
int main(void)
{
    pthread_t tid;
    pthread_create(&tid_one,NULL,func,NULL);
    pthread_join(tid_one,NULL);
    return 0;
}

pthread_join函数

       int pthread_join(pthread_t thread,void ** retval);

       功能:和一个已经终止的线程进行连接,回收资源

                  回收子进程的资源

                  这个函数是阻塞的,调用一次只能回收一个子进程

                  一般在主线程中使用

       参数:thread  需要回收的子进程ID

                  retval   接收子进程退出时的返回值

       返回值:

                  0   成功

                  非0,失败,返回错误号


线程的分离

       int pthread_detach(pthread_t thread);

       功能:分离一个线程。被分离的线程在终止的时候,会自动释放资源返回给系统

       注意事项:

               1).不能多次分离,会产生不可预测的行为

               2).不能去连接一个已经分离的线程,会报错【pthread_join】

       参数:

               需要分离的线程的ID

       返回值:

               成功0   失败错误号

重点说明:

       线程分离状态:指定该状态,线程主动与主控制线程断开关系。线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。网络、多线程服务器常用。

       进程如果有这样的机制,将不会产生僵尸进程,僵尸进程的产生主要由于进程死亡后,大部分资源被释放,一点残留资源仍然在系统中,导致内核认为该进程仍然存在

       一般情况下,线程终止后,其终止状态一直保留到其他线程调用pthread_join获取它的状态为止,但是线程也可以被设置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。不能对一个已经处于detach状态的线程调用pthread_join这样的调用将返回EINVAL错误。

#include <stdio.h>
#include <pthread.h>
void* func(void *arg)
{
    printf("我是子线程:%lu\n",pthread_self());
    return NULL;
}
int main(void)
{
    pthread_t tid;
    pthread_create(&tid,NULL,func,NULL);
    //设置线程分离
    pthread_detach(tid);
    sleep(3);
    return 0;
}

线程的取消

       int pthread_cancel(pthread_t  thread);

       功能:取消线程(让线程终止)

                  取消某个进程,可以终止某个线程的运行。

                  但不是立马终止,而是当子线程执行到一个取消点,线程才会终止

                  取消点:系统调用(从用户态切换到内核态的时候)  creat open pause read write..

              如果线程终没有取消点,可以通过调用pthread_testcancel函数自行设置一个取消点

       被取消的线程,退出值定义在Linux的pthread库中。常数PTHREAD_CANCELED的值是一个 -1。可在pthread.h中找到它的定义。

       

       因此我们对一个已经被取消的线程使用pthread_join回收时,得到的返回值-1

#include <stdio.h>
#include <pthread.h>
void *func(void *arg)
{
    //printf("我是子线程:%lu\n",pthread_self()); //printf会产生一个系统调用
    pthread_testcancel();   //自己设置一个取消点
    return NULL;
}
int main(void)
{
    pthread_t tid;
    pthread_create(&tid,NULL,func,NULL);
    //取消这个线程
    pthread_cancel(tid);
    //回收
    int childRet;
    pthread_join(tid,(void **)&childRet);   //阻塞的
    printf("回收的返回值:%d\n",childRet);  
    return 0;
}
相关文章
|
4月前
|
存储 Linux C语言
Linux C/C++之IO多路复用(aio)
这篇文章介绍了Linux中IO多路复用技术epoll和异步IO技术aio的区别、执行过程、编程模型以及具体的编程实现方式。
164 1
Linux C/C++之IO多路复用(aio)
|
7天前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
本文详细介绍了如何在Linux中通过在业务线程中注册和处理信号。我们讨论了信号的基本概念,并通过完整的代码示例展示了在业务线程中注册和处理信号的方法。通过正确地使用信号处理机制,可以提高程序的健壮性和响应能力。希望本文能帮助您更好地理解和应用Linux信号处理,提高开发效率和代码质量。
38 17
|
16天前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
通过本文,您可以了解如何在业务线程中注册和处理Linux信号。正确处理信号可以提高程序的健壮性和稳定性。希望这些内容能帮助您更好地理解和应用Linux信号处理机制。
50 26
|
8天前
|
消息中间件 Linux C++
c++ linux通过实现独立进程之间的通信和传递字符串 demo
的进程间通信机制,适用于父子进程之间的数据传输。希望本文能帮助您更好地理解和应用Linux管道,提升开发效率。 在实际开发中,除了管道,还可以根据具体需求选择消息队列、共享内存、套接字等其他进程间通信方
39 16
|
4月前
|
安全 程序员 编译器
【实战经验】17个C++编程常见错误及其解决方案
想必不少程序员都有类似的经历:辛苦敲完项目代码,内心满是对作品品质的自信,然而当静态扫描工具登场时,却揭示出诸多隐藏的警告问题。为了让自己的编程之路更加顺畅,也为了持续精进技艺,我想借此机会汇总分享那些常被我们无意间忽视却又导致警告的编程小细节,以此作为对未来的自我警示和提升。
637 16
|
3月前
|
Unix Linux Shell
linux入门!
本文档介绍了Linux系统入门的基础知识,包括操作系统概述、CentOS系统的安装与远程连接、文件操作、目录结构、用户和用户组管理、权限管理、Shell基础、输入输出、压缩打包、文件传输、软件安装、文件查找、进程管理、定时任务和服务管理等内容。重点讲解了常见的命令和操作技巧,帮助初学者快速掌握Linux系统的基本使用方法。
185 3
|
3月前
|
消息中间件 存储 安全
|
4月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
79 1
|
3月前
|
自然语言处理 编译器 Linux
告别头文件,编译效率提升 42%!C++ Modules 实战解析 | 干货推荐
本文中,阿里云智能集团开发工程师李泽政以 Alinux 为操作环境,讲解模块相比传统头文件有哪些优势,并通过若干个例子,学习如何组织一个 C++ 模块工程并使用模块封装第三方库或是改造现有的项目。
|
4月前
|
存储 并行计算 安全
C++多线程应用
【10月更文挑战第29天】C++ 中的多线程应用广泛,常见场景包括并行计算、网络编程中的并发服务器和图形用户界面(GUI)应用。通过多线程可以显著提升计算速度和响应能力。示例代码展示了如何使用 `pthread` 库创建和管理线程。注意事项包括数据同步与互斥、线程间通信和线程安全的类设计,以确保程序的正确性和稳定性。