自学内容网 自学内容网

Linux多线程

Linux线程概念

什么是线程

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

线程的优点

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

线程的缺点

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

线程异常

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

线程用途

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

Linux进程VS线程

进程和线程

  • 进程是资源分配的基本单位
  • 线程是调度的基本单位
  • 线程共享进程数据,但也拥有自己的一部分数据:
    • 线程ID
    • 一组寄存器
    • errno
    • 信号屏蔽字
    • 调度优先级

进程的多个线程共享

因为是在同一个地址空间,因此所谓的代码段(Text Segment)、数据段(Data Segment)都是共享的:

  • 如果定义一个函数,在各线程中都可以调用。

  • 如果定义一个全局变量,在各线程中都可以访问到。
    除此之外,各线程还共享以下进程资源和环境:

  • 文件描述符表。(进程打开一个文件后,其他线程也能够看到)

  • 每种信号的处理方式。(SIG_IGN、SIG_DFL或者自定义的信号处理函数)

  • 当前工作目录。(cwd)

  • 用户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 <stdio.h>
#include <pthread.h>
#include <unistd.h>

void* Routine(void* args)
{
char* msg = (char*)args;
while (1){
printf("I am %s\n", msg);
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, Routine, (void*)"thread 1");
while (1){
printf("I am main thread!\n");
sleep(2);
}
return 0;
}

可是却发生了报错,这是为什么呢,这里的英文描述大致意思就是没有找到pthread_create这个函数
在这里插入图片描述
原因就在于在编译时没有引入pthread线程库
在这里插入图片描述
在编译选项中引入线程库后编译,就没有问题了
在这里插入图片描述

在这里插入图片描述
那么会创建线程后,我们来检验一个现象,进程创建出来的线程崩溃,是否会引起进程的崩溃

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

using namespace std;


void *threadRun1(void *args)
{
    while (true)
    {
        sleep(1);
        cout << "t1 thread..." << getpid() << endl;
    }
}

void *threadRun2(void *args)
{
    char *s = "hello bit";
    while (true)
    {
        sleep(1);
        cout << "t2 thread..." << getpid() << endl;
        *s = 'H'; // 让这一个线程崩溃
    }
}

int main()
{
    pthread_t t1, t2, t3;

    pthread_create(&t1, nullptr, threadRun1, nullptr);
    pthread_create(&t1, nullptr, threadRun2, nullptr);

    while (true)
    {
        sleep(1);
        cout << "main thread..." << getpid() << endl;
    }
}

在这里插入图片描述在这里插入图片描述
这里的现象印证了上面多线程的缺点,健壮性降低 同时也得出一个结论

  • 多线程程序中,任何一个线程崩溃了,最后都会导致进程崩溃
    • 为什么?
    • ——系统角度,线程是进程的执行分支,线程干了,就是进程干了
    • ——信号角度,页表转换的时候MMU识别写入权限的,验证没有通过
    • MMU异常->OS识别->给进程发信号->Linux进程信号----信号是以进程为主的

下面还有一条结论:

  • 因为执行流看到的资源是通过地址空间看到的,多个LWP(轻量级线程)看到的是同一个地址空间,所以,所有的线程可能会共享进程的大部分资源!
    这里在上面用例的基础上用一个全局变量来验证多个线程对变量的访问,线程1和主线程负责打印,线程2负责修改
#include <iostream>
#include <pthread.h>
#include <unistd.h>

using namespace std;

int g_val = 0; // 全局变量,在多线程场景中,我们多个线程看到的是同一个变量! -- 为什么?

void *threadRun1(void *args)
{
    while (true)
    {
        sleep(1);
        cout << "t1 thread..." << getpid() << " &g_val: " << &g_val << " , g_val: " << g_val << endl;
    }
}

void *threadRun2(void *args)
{
    // char *s = "hello bit";
    while (true)
    {
        sleep(1);
        cout << "t2 thread..." << getpid()  << " &g_val: " << &g_val << " , g_val: " << g_val++ << endl;
        // *s = 'H'; // 让这一个线程崩溃
    }
}

int main()
{
    pthread_t t1, t2, t3 

    pthread_create(&t1, nullptr, threadRun1, nullptr);
    pthread_create(&t1, nullptr, threadRun2, nullptr);

    while (true)
    {
        sleep(1);
        cout << "main thread..." << getpid()  << " &g_val: " << &g_val << " , g_val: " << g_val << endl;
    }
}

在这里插入图片描述
一个线程修改,其他线程也能看到,正因为如此,也体现了多线程的一个缺点 缺乏访问控制

线程等待

为什么需要线程等待?

  • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
  • 创建新的线程不会复用刚才退出线程的地址空间
功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码

调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的
终止状态是不同的,总结如下:

    1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
    1. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数
      PTHREAD_ CANCELED。
    1. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参
      数。
    1. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
      在这里插入图片描述

线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

  1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
  2. 线程可以调用pthread_ exit终止自己。
  3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。

pthread_exit函数

功能:线程终止
原型
void pthread_exit(void *value_ptr);
参数
value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函
数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

pthread_cancel函数

功能:取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值:成功返回0;失败返回错误码

线程分离

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放
    资源,从而造成系统泄漏。
  • 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线
    程资源。

int pthread_detach(pthread_t thread);
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
pthread_detach(pthread_self());
joinable和分离是冲突的,一个线程不能既是joinable又是分离的。


原文地址:https://blog.csdn.net/m0_70289867/article/details/136355027

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!