【Linux】Linux之多线程1

一.线程

1.什么是线程

在一个程序里的一个执行路线叫做线程(thread),更准确的定义是:线程是一个进程内部的控制序列。

一切进程至少都有一个执行线程。
线程在进程内部运行,本质是在进程地址空间内运行。
在 Linux 系统中,在 CPU 眼中,看到的 PCB 都要比传统的进程更加轻量化。
透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。

2.线程的优点

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

3.线程的缺点

性能损失

一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。

健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些 OS 函数会对整个进程造成影响
编程难度提高

4.线程异常

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

5.线程用途

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

二.Linux进程VS线程

1.线程和进程

进程是资源分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据
说明:

线程之间能够方便,快速的共享信息,只需将数据复制到共享(全局或堆)变量中即可。

创建线程比创建进程通常要快十倍甚至更多,线程间是共享虚拟地址空间的,无需采用写时复制内存,也无需复制页表。

线程 ID
一组寄存器

errno
信号屏蔽字
调度优先级
进程的多个线程共享 同一地址空间 , 因此 Text Segment 、 Data Segment 都是共享的 , 如果定义一个函数 , 在各线程中都可以调用, 如果定义一个全局变量 , 在各线程中都可以访问到 , 除此之外 , 各线程还共享以下进程资源和环境:


进程和线程的关系如下图:

三.Linux线程控制

1.posix线程库

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

2.创建线程

cpp 复制代码
int pthread_create(pthread_t *thread, const pthread_attr_t, 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 变量的开销更小

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>

using namespace std;

void *printf(void *arg)
{
    std::cout << "child thread : " << getpid() << endl;
}

int main()
{
    pthread_t pid;
    int ret = pthread_create(&pid, NULL, printf, NULL);
    if (ret != 0)
    {
        std::cout << "create failed!" << endl;
    }
    while (1)
    {
        std::cout << "I am main thread!" << endl;
        sleep(10);
        }
    return 0;
}

3.线程ID及进程地址空间布局

pthread_ create 函数会产生一个线程 ID ,存放在第一个参数指向的地址中。该线程 ID 和前面说的线程 ID不是一回事。
前面讲的线程 ID 属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
pthread_ create 函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程 ID ,属于NPTL 线程库的范畴。线程库的后续操作,就是根据该线程 ID 来操作线程的
线程库 NPTL 提供了 pthread_ self 函数,可以获得线程自身的 ID:

cpp 复制代码
pthread_t pthread_self(void);

pthread_t 到底是什么类型呢?取决于实现。对于 Linux 目前实现的 NPTL 实现而言, pthread_t 类型的线程 ID ,本质就是一个进程地址空间上的一个地址。

4.线程终止

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

  1. 从线程函数 return 。这种方法对主线程不适用 , 从 main 函数 return 相当于调用 exit 。
  2. 线程可以调用 pthread_ exit 终止自己。
  1. 一个线程可以调用 pthread_ cancel 终止同一进程中的另一个线程
    pthread_exit函数
cpp 复制代码
void pthread_exit(void *value_ptr);

功能:终止线程

参数:

value_ptr:value_ptr不要指向一个局部变量

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

cpp 复制代码
int pthread_cancel(pthread_t thread);

功能:取消一个执行中的线程

参数:

thread:线程ID

返回值:成功返回0;失败返回错误码

5.线程等待

为什么需要线程等待????

已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
创建新的线程不会复用刚才退出线程的地址空间。

cpp 复制代码
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 线程函数的返回值。
  2. 如果 thread 线程被别的线程调用 pthread_ cancel 异常终掉 ,value_ ptr 所指向的单元里存放的是常数PTHREAD_ CANCELED。
  3. 如果 thread 线程是自己调用 pthread_exit 终止的 ,value_ptr 所指向的单元存放的是传给 pthread_exit 的参数。
  1. 如果对 thread 线程的终止状态不感兴趣 , 可以传 NULL 给 value_ ptr 参数。
cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

using namespace std;

void *thread1(void *arg)
{
    cout << "thread 1 returning ..." << endl;
    int *p = new int;
    *p = 1;
    return (void *)p;
}

void *thread2(void *arg)
{
    cout << "thread 2 exiting ..." << endl;
    int *p = new int;
    *p = 2;
    pthread_exit((void *)p);
}

void *thread3(void *arg)
{
    while (1)
    {
        cout << "thread 3 running ..." << endl;
        sleep(2);
    }
}

int main()
{
    pthread_t tid;
    void *ret;

    pthread_create(&tid, NULL, thread1, NULL);
    pthread_join(tid, &ret);

    cout << "thread return, thread id is " << tid << "return code : " << *(int *)ret << endl;
    free(ret);

    pthread_create(&tid, NULL, thread2, NULL);
    pthread_join(tid, &ret);

    cout << "thread return, thread id is " << tid << "return code : " << *(int *)ret << endl;
    free(ret);

    pthread_create(&tid, NULL, thread3, NULL);
    sleep(3);
    pthread_cancel(tid);
    pthread_join(tid, &ret);
    if (ret == PTHREAD_CANCELED)
    {
        cout << "thread return, thread id is " << tid << "return code : PTHREAD_CANCELED" << endl;
    }
    else
    {
        cout << "thread return, thread id is " << tid << "return code : NULL" << endl;
    }
    return 0;
}

运行结果:

6.分离线程

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

cpp 复制代码
int pthread_detach(pthread_t thread);

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离 :

cpp 复制代码
pthread_detach(pthread_self());

joinable和分离是冲突的,一个线程不能既是joinable又是分离的。

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>

using namespace std;

void *thread_run(void *arg)
{
    pthread_detach(pthread_self());
    cout << (char *)arg << endl;
    return NULL;
}

int main()
{
    pthread_t tid;
    if (pthread_create(&tid, NULL, thread_run, (void *)"thread1 run ...") != 0)
    {
        cout << "create error!" << endl;
        return 1;
    }
    int ret = 0;
    sleep(1); // 很重要,要让线程先分离,再等待

    if (pthread_join(tid, NULL) == 0)
    {
        printf("pthread wait success\n");
        ret = 0;
    }
    else
    {
        printf("pthread wait failed\n");
        ret = 1;
    }
    return ret;
}

运行结果:

注意:pthread_create()函数的第四个参数是void*类型的,而"pthread run ... "是const char* 类型,需要进行类型转换,才能进行编译运行。

相关推荐
逐雨~28 分钟前
9.8C++作业
开发语言·c++
我爱挣钱我也要早睡!1 小时前
Java 复习笔记
java·开发语言·笔记
磊灬泽3 小时前
【日常错误】鼠标无反应
linux·windows
知识分享小能手4 小时前
React学习教程,从入门到精通, React 属性(Props)语法知识点与案例详解(14)
前端·javascript·vue.js·学习·react.js·vue·react
汇能感知6 小时前
摄像头模块在运动相机中的特殊应用
经验分享·笔记·科技
阿巴Jun6 小时前
【数学】线性代数知识点总结
笔记·线性代数·矩阵
茯苓gao7 小时前
STM32G4 速度环开环,电流环闭环 IF模式建模
笔记·stm32·单片机·嵌入式硬件·学习
是誰萆微了承諾7 小时前
【golang学习笔记 gin 】1.2 redis 的使用
笔记·学习·golang
利刃大大7 小时前
【高并发内存池】五、页缓存的设计
c++·缓存·项目·内存池
DKPT7 小时前
Java内存区域与内存溢出
java·开发语言·jvm·笔记·学习