【Linux取经路】初识线程——线程控制

文章目录

  • 一、什么是线程?
    • 1.1 Linux 中线程该如何理解?
    • 1.2 如何理解把资源分配给线程?
      • 1.2.1 虚拟地址到物理地址的转换
    • 1.3 线程 VS 进程
      • 1.3.1 线程为什么比进程更轻量化?
      • 1.3.2 线程的优点
      • 1.3.3 线程缺点
      • 1.3.4 线程异常
      • 1.3.5 线程用途
      • 1.3.6 线程和进程
  • 二、线程控制
    • 2.1 pthread_create——创建一个线程
    • 2.2 全局变量在线程间是共享的
    • 2.3 pthread_join——线程等待
    • 2.4 pthread_exit——终止一个线程
    • 2.5 pthread_cancel——取消一个线程
    • 2.6 全局函数可以被多个线程同时调用
    • 2.7 线程函数的参数和返回值可以传递对象
    • 2.8 C++11 的线程库
    • 2.9 pthread_self——获取线程ID
    • 2.10 再来理解 `pthread` 线程库
    • 2.11 创建一批线程
    • 2.12 验证——每个线程都有自己独立的栈结构
    • 2.13 线程之间没有秘密
    • 2.14 线程的局部存储
    • 2.15 pthread_detach——线程分离
  • 三、结语

在这里插入图片描述

一、什么是线程?

线程是进程内的一个执行分支,线程的执行粒度,要比进程细。

1.1 Linux 中线程该如何理解?

地址空间是进程的资源窗口。Linux 中,线程在进程”内部“执行,即线程在进程的地址空间内运行,任何执行流要执行,都要有资源(代码,数据,CPU资源);在 Linux 中,线程的执行粒度要比进程更细,即线程执行进程代码的一部分;在 Linux 中,复用进程数据结构和管理算法来描述和组织线程Linux 中没有真正意义上的线程,即在 Linux 中没有为线程创建独属于自己的 TCB 结构体thread ctrl block) ,而是用”进程“的内核数据结构(PCB)模拟的线 程;CPU 只有执行流的概念,所以从原则上来说,CPU 是不区分进程和线程的,但是 Linux 操作系统要区分进程和线程;我们把 Linux 中的执行流叫做轻量级进程

线程:我们认为线程是操作系统调度的基本单位。

进程:进程是承担分配系统资源(线程(执行流资源)、地址空间、页表、物理内存)的基本实体。

1.2 如何理解把资源分配给线程?

1.2.1 虚拟地址到物理地址的转换

image-20240311181055099

站在地址空间角度,线程分配资源本质就是分派地址空间范围。

1.3 线程 VS 进程

1.3.1 线程为什么比进程更轻量化?

  • 创建释放更加轻量化,创建线程只需要常见 PCB 对象就行。(生死)
  • 切换更加轻量化(运行)。同一个进程内的多个线程在切换的时候,不需要更新 CPU 中的 cache 缓存、进程地址空间、页表等。只需要更新少量的上下文数据。

创建线程不能给该线程重新申请时间片,而是将线程的时间片划分部分给线程。

在这里插入图片描述

cat /proc/cpuinfo:查看 CPU 的信息。

1.3.2 线程的优点

  • 创建一个新线程的代价要比创建一个新进程小的多。

  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。

  • 线程占用的资源要比进程少很多。

  • 能充分利用多处理器的可并行数量。

  • 在等待慢速 I/O 操作结束的同时,程序可执行其他的计算任务。

  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。

  • I/O 密集型应用,为了提高性能,将 I/O 操作重叠。线程可以同时等待不同的 I/O 操作。

1.3.3 线程缺点

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

  • 健壮性降低:编写多线程需要更全面深入的考虑,在一个线程程序里,因时间分配上的细微偏差或因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说就是线程之间缺乏安全保护。

  • 缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些 OS 函数会对整个进程造成影响。

  • 编程难度提高:编写与调试一个多线程程序比单线程程序困难的多。

1.3.4 线程异常

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。

  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。

1.3.5 线程用途

  • 合理的使用多线程,能提高 CPU 密集型程序的执行效率。

  • 合理的使用多线程,能提高 I/O 密集型程序的用户体验(如一边写代码一边下载开发工具,就是多线程运行的一种表现)

1.3.6 线程和进程

  • 进程是资源分配的基本单位。

  • 线程是调度的基本单位。

  • 线程共享进程数据,但也拥有自己的一部分数据:

    • 线程 ID

    • 一组寄存器(线程的上下文)

    • errno

    • 信号屏蔽子

    • 调度优先级

进程的多个线程之间共享同一地址空间,因此代码段、数据段都是共享的,如果定义一个函数,在各线程中都可以的调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各个线程还共享以下进程资源和环境:

  • 文件描述符表
  • 每种信号的处理方式(SIG_IGNSIG_DFL 或者自定义的信号处理函数)
  • 当前工作目录
  • 用户 id 和组 id

image-20240311215931222

二、线程控制

Linux 的内核中,没有很明确的线程概念,只有轻量级进程的概念。所以 Linux 操作系统不会给我们直接提供线程的系统调用,只会给我们提供轻量级进程的系统调用。伟大的 Linux 程序员将轻量级进程的接口进行封装,给用户在应用层开发出来了一个 pthread 线程库。几乎所有的 Linux 平台都是默认自带这个库的,Linux 中编写多线程代码,需要使用第三方 pthread 库。

2.1 pthread_create——创建一个线程

#include <pthread.h>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;创建失败返回对应的错误码

#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;void *threadRoutine(void *args)
{while(true){cout << "new thread, pid: " << getpid() << endl;sleep(2);}
}int main()
{pthread_t pid;pthread_create(&pid, nullptr, threadRoutine, nullptr);while(true){cout << "main thread, pid: " << getpid() << endl;sleep(1);}return 0;
}

image-20240312165319396

在编译线程代码的时候,需要加上 -lpthread 选项。因为 pthread.h 是第三方库,但是 g++ 编译器仅仅可以找到 pthread.h 和该库的位置,并不会默认帮我们去链接 pthread 库,因此我们要加 -lpthread 选项,告诉 g++ 编译器,我们要链接这个库。

image-20240312171343716

ps -aL:其中 L 表示查看当前操作系统中的所有轻量级线程。

image-20240312171751985

  • LWP:一个轻量级进程的 ID,CPU 是按照 LWP 来进行调度的。

CPU 调度的基本单位是线程,PID == LWP 的线程叫做主线程

2.2 全局变量在线程间是共享的

#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;int g_val = 100;void *threadRoutine(void *args)
{const char *name = (const char*)args;while (true){printf("%s, pid: %d, g_val: %d, &g_val: 0X%p\n", name, getpid(), g_val, &g_val);sleep(1);}
}int main()
{pthread_t pid;pthread_create(&pid, nullptr, threadRoutine, (void *)"Thread 1");while (true){printf("main thread, pid: %d, g_val: %d, &g_val: 0X%p\n", getpid(), g_val, &g_val);sleep(1);g_val++;}return 0;
}

image-20240312222311854

2.3 pthread_join——线程等待

#include <pthread.h>int pthread_join(pthread_t thread, void **retval);
  • thread:要等待的线程 ID
  • retval:输出型参数,获取线程函数的返回值
  • **返回值:**等待成功0被返回;等待失败,错误码被返回

image-20240312230358310

线程等待的目的:

  • 防止新线程内存泄露

  • 主线程获取子线程的执行结果

#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;int g_val = 100;void *threadRoutine(void *args)
{const char *name = (const char*)args;int cnt = 5;while (true){printf("%s, pid: %d, g_val: %d, &g_val: 0X%p\n", name, getpid(), g_val, &g_val);sleep(1);cnt--;if(cnt == 0) break;}return (void *)100;
}int main()
{pthread_t pid;pthread_create(&pid, nullptr, threadRoutine, (void *)"Thread 1");// while (true)// {//     printf("main thread, pid: %d, g_val: %d, &g_val: 0X%p\n", getpid(), g_val, &g_val);//     sleep(1);//     g_val++;// }void *ret;pthread_join(pid, &ret);cout << "main thread quit..., Thread 1 return val: " << (long long int)ret << endl;return 0;
}

image-20240313083537658

线程执行完它的函数后就退出了,主线程在等待的时候,默认是阻塞等待。主线程等待子线程,只能获取到子线程执行函数的返回值,不考虑子线程出异常,因为子线程一旦出异常,主线程也会跟着遭殃。

exit 是用来终止进程的,不能用来直接终止线程。任何一个子线程在任何地方调用 exit 都表示整个进程退出。

2.4 pthread_exit——终止一个线程

pthread_exit:终止调用该函数的线程

#include <pthread.h>void pthread_exit(void *retval);
  • retval :线程函数的返回值
#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;int g_val = 100;void *threadRoutine(void *args)
{const char *name = (const char*)args;int cnt = 5;while (true){printf("%s, pid: %d, g_val: %d, &g_val: 0X%p\n", name, getpid(), g_val, &g_val);sleep(1);cnt--;if(cnt == 0) break;}pthread_exit((void *)200);return (void *)100;
}int main()
{pthread_t pid;pthread_create(&pid, nullptr, threadRoutine, (void *)"Thread 1");void *ret;pthread_join(pid, &ret);cout << "main thread quit..., Thread 1 return val: " << (long long int)ret << endl;return 0;
}

image-20240313084734928

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

2.5 pthread_cancel——取消一个线程

pthread_cancel:取消一个已存在的目标线程。

#include <pthread.h>int pthread_cancel(pthread_t thread);
  • thread:要取消的进程 ID

  • 返回值:取消成功返回0;取消失败,对应的错误码被返回

#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;int g_val = 100;void *threadRoutine(void *args)
{const char *name = (const char*)args;int cnt = 5;while (true){printf("%s, pid: %d, g_val: %d, &g_val: 0X%p\n", name, getpid(), g_val, &g_val);sleep(1);cnt--;if(cnt == 0) break;}pthread_exit((void *)200);return (void *)100;
}int main()
{pthread_t pid;pthread_create(&pid, nullptr, threadRoutine, (void *)"Thread 1");sleep(1);pthread_cancel(pid);void *ret;pthread_join(pid, &ret);cout << "main thread quit..., Thread 1 return val: " << (long long int)ret << endl;return 0;
}

image-20240313085650274

一个进程被取消,它的返回值是 PTHREAD_CANCELED 一个宏:

image-20240313085905095

2.6 全局函数可以被多个线程同时调用

#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;int g_val = 100;void Print(const string &name)
{printf("%s is running, pid: %d, g_val: %d, &g_val: 0X%p\n", name.c_str(), getpid(), g_val, &g_val);
}void *threadRoutine(void *args)
{const char *name = (const char*)args;int cnt = 5;while (true){Print(name);// 调用全局函数sleep(1);cnt--;if(cnt == 0) break;}pthread_exit((void *)200);return (void *)100;
}int main()
{pthread_t pid;pthread_create(&pid, nullptr, threadRoutine, (void *)"Thread 1");int cnt = 0;while (true){Print("main thread");// 调用全局函数sleep(1);g_val++;cnt++;if(cnt == 10) break;}void *ret;pthread_join(pid, &ret);cout << "main thread quit..., Thread 1 return val: " << (long long int)ret << endl;return 0;
}

image-20240313090718302

这说明代码区对所有的线程来说是共享的。

2.7 线程函数的参数和返回值可以传递对象

一个求和任务

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>
#include <cstdlib>using namespace std;class Request
{
public:Request(int start, int end, const string &threadname):start_(start),end_(end),threadname_(threadname){}int sum(){int ret = 0;for(int i = start_; i <= end_; i++){cout << threadname_ << " is running..." << endl;ret += i;usleep(10000);}return ret;}public:int start_; // 起始数int end_; // 终止数string threadname_; // 线程的名字
};class Response
{
public:Response(int result, int exitcod):result_(result),exitcode_(exitcod){}
public:int result_; // 计算结果int exitcode_; // 标记结果的可靠性
};void *SumCount(void *args)
{Request *rq = static_cast<Request *>(args);Response *rp = new Response(rq->sum(), 0);delete rq;return rp;
}int main()
{pthread_t tid;// 创建一个线程Request *rq = new Request(1, 100, "Thread 1");pthread_create(&tid, nullptr, SumCount, rq);void *ret;pthread_join(tid, &ret); // 线程等待,获取线程的返回值Response *rp = static_cast<Response *>(ret);cout << "result: " << rp->result_ << ", exitcode: " << rp->exitcode_ << endl;delete(rp);return 0;
}

image-20240313095804127

该实例证明了堆空间也是共享的。

2.8 C++11 的线程库

pthread.h 是原生线程库。C++11 的线程库本质上是封装了原生线程库。在 Linux 下,C++11 的线程库底层封装的是 Linux 的系统调用,在 Windows 下,C++11 底层封装的是 Windows 的系统调用。这也是 C++ 语言具有跨平台性的体现。如果代码中直接使用系统调用,那么就不具有跨平台性。

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <thread>using namespace std;void threadrun()
{while(true){cout << "I am a new thread for C++" << endl;sleep(1);}
}int main()
{thread th(threadrun);// 创建一个线程th.join();return 0;
}

image-20240313101642708

2.9 pthread_self——获取线程ID

返回调用该函数的线程 ID。

#include <pthread.h>pthread_t pthread_self(void);
  • 返回值:该函数始终会调用成功,返回调用该函数线程的 ID
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <thread>
#include <string>using namespace std;string toHex(int num) // 转十六进制接口
{char ret[64];snprintf(ret, sizeof(ret), "%p", num);return ret;
}void *threadroutine(void *args)
{while(true){sleep(2);cout << "thread id: " << toHex(pthread_self()) << endl;}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadroutine, nullptr);while(true){cout << "creat a new thread, id: " << toHex(tid) << endl;sleep(1);}pthread_join(tid, nullptr);return 0;
}

image-20240313103852295

2.10 再来理解 pthread 线程库

Linux 内核中没有很明确线程的概念,只有轻量级进程的概念,clone 接口就是用来创建一个轻量级进程。pthread_creat 底层就是封装了 clone

#include <sched.h>int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, .../* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );

image-20240313105433864

线程的概念是 pthread 线程库给我们提供的,我们在使用原生线程库的时候,g++ 默认使用动态链接,所以该库是会被加载到内存中的,然后通过页表映射到进程的共享区中,线程肯定不止一个,所以线程库一定要把当前操作系统中创建的所有线程管理起来,管理的方式就是通过先描述再组织,因此在线程库中一定存在一个描述线程的结构体,将该结构体称作 TCB,一个线程的 tid (上文中提到的线程 ID)就是其在线程库中的 TCB 对象的起始地址(改地址是一个虚拟地址)。这个 tid 是用户层面的,给用户来使用的,LWP 是内核中的概念,因为 CPU 调度的最小单位是线程(也就是轻量级进程),所以操作系统需要有一个编号来唯一标识一个线程。在Linux 中,我们所说的线程是用户级线程,因为在 Linux 中,线程的概念是 pthread 为我们提供的,在 Windows 中的线程是内核级线程,因为它是由操作系统直接提供的。在 Linux 中一个用户级线程对应一个内核级线程。

在这里插入图片描述

每个线程在被创建出来之后,都要有自己独立的栈结构,因为每个线程都有自己的调用链,执行流的本质就是调用链,该栈空间会保存一个执行流在运行过程中产生的临时变量,函数调用进行的入栈操作。主线程直接使用地址空间中为我们提供的栈结构即可,其他子线程的独立栈,都在共享区,具体来说是在 pthread 库中,tid 指向的 TCB 中维护了该线程的独立栈。

2.11 创建一批线程

#include <iostream>
#include <pthread.h>
#include <vector>
#include <string>
#include <unistd.h>using namespace std;#define NUM 10class ThreaInfo
{
public:ThreaInfo(const string &threadname):threadname_(threadname){}public:string threadname_;
};string toHex(pthread_t tid)
{char buffer[64];snprintf(buffer, sizeof(buffer), "%p", tid);return buffer;
}void *threadroutine(void *args)
{int i = 0;ThreaInfo *ti = static_cast<ThreaInfo*>(args);while(i < 10){cout << ti->threadname_.c_str() << " is running, tid: " << toHex(pthread_self()) << ", pid: " << getpid() << endl;i++;sleep(1);}return nullptr;
}int main()
{vector<pthread_t> tids;for(int i = 0; i < NUM; i++){pthread_t tid;ThreaInfo *ti = new ThreaInfo("Thread-"+to_string(i));pthread_create(&tid, nullptr, threadroutine, ti);tids.push_back(tid);sleep(1);}for(auto tid:tids){pthread_join(tid, nullptr);}return 0;
}

创建一批线程

2.12 验证——每个线程都有自己独立的栈结构

#include <iostream>
#include <pthread.h>
#include <vector>
#include <string>
#include <unistd.h>using namespace std;#define NUM 3class ThreaInfo
{
public:ThreaInfo(const string &threadname):threadname_(threadname){}public:string threadname_;
};string toHex(pthread_t tid)
{char buffer[64];snprintf(buffer, sizeof(buffer), "%p", tid);return buffer;
}void *threadroutine(void *args)
{int i = 0;int num = 0;ThreaInfo *ti = static_cast<ThreaInfo*>(args);while(i < 10){cout << ti->threadname_.c_str() << " is running, tid: " << toHex(pthread_self()) << ", pid: " << getpid()  << ", num: " << num << ", &num: " << toHex((pthread_t)&num) << endl;i++;num++;usleep(10000);}return nullptr;
}int main()
{vector<pthread_t> tids;for(int i = 0; i < NUM; i++){pthread_t tid;ThreaInfo *ti = new ThreaInfo("Thread-"+to_string(i));pthread_create(&tid, nullptr, threadroutine, ti);tids.push_back(tid);// sleep(1);usleep(1000);}for(auto tid:tids){pthread_join(tid, nullptr);}return 0;
}

image-20240313141716510

每个线程都去调用了 threadroutine 函数,但是每个线程都有自己的 num,都是从0开始,并且 num 的地址都不同。这正是因为每个线程都有自己独立的栈结构,每个线程在调用该函数时,都将该函数中的局部变量压入自己所在的栈空间。

2.13 线程之间没有秘密

虽然每一个线程都有自己独立的栈结构,但是对于同一个进程创建的多个线程来说,它们都是在该进程的地址空间中,所以只要你想,一个进程是可以拿到另一个线程栈中的数据。

定义一个全局的指针变量,让其指向线程1栈空间中的一个变量,这样就能在主线程中去获取子线程栈空间的数据

#include <iostream>
#include <pthread.h>
#include <vector>
#include <string>
#include <unistd.h>using namespace std;#define NUM 3int *p = nullptr;class ThreaInfo
{
public:ThreaInfo(const string &threadname):threadname_(threadname){}public:string threadname_;
};string toHex(pthread_t tid)
{char buffer[64];snprintf(buffer, sizeof(buffer), "%p", tid);return buffer;
}void *threadroutine(void *args)
{int i = 0;int num = 0;ThreaInfo *ti = static_cast<ThreaInfo*>(args);if(ti->threadname_ == "Thread-1") p = &num; // 将线程1中的 num 变量的地址存到 p 指针里面while(i < 10){cout << ti->threadname_.c_str() << " is running, tid: " << toHex(pthread_self()) << ", pid: " << getpid()  << ", num: " << num << ", &num: " << &num << endl;i++;num++;usleep(10000);}return nullptr;
}int main()
{vector<pthread_t> tids;for(int i = 0; i < NUM; i++){pthread_t tid;ThreaInfo *ti = new ThreaInfo("Thread-"+to_string(i));pthread_create(&tid, nullptr, threadroutine, ti);tids.push_back(tid);// sleep(1);usleep(1000);}cout << "main thread get Thread-1 num: " << *p << ", &num: " << p << endl;for(auto tid:tids){pthread_join(tid, nullptr);}return 0;
}

image-20240313143018887

虽然这样做可以,但是我们在代码中是禁止这样做的。

2.14 线程的局部存储

定义的普通全局变量是被所有线程所共享的,如果想让该全局变量在每个线程内部,各自私有一份,可以在定义全局变量的前面加上 __thread ,这并不是语言给我们提供的,而是编译器给我们提供。并且 __thread 只能用来修饰内置类型,不能用来修饰自定义类型。

#include <iostream>
#include <pthread.h>
#include <vector>
#include <string>
#include <unistd.h>using namespace std;#define NUM 3int *p = nullptr;__thread int val = 100;class ThreaInfo
{
public:ThreaInfo(const string &threadname):threadname_(threadname){}public:string threadname_;
};string toHex(pthread_t tid)
{char buffer[64];snprintf(buffer, sizeof(buffer), "%p", tid);return buffer;
}void *threadroutine(void *args)
{int i = 0;// int num = 0;ThreaInfo *ti = static_cast<ThreaInfo*>(args);// if(ti->threadname_ == "Thread-1") p = &num; // 将线程1中的 num 变量的地址存到 p 指针里面while(i < 10){cout << ti->threadname_.c_str() << " is running, tid: " << toHex(pthread_self()) << ", pid: " << getpid()  << ", val: " << val << ", &num: " << &val << endl;i++;// num++;val++;usleep(10000);}return nullptr;
}int main()
{vector<pthread_t> tids;for(int i = 0; i < NUM; i++){pthread_t tid;ThreaInfo *ti = new ThreaInfo("Thread-"+to_string(i));pthread_create(&tid, nullptr, threadroutine, ti);tids.push_back(tid);// sleep(1);usleep(1000);}// cout << "main thread get Thread-1 num: " << *p << ", &num: " << p << endl;for(auto tid:tids){pthread_join(tid, nullptr);}return 0;
}

image-20240313151304588

此时 val 虽然定义在全局,但实际上在每一个进程的独立栈中间中都会为 val 开辟一块空间,来进行存储。

2.15 pthread_detach——线程分离

  • 默认情况下,新创建的线程是 joinable 的,线程退出后,需要对其进行 pthread_join 操作,否则无法释放资源,从而造成资源泄露。
  • 如果主线程不关心子线程的返回值,join 是一种负担,这个时候,我们可以告诉操作系统,当线程退出时,自动释放线程资源。
#include <pthread.h>int pthread_detach(pthread_t thread);
  • thread:要分离的线程 ID

  • 返回值:分离成功返回0;失败错误码被返回

  • 小Tips:该函数可以由主线程来调用,也可以由子线程来调用。

#include <iostream>
#include <pthread.h>
#include <vector>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <cstring>using namespace std;#define NUM 3int *p = nullptr;// __thread int val = 100;class ThreaInfo
{
public:ThreaInfo(const string &threadname):threadname_(threadname){}public:string threadname_;
};string toHex(pthread_t tid)
{char buffer[64];snprintf(buffer, sizeof(buffer), "%p", tid);return buffer;
}void *threadroutine(void *args)
{int i = 0;// int num = 0;ThreaInfo *ti = static_cast<ThreaInfo*>(args);// if(ti->threadname_ == "Thread-1") p = &num; // 将线程1中的 num 变量的地址存到 p 指针里面while(i < 10){cout << ti->threadname_.c_str() << " is running, tid: " << toHex(pthread_self()) << ", pid: " << getpid() << endl;i++;// num++;// val++;usleep(10000);}return nullptr;
}int main()
{vector<pthread_t> tids;for(int i = 0; i < NUM; i++){pthread_t tid;ThreaInfo *ti = new ThreaInfo("Thread-"+to_string(i));pthread_create(&tid, nullptr, threadroutine, ti);tids.push_back(tid);// sleep(1);usleep(1000);}// cout << "main thread get Thread-1 num: " << *p << ", &num: " << p << endl;for(auto tid:tids){pthread_detach(tid); // 线程分离}for(auto tid:tids){int ret = pthread_join(tid, nullptr);printf("%p: ret: %d, messg: %s\n", tid, ret, strerror(ret));}return 0;
}

image-20240313151716109

子线程各自只执行了一次,是因为,子线程在被创建出来之后,主线程立即将所有的子线程进行了分离,然后,主线程又去进行 join,此时因为所有的子线程已经被分离了,主线程去 join 就不会阻塞等待了,而是直接出错返回,最后主线程执行完毕就直接退出了,主线程退出,也就意味着进程退出,进程退出所有的资源就要被释放,所有子线程赖以生存的环境也没了,所以子线程也就跟着没了。因此,线程分离后,要保证主线程最后退出,常见的情况是主线程跑死循环,一直不退出。

线程分离本质上是线程 TCB 中的一个属性,pthread_detach 本质上就是去修改改属性。

三、结语

今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,春人的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是春人前进的动力!

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/13517.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

关于基础的流量分析(1)

1.对于流量分析基本认识 1&#xff09;简介&#xff1a;网络流量分析是指捕捉网络中流动的数据包&#xff0c;并通过查看包内部数据以及进行相关的协议、流量分析、统计等来发现网络运行过程中出现的问题。 2&#xff09;在我们平时的考核和CTF比赛中&#xff0c;基本每次都有…

MySQL用户管理操作

用户权限管理操作 DCL语句 一.用户管理操作 MySQL软件内部完整的用户格式&#xff1a; 用户名客户端地址 admin1.1.1.1这个用户只能从1.1.1.1的客服端来连接服务器 admin1.1.1.2这个用户只能从1.1.1.2的客服端来连接服务器 rootlocal host这个用户只能从服务器本地进行连…

ACM实训

【碎碎念】继续搞习题学习&#xff0c;今天完成第四套的ABCD&#xff0c;为下一周挤出时间复习&#xff0c;加油 Digit Counting 问题 法希姆喜欢解决数学问题。但有时解决所有的数学问题对他来说是一个挑战。所以有时候他会为了解决数学难题而生气。他拿起一支粉笔&#xff…

Java面试八股之进程和线程的区别

Java进程和线程的区别 定义与作用&#xff1a; 进程&#xff1a;在操作系统中&#xff0c;进程是程序执行的一个实例&#xff0c;是资源分配的最小单位。每个进程都拥有独立的内存空间&#xff0c;包括代码段、数据段、堆空间和栈空间&#xff0c;以及操作系统分配的其他资源…

工厂模式(简单工厂模式+工厂模式)

工厂模式的目的就是将对象的创建过程隐藏起来&#xff0c;从而达到很高的灵活性&#xff0c;工厂模式分为三类&#xff1a; 简单工厂模式工厂方法模式抽象工厂模式 在没有工厂模式的时候就是&#xff0c;客户需要一辆马车&#xff0c;需要客户亲自去创建一辆马车&#xff0c;…

经验分享:C++ error:‘syscall’ was not declared in this scope

明明已经加了头文件 #include <sys/syscall.h>#define gettid() syscall(__NR_gettid)但是依旧不能使用 syscall() 函数&#xff0c; 检查源码后&#xff1a; sys/syscall.h 内部表示&#xff0c;他封装了 打开对应的 syscall.h 文件内部依旧没有 syscall()函数的声明…

使用docker+jenkins构建前端项目发布到nginx

1.准备环境 为了方便公司开发优化代码&#xff0c;不需要反复地将项目包发送给运维部署&#xff0c;我们对开发环境的前端项目利用jenkinsCI/CD进行自动化部署 需要两台服务器 一台jenkins 一台发布服务器,这里发布服务器 我直接使用开发环境的服务器 将admin界面与云计算展示…

全栈实现发送验证码注册账号 全栈开发之路——全栈篇(3)

全栈开发一条龙——前端篇 第一篇&#xff1a;框架确定、ide设置与项目创建 第二篇&#xff1a;介绍项目文件意义、组件结构与导入以及setup的引入。 第三篇&#xff1a;setup语法&#xff0c;设置响应式数据。 第四篇&#xff1a;数据绑定、计算属性和watch监视 第五篇 : 组件…

基于JAVA的Dubbo 实现的各种限流算法

在基于 Java 的 Dubbo 实现中&#xff0c;限流&#xff08;Rate Limiting&#xff09;同样是一个关键的需求。Dubbo 是阿里巴巴开源的一款高性能 Java RPC 框架&#xff0c;广泛应用于分布式服务架构中。实现限流可以帮助服务在高并发场景下保持稳定性和可靠性。以下是几种常见…

Linux进程调度与切换、环境变量

文章目录 Linux优先级Linux的调度与切换**进程切换**&#xff1a;**进程调度**&#xff1a;优先级活动队列过期队列active指针和expired指针 环境变量main函数参数 int main(int argc, char *argv[], char *envp[]) 环境变量环境变量和本地变量echo查看单个环境变量的方法expor…

蓝牙模块在无人机 ID识别、标准制定发挥的作用及其应用优势和面临的挑战

随着科技的飞速发展&#xff0c;无人机已经广泛应用于航拍、农业、救援、物流等多个领域。而在无人机的通信与控制系统中&#xff0c;蓝牙模块扮演着重要的角色。本文将探讨蓝牙模块在无人机Remote ID识别和标准制定执行中发挥的作用&#xff0c;并分析其应用优势和面临的挑战。…

Java读取串口及端口调试

本篇主要讲述使用Java对串口进行读取和发送操作 准备 在项目中导入第三方Jar包 Jar包已经在资源中绑定&#xff0c;或者去官网上自行下载jSerialComm 注意当前jar包是配合JDK1.8环境使用&#xff0c;如果是1.8以下程序将直接中断 安装虚拟串口的软件 Configure Virtual Seri…

一款功能强大的安卓虚拟机应用——VMOS Pro使用分享

前段时间我刚刚分享一个WeChat平板模块能够允许用户自由修改系统设置&#xff0c;让你的Android备用手机焕发新生&#xff0c;实现手机PAD化&#xff0c;实现两台设备同时登录微信号。今天我分享的这个相比WeChat更为简单&#xff0c;因为它可以通过虚拟机的方式进行多种androi…

分类和品牌关联

文章目录 1.数据库表设计1.多表关联设计2.创建表 2.使用renren-generator生成CRUD1.基本配置检查1.generator.properties2.application.yml 2.生成代码1.进入localhost:81生成代码2.将main目录覆盖sunliving-commodity模块的main目录 3.代码检查1.注释掉CategoryBrandRelationC…

JavaWeb基础(HTML,CSS,JS)

这些知识用了三四天左右学完&#xff0c;因为是JavaWeb&#xff0c;并不是前端&#xff0c;所以只是够用&#xff0c;不是深入&#xff0c;但是这确实是学校一个学期交的东西&#xff08;JavaWeb课程&#xff09;。 总结一下网页分为三部分&#xff1a;HTML(内容结构),CSS&…

MySql--SQL语言

目录 SQl---DDL 结构定义 创建、删除 数据库 代码 运行 设计表 数据类型 整数 浮点数 主键 约束 主键自增长 默认值 字段注释 创建、删除 表 代码 运行 代码 代码 运行 SQL---DML 数据操纵 插入数据 代码 运行 代码 运行 代码 运行 代码 …

【实战教程】使用Spring AOP和自定义注解监控接口调用

一、背景 随着项目的长期运行和迭代&#xff0c;积累的功能日益繁多&#xff0c;但并非所有功能都能得到用户的频繁使用或实际上根本无人问津。 为了提高系统性能和代码质量&#xff0c;我们往往需要对那些不常用的功能进行下线处理。 那么&#xff0c;该下线哪些功能呢&…

docker部署kafka实战

目录 一、部署kafaka、zookeeper 二、测试信息发送与接收 三、kafka进阶 一、部署kafaka、zookeeper 请提前安装docker、docker-compose 安装docker&#xff1a;docker--安装docker-ce-CSDN博客 安装docker-compose&#xff1a; 安装docker-compose_安装 docker-compose-CSD…

云下到云上,丽迅物流如何实现数据库降本50% | OceanBase案例

在2024年3月20日的首场OceanBase数据库城市行活动中&#xff0c;专注于物流及供应链解决方案的丽迅物流的架构师阳磊&#xff0c;围绕“OB Cloud在丽迅物流的实践”这一主题&#xff0c;进行了精彩的演讲。本文为此次演讲的内容回顾。 在丽迅物流&#xff08;Lesoon Logistics…

9.1 Go语言入门(环境篇)

Go语言入门&#xff08;环境篇&#xff09; 目录一、什么是Go语言二、下载安装配置Go语言开发环境1. 下载2. 安装3. 配置环境变量4. 安装环境验证 三、 开发工具1. 下载2. 安装3. 激活4. 配置SDK 四、 创建go工程文件并运行1. 创建go工程2. 示例代码3. 运行代码 目录 一、什么…