Linux:多线程

背景知识

a.重谈地址空间

b.理解代码数据划分的本质

1.理解文件缓冲区

2.虚拟地址的本质是什么,是一种资源

Linux线程概念

什么是线程

  • 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是一个进程内部的控制序列”
  • 一切进程至少都有一个执行线程
  • 线程在进程内部运行,本质是在进程地址空间内运行
  • Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
  • 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流

回忆一下进程

进程的定义---内核观点

进程=内核数据结构+进程代码和数据

进程:承担分配系统资源的基本实体

以前我们使用进程的时候,内部只有一个执行流的进程

task_struct<=进程

CPU不做区分task_struct是进程还是线程,都是执行流

CPU看到的执行流<=进程

Linux中的执行流:轻量级进程  (Linux用进程模拟的线程)

Linux中是轻量级进程与其他操作系统还是有区别的

Linux中只有一个轻量级进程只有一个PCB,用这个轻量级进程可以模拟多线程

其他操作系统有tcb和多个pcb线程结构体

Linux将轻量级线程封装成其他操作系统多线程一样的接口

线程的优点

  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  • 线程占用的资源要比进程少很多
  • 能充分利用多处理器的可并行数量
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

线程的缺点

  • 性能损失
  • 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
  • 健壮性降低
  • 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
  • 缺乏访问控制
  • 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
  • 编程难度提高
  • 编写与调试一个多线程程序比单线程程序困难得多

线程异常

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出

线程用途

合理的使用多线程,能提高 CPU 密集型程序的执行效率
合理的使用多线程,能提高 IO 密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)

进程和线程

  • 进程是资源分配的基本单位
  • 线程是调度的基本单位
  • 线程共享进程数据,但也拥有自己的一部分数据:
  •           线程ID
  •           一组寄存器(硬件上下文数据--线性可以动态运行)
  •           栈(线程运行时生成的各种临时变量,临时变量会被每个线程保存在自己的栈区)
  •           errno
  •           信号屏蔽字
  •           调度优先级
进程的多个线程共享 同一地址空间 , 因此 Text Segment Data Segment 都是共享的 , 如果定义一个函数 , 在各线程中都可以调用, 如果定义一个全局变量 , 在各线程中都可以访问到 , 除此之外 , 各线程还共享以下进程资源和环境 :
  • 文件描述符表
  • 每种信号的处理方式(SIG_ IGNSIG_ DFL或者自定义的信号处理函数)
  • 当前工作目录
  • 用户id和组id
进程和线程的关系如下图:

Linux线程控制

POSIX线程库

  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
  • 要使用这些函数库,要通过引入头文<pthread.h>
  • 链接这些线程函数库时要使用编译器命令的“-lpthread”选项

创建线程

功能:创建一个新的线程
原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine)(void*), void *arg);
参数
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
错误检查:
传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误, 建议通过返回值判定,因为读取返回值要比读取线程内的errno变量的开销更小

#include <iostream>
#include <unistd.h>
#include <ctime>

int gval = 100;

void fun()
{
}

// 新线程
// 0x1111~0x2222
void *threadStart(void *args)
{
    while (true)
    {
        sleep(1);

        // int x = rand() % 5;

        std::cout << "new thread running..." << ", pid: " << getpid()
                  << ", gval: " << gval << ", &gval: " << &gval << std::endl;
        fun();
        // if(x == 0)
        // {
        //     int *p = nullptr;
        //     *p = 100; // 野指针
        // }
    }
}
// 0x3333~0x4444
int main()
{
    srand(time(nullptr));

    pthread_t tid1;
    pthread_create(&tid1, nullptr, threadStart, (void *)"thread-new");

    pthread_t tid2;
    pthread_create(&tid2, nullptr, threadStart, (void *)"thread-new");

    pthread_t tid3;
    pthread_create(&tid3, nullptr, threadStart, (void *)"thread-new");
    // 主线程
    while (true)
    {
        std::cout << "main thread running..." << ", pid: " << getpid()
                  << ", gval: " << gval << ", &gval: " << &gval << std::endl;

        gval++; // 修改!
        sleep(1);
    }
    return 0;
}

查看线程的指令

ps - aL

PID是进程号,LWP是线程号

编译时要带库

等待线程

 #include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

  pthread_t thread: 被连接线程的线程号
  void **retval   : 二级指针,输出型参数,指向一个指向被连接线程的返回码的一级指针

  返回值 : 0代表成功。 失败,返回的则是错误号。

pthread_join()即是子线程合入主线程,主线程阻塞等待子线程结束,然后回收子线程资源

当调用 pthread_join() 时,当前线程会处于阻塞状态,直到被调用的线程结束后,当前线程才会重新开始执行。当 pthread_join() 函数返回后,被调用线程才算真正意义上的结束,它的内存空间也会被释放(如果被调用线程是非分离的)。这里有三点需要注意:

  • 被释放的内存空间仅仅是系统空间,你必须手动清除程序分配的空间,比如 malloc() 分配的空间
  • 一个线程只能被一个线程所连接。
  • 被连接的线程必须是非分离的,否则连接会出错。

所以可以看出pthread_join()有两种作用:

  • 用于等待其他线程结束:当调用 pthread_join() 时,当前线程会处于阻塞状态,直到被调用的线程结束后,当前线程才会重新开始执行。
  • 对线程的资源进行回收:如果一个线程是非分离的(默认情况下创建的线程都是非分离)并且没有对该线程使用 pthread_join() 的话,该线程结束后并不会释放其内存空间,这会导致该线程变成了“僵尸线程”。

退出线程

 #include <pthread.h>
void pthread_exit(void *retval);

参数:retval表示线程退出状态,通常传NULL

作用:将单个线程退出。

注意几点:

  • return的作用是返回到函数的调用点,如果是main函数中的return,则代表该进程结束,并释放进程地址空间,所有线程都终止。对于其它函数的return,则直接返回到函数的调用点。exit和_exit函数会直接终止整个进程,导致所有线程结束。pthread_exit函数则会导致调用该函数的线程结束。所以,多线程环境中,应尽量少用,或者不使用exit函数,取而代之使用pthread_exit函数,将单个线程退出。任何线程里exit导致进程退出,其他线程也结束,主控线程退出时不能return或exit。
  • 另注意,pthread_exit或者return返回的指针(线程执行的函数用return或者pthread_exit结束线程时)所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了,此时退出的这个线程函数所占据的栈空间可能又会被重新分配出去,因此其他线程再次使用这个返回的地址没有意义。

杀死线程

 #include <pthread.h>
pthread_cancel(pthread_t thread);

杀死一个线程,对应进程中的kill函数;成功返回0,失败返回错误码;

注意:线程的取消并不是实时的,需要一个取消点,而这个取消点一般是一个系统调用函数,如果线程之中没有这个取消点,可以调用pthread_testcancel函数自行设置一个取消点

分离线程

 #include <pthread.h>
int pthread_detach(pthread_t thread);

功能:即主线程与子线程分离,子线程结束后,资源自动回收

pthread_join()函数的替代函数,可回收创建时detachstate属性设置为PTHREAD_CREATE_JOINABLE的线程的存储空间。该函数不会阻塞父线程。pthread_join()函数用于只是应用程序在线程tid终止时回收其存储空间。如果tid尚未终止,pthread_detach()不会终止该线程。当然pthread_detach(pthread_self())也是可以的。

返回值:pthread_detach() 在调用成功完成之后返回零。其他任何返回值都表示出现了错误。如果检测到以下任一情况,pthread_detach()将失败并返回相应的值。

  • EINVAL:tid是分离线程
  • ESRCH:tid不是当前进程中有效的为分离线程
 #include <pthread.h>
pthread_t pthread_self(void);

线程可以通过调用pthread_self函数获得自身线程标识。

pthread_detach(pthread_self());
使线成分离出来。当这个线程执行完成任务后释放释放资源。不然它会保留退出状态,等待别人来取。
      pthread_detach(threadid)函数的功能是使线程ID为threadid的线程处于分离状态,一旦线程处于分离状态,该线程终止时底 层资源立即被回收;否则终止子线程的状态会一直保存(占用系统资源)直到主线程调用pthread_join(threadid,NULL)获取线程的退出状态。通常是主线程使用pthread_create()创建子线程以后,一般可以调用pthread_detach(threadid)分离刚刚创建的子线程,这里的threadid是指子线程的threadid;如此以来,该子线程止时底层资源立即被回收;被创建的子线程也可以自己分离自己,子线程调用pthread_detach(pthread_self())就是分离自己,因为pthread_self()这个函数返回的就是自己本身的线程ID;
 

相关问题探究

问题1: main 主线程和 新线程谁先运行?不确定

问题2: 我们期望谁最后退出? main thread,你如何保证呢?

用pthread_join主线程等待回收来保证。 不join呢?会造成类似僵尸进程的问题

问题3: tid 是什么样子的?是什么呢?虚拟地址! 为什么?

问题4:全面看待线程函数传参: 我们可以传递任意类型,但你一定要能想得起来,也可以传递类对象的地址!!!

问题5: 全面看待线程函数返回:

a. 只考虑正确的返回,不考虑异常,因为异常了,整个进程就崩溃了,包括主线程

b. 我们可以传递任意类型,但你一定要能想得起来,也可以传递类对象的地址!!!

问题6: 如何创建多线程呢?

问题7: 新线程如何终止?

1. 线程函数 return

2. pthread_exit

3. main thread call pthread_cancel, 新线程退出结果是-1

问题8: 可以不可以不join线程,让他执行完就退出呢??可以!

a. 一个线程被创建,默认是joinable的,必须要被join的

b. 如果一个线程被分离,线程的工作状态分离状态,不需要/不能被join的. 依旧属于进程内部,但是不需要被等待了,该线程崩溃时整个进程也会崩溃。

主线程结束,子线程是否也强制结束?

#include <iostream>
#include <string>
#include <vector>
#include <thread>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

// // 可以给线程传递多个参数,甚至方法了
// // class ThreadData
// // {
// // public:
// //     int Excute()
// //     {
// //         return x + y;
// //     }
// // public:
// //     std::string name;
// //     int x;
// //     int y;
// //     // other
// // };

// // class ThreadResult
// // {
// // public:
// //     std::string print()
// //     {
// //         return std::to_string(x) + "+" + std::to_string(y) + "=" + std::to_string(result);
// //     }
// // public:
// //     int x;
// //     int y;
// //     int result;
// // };

// // // 问题5: 全面看待线程函数返回: 
// // // a. 只考虑正确的返回,不考虑异常,因为异常了,整个进程就崩溃了,包括主线程 
// // // b. 我们可以传递任意类型,但你一定要能想得起来,也可以传递类对象的地址!!!
// // void *threadRun(void *args)
// // {
// //     //std::string name = (const char*)args;
// //     //int a = *(int*)args; // warning
// //     ThreadData *td = static_cast<ThreadData*>(args); // (ThreadData*)args
// //     ThreadResult *result = new ThreadResult();
// //     int cnt = 10;
// //     while(cnt)
// //     {
// //         sleep(3); 
// //         std::cout << td->name << " run ..." << ", cnt: " << cnt-- << std::endl;
// //         result->result = td->Excute();
// //         result->x = td->x;
// //         result->y = td->y;
// //         break;
// //         // int *p = nullptr;
// //         // *p = 100; // 故意野指针
// //     }
// //     delete td;
// //     return (void*)result;
// // }

// std::string PrintToHex(pthread_t &tid)
// {
//     char buffer[64];
//     snprintf(buffer, sizeof(buffer), "0x%lx", tid);
//     return buffer;
// }

// const int num = 10;

// void *threadrun(void *args)
// {
//     // pthread_detach(pthread_self());
//     std::string name = static_cast<const char*>(args);
//     while(true)
//     {
//         std::cout << name << " is running" << std::endl;
//         sleep(3);
//         // int *p = nullptr;
//         // *p = 100;
//         break;
//     }
//     // return args;
//     // exit(1); // 进程: 专门用来终止进程的,不能用来终止线程!
//     pthread_exit(args); // 专门终止一个线程的!
// }

// // main函数结束,main thread结束,表示进程结束!
// int main()
// {
//     // 问题7: 新线程如何终止?
//     // 1. 线程函数 return
//     // 2. pthread_exit
//     // 3. main thread call pthread_cancel, 新线程退出结果是-1

//     // 问题8: 可以不可以不join线程,让他执行完就退出呢??可以!
//     // a. 一个线程被创建,默认是joinable的,必须要被join的.
//     // b. 如果一个线程被分离,线程的工作状态分离状态,不须要/不能被join的. 依旧属于进程内部,但是不需要被等待了

//     // std::vector<pthread_t> tids;
//     // // 问题6: 如何创建多线程呢?
//     // for(int i = 0; i < num; i++)
//     // {
//     //     // 1. 有线程的id
//     //     pthread_t tid;
//     //     // 2. 线程的名字
//     //     char *name = new char[128];
//     //     snprintf(name, 128, "thread-%d", i+1);
//     //     pthread_create(&tid, nullptr, threadrun, /*线程的名字*/name);

//     //     // 3. 保存所有线程的id信息 --- base
//     //     // tids.push_back(tid);
//     //     tids.emplace_back(tid);
//     // }

//     // for (auto tid : tids)
//     // {
//     //     pthread_detach(tid); // 主线程分离新线程,新线程必须存在
//     // }
    

//     // // //做我的事情
//     // // while(true)
//     // // {
//     // //     sleep(1);
//     // // }
//     // // // join todo
//     // for(auto tid : tids)
//     // {
//     //     // pthread_cancel(tid); // 取消
//     //     // std::cout << "cancel: " << PrintToHex(tid) << std::endl;
//     //     void *result = nullptr; // 线程被取消线程的退出结果是:-1 #define PTHREAD_CANCELED ((void *) -1)
//     //     int n = pthread_join(tid, &result);
//     //     // std::cout << PrintToHex(tid) << " quit... "<< std::endl;
//     //     std::cout << (long long int)result << " quit..., n : " << n << std::endl;
//     //     // delete (const char*)name;
//     // }
//     // sleep(100);

//     // pthread_t tid; // unsigned long int
//     // // 问题1: main 和 new 线程谁先运行?不确定
//     // // 问题4:全面看待线程函数传参: 我们可以传递任意类型,但你一定要能想得起来,也可以传递类对象的地址!!!
//     // ThreadData *td = new ThreadData();
//     // td->name = "thread-1";
//     // td->x = 10;
//     // td->y = 20;

//     // int n = pthread_create(&tid, nullptr, threadRun, td);

//     // // td.name = "thread-2";
//     // // td.num = 2;
//     // // int n = pthread_create(&tid, nullptr, threadRun, (void*)&td);
//     // if(n != 0) // 后面我们暂时就关心了
//     // {
//     //     std::cerr << "create thread error" << std::endl;
//     //     return 1;
//     // }

//     // // 问题3: tid 是什么样子的?是什么呢?虚拟地址! 为什么? TODO
//     // std::string tid_str = PrintToHex(tid); // 我们按照16进行打印出来
//     // std::cout << "tid : " << tid_str << std::endl;


//     // std::cout << "main thread join begin..." << std::endl;
//     // // 问题2:我们期望谁最后退出? main thread,你如何保证呢?
//     // ThreadResult *result = nullptr; // 开辟了空间的!!!
//     // n = pthread_join(tid, (void**)&result); // join来保证。 不join呢?会造成类似僵尸进程的问题
//     // if(n == 0)
//     // {
//     //     std::cout << "main thread wait success, new thread exit code: " << result->print() << std::endl;
//     // }

//     // sleep(100);
//     return 0;
// }

C++11多线程封装

#include <iostream>
#include <string>
#include <vector>
#include <thread>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

void threadrun(std::string name, int num)
{
    while(num)
    {
        std::cout << name << " num : " << num<< std::endl;
        num--;
        sleep(1);
    }
}

int main()
{
    std::string name = "thread-1";
    std::thread mythread(threadrun, std::move(name), 10);
    while(true)
    {
        std::cout << "main thhread..." << std::endl;
        sleep(1);
    }
    mythread.join();
    return 0;
}

C++11的多线程本质:就是对原生线程库接口的封装

底层就是各操作系统的接口函数

编译时依旧需要加后缀 -pthread

相关推荐

  1. Linux线

    2024-06-06 19:52:03       52 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-06-06 19:52:03       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-06-06 19:52:03       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-06-06 19:52:03       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-06-06 19:52:03       20 阅读

热门阅读

  1. 小程序vant DropdownMenu 下拉菜单无法关闭

    2024-06-06 19:52:03       9 阅读
  2. Kotlin 委托

    2024-06-06 19:52:03       9 阅读
  3. 2024中国机器人开发大会

    2024-06-06 19:52:03       7 阅读
  4. 探索Sass:Web开发的强大工具

    2024-06-06 19:52:03       11 阅读
  5. Spark SQL内置函数

    2024-06-06 19:52:03       7 阅读
  6. 【Linux】批量恢复文件权限

    2024-06-06 19:52:03       9 阅读
  7. 在Spring Boot项目中使用Redisson实现延迟执行

    2024-06-06 19:52:03       8 阅读