Linux 线程:使用管理线程、多线程、分离线程

目录

一、使用线程

1、pthread_create创建线程

2、pthread_join等待线程

主线程获取新线程退出结果

 获取新线程退出返回的数组

3、线程异常导致进程终止

4、pthread_exit

5、pthread_cancel

6、主线程可以取消新线程,新线程可以取消主线程吗

二、如何管理线程

pthread_self函数: 

三、多线程共享进程资源

四、分离线程


一、使用线程

1、pthread_create创建线程

在Linux环境下,POSIX线程库(Pthreads)为多线程编程提供了一系列强大的工具函数,这些函数均以前缀“pthread_”开始。为了在程序中使用这些函数,开发者需要包含头文件 <pthread.h>,并在编译阶段通过 -lpthread 参数链接线程库。

关于线程的创建,POSIX线程库提供了一个关键函数 pthread_create(),用于生成一个新的执行线程。函数原型如下:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
  • thread:这是一个指向 pthread_t 类型变量的指针,用于存储新创建线程的唯一标识符。在函数成功执行后,新线程的ID会被填入这个指针指向的内存位置。

  • attr:这是一个指向 pthread_attr_t 结构体的指针,用于指定线程的属性,如堆栈大小、调度策略等。如果传入 NULL,则表示线程使用默认属性创建。

  • start_routine:这是一个指向线程入口函数的指针,当新线程开始执行时,会先调用这个函数。该函数必须接受一个指向 void 的指针作为参数,并返回 void* 类型的结果。

  • arg:这是一个通用指针,它会被作为参数传递给 start_routine 函数,这样开发者可以在启动线程时传递必要的数据给新线程。

函数的返回值:如果成功创建线程,pthread_create() 会返回零;如果创建失败,则会返回一个非零的错误码。不同于许多传统的POSIX函数,pthread_create() 不会修改全局的 errno 变量来报告错误,而是直接通过返回值反映错误状态。

虽然如此,Pthreads库仍然在每个线程内部维护了自己的 errno 变量副本,以便在使用依赖于 errno 的代码时能够正常工作。但在实际编程实践中,为了优化性能并确保准确性,推荐直接通过检查 pthread_create() 函数的返回值来判断是否成功创建线程,而不是通过读取线程内部的 errno 变量。

-lpthread: 是链接线程库的选项,表示在编译时链接POSIX线程库,以便支持多线程编程。

mytest:test.cc
	g++ -o mytest test.cc -lpthread
.PHONY: clean
clean:
	rm -f mytest
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <string>
using namespace std;

void *threadRoutine(void *args)
{
    while (true)
    {
        cout << "新线程:" << (char *)args << "running" << endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void *)"thread 1");
    while (true)
    {
        cout << "主线程running" << endl;
        sleep(1);
    }
    return 0;
}

 这段C++代码创建了一个简单的多线程程序,它展示了如何使用POSIX线程库(pthread)在Linux环境下创建并运行一个子线程。下面详细解析一下代码的工作原理:

  1. 首先,包含了必要的头文件:

    • <iostream> 用于C++的标准输入输出功能。
    • <pthread.h> 是POSIX线程库头文件,提供创建和管理线程所需的函数原型。
    • <unistd.h> 提供了sleep()函数,用于让线程休眠指定秒数。
    • <stdio.h> 提供了C风格的输入输出函数,这里没有直接使用,但通常包含此头文件以备不时之需。
  2. 定义了一个名为threadRoutine的函数,该函数接受一个指向void类型的指针作为参数,返回值也是一个指向void类型的指针。这是线程执行体,线程运行时会执行这个函数的内容。在这个例子中,函数会无限循环打印一条信息,指出这是一个新的线程在运行,并在每次打印后让线程休眠1秒。

  3. main()函数是程序的入口点,它做了以下几件事:

    • 定义了一个pthread_t类型的变量tid,用来存储线程ID。
    • 使用pthread_create()函数创建一个新线程,传入四个参数:
      • 第一个参数是线程ID的指针,用于接收新创建线程的ID。
      • 第二个参数是线程属性指针,这里设置为nullptr表示使用默认属性。
      • 第三个参数是线程执行函数的指针,指向threadRoutine函数。
      • 第四个参数是要传递给新线程函数的参数,这里是字符串常量"thread 1"的地址。
  4. 创建完新线程后,主线程也开始无限循环,不断打印"主线程running",并在每次打印后同样休眠1秒。

  5. 当程序运行时,你会看到终端交替打印出主线程和新线程的消息,这是因为两个线程都在独立地并发执行。操作系统会在两个线程之间进行上下文切换,看起来就像是两个线程在轮流执行。

    hbr@VM-16-9-centos thread]$ ./mytest 
    主线程running新线程:thread 1running
    
    主线程running
    新线程:thread 1running
    主线程running
    新线程:thread 1running
    主线程running
    新线程:thread 1running
    主线程running
    新线程:thread 1running
    主线程running
    新线程:thread 1running

注意:在这个示例中,因为没有显式地调用pthread_join()函数去等待子线程结束,所以主线程和子线程都会一直运行下去,除非手动停止程序。如果想要在主线程结束前等待子线程完成,应该在适当的地方调用pthread_join(tid, nullptr)来同步主线程和子线程的执行。

查看进程和线程信息

在Linux系统中,ps -aL命令是用来查看所有进程及其所含的线程信息。这条命令的输出可以帮助我们理解上述多线程程序的运行状态。 

[hbr@VM-16-9-centos thread]$ ps -aL | head -1 && ps -aL | grep mytest
  PID   LWP TTY          TIME CMD
10238 10238 pts/0    00:00:00 mytest
10238 10239 pts/0    00:00:00 mytest

输出的第一行(head -1)通常显示列标题,表示每列的信息含义:

  • PID:进程ID
  • LWP:轻量级进程ID,也被称为线程ID
  • TTY:终端设备关联的名称
  • TIME:该进程或线程已经消耗的CPU时间
  • CMD:命令名或命令行参数

接下来的两行(grep mytest筛选出与mytest程序相关的行)显示了你的多线程程序mytest的详细信息:

  • 第一行10238 10238 pts/0 00:00:00 mytest表明进程ID(PID)为10238的进程是你的mytest程序,同时这个进程的主线程ID(LWP)也是10238,它在pts/0终端运行,自启动以来还未消耗任何CPU时间(00:00:00)。
  • 第二行10238 10239 pts/0 00:00:00 mytest表示的是同一个进程(PID仍为10238)内的第二个线程,其线程ID(LWP)为10239,同样在pts/0终端运行,且目前还未消耗CPU时间。

为子线程添加除零操作会导致主线程也终止。

在新的代码中,子线程有一个会导致运行时错误的操作——整数除以零(a /= 0;)。在大多数系统中,这样的操作会产生一个运行时异常,具体来说,在C++中这样的行为通常会触发“除以零”错误(floating point exception或integer division by zero error),这会导致整个程序终止,包括主线程。 

    void *threadRoutine(void *args)
    {
        while (true)
        {
            cout << "新线程:" << (char *)args << "running" << endl;
            sleep(1);
            int a = 100;
            a /= 0;
        }
    }

[hbr@VM-16-9-centos thread]$ ./mytest 
主线程running
新线程:thread 1running
主线程running
Floating point exception
  • 在C++中,特别是使用POSIX线程(pthreads)的情况下,如果线程由于未处理的信号(如SIGFPE,即浮点异常信号)而终止,且没有采取额外的同步措施来确保其他线程在这种情况下继续执行,那么整个进程(包括主线程)都会因此而结束。
  • 为了防止这种情况导致整个程序终止,我们可以考虑在适当的地方添加信号处理函数来捕获并处理这类运行时错误,而不是让它们默认地终止进程。然而,在实际编程中,应当尽量避免产生这类运行时错误,因为它们通常是不可恢复的逻辑错误。

2、pthread_join等待线程

pthread_join()是POSIX线程库中的一个函数,用于等待指定的线程终止,并获取该线程的退出状态。这个函数在多线程编程中扮演了同步角色,确保主线程在子线程结束后再继续执行或者获取子线程的退出信息。

函数原型如下:

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

参数说明:

  • pthread_t thread:你希望等待的那个线程的标识符,通常是由pthread_create()函数创建线程时返回的。

  • void **retval:这是一个可选参数,用于接收线程的退出状态。如果该参数非空,则线程结束时,其退出状态(通过调用pthread_exit()传递的值或默认的NULL)会被复制到*retval指向的地址。如果不需要获取线程的退出状态,可以传递NULL。

使用pthread_join()的主要目的是确保在主线程继续执行之前,已经创建的子线程已完成其任务。如果不调用pthread_join(),那么当主线程结束时,未结束的子线程可能会被强制终止(取决于具体的系统和线程属性),而且无法获取子线程的退出状态。

示例:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

void *threadRoutine(void *args)
{
    int i = 0;
    while (true)
    {
        cout << "新线程: " << (char *)args << " running" << endl;
        sleep(1);
        if (i++ == 10)
            break;
    }
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void *)"thread1");
    pthread_join(tid, nullptr);
    cout << "main thread wait done, main quit" << endl;
    return 0;
}

threadRoutine() 函数是定义的线程执行体:

  1. 接受一个 void* 类型的参数,这个参数在线程创建时传入,此处转换为 (char*) 类型并作为输出的一部分。
  2. 在循环中,线程每秒输出一条包含传入参数的消息,表明线程正在运行。
  3. 当循环计数器 i 达到10时,线程会跳出循环并自然结束。

 main() 函数中:

  1. 定义了一个 pthread_t 类型的变量 tid,用于保存新创建线程的标识符。
  2. 调用 pthread_create() 函数创建一个新的线程。传入参数分别为指向线程标识符 tid 的指针、线程属性(这里设置为 nullptr 表示使用默认属性)、线程运行函数(即 threadRoutine())以及传递给线程函数的参数(这里是字符串 "thread1" 的地址)。
  3. 主线程接着调用 pthread_join() 函数,阻塞等待之前创建的线程执行完毕。这里的第二个参数同样是 nullptr,因为主线程并不关心线程结束时的返回值。
  4. 线程结束后,主线程打印出提示信息 "main thread wait done, main quit" 并返回0,程序结束。

 由于程序创建了一个额外的线程,所以在监控输出中看到的是主进程ID(PID)为 30092 的进程有两个LWP ID,一个是主线程LWP ID 30092,另一个是新创建的线程LWP ID 30093。随着 sleep(1) 的执行,每次循环间隔一秒,所以监控窗口中每隔一秒就会刷新一次状态,显示出主线程和子线程都在运行的状态。当子线程执行完毕之后,pthread_join() 解除阻塞,主线程继续执行并最终退出整个程序。

主线程获取新线程退出结果

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

void *threadRoutine(void *args)
{
    int i = 0;
    while (true)
    {
        cout << "新线程: " << (char *)args << " running" << endl;
        sleep(1);
        if (i++ == 5)
            break;
    }
    cout << "new thread quit" << endl;
    return (void *)10;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void *)"thread1");
    void *ret = nullptr;
    pthread_join(tid, &ret);
    cout << "main thread wait done, main quit: new thread quit: "<< (long long)ret << endl;
    return 0;
}
  • 调用pthread_join(tid, &ret)来等待线程tid完成其任务。当pthread_join()返回时,线程已经终止,&ret参数将接收到线程函数threadRoutine的返回值。

  • 最后,主线程输出一条消息,显示主线程已等待完毕,并打印出线程函数返回的值(转换为long long类型以便输出)。

[hbr@VM-16-9-centos thread]$ ./mytest 
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
new thread quit
main thread wait done, main quit: new thread quit: 10
[hbr@VM-16-9-centos thread]$

 获取新线程退出返回的数组

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

void *threadRoutine(void *args)
{
    int i = 0;
    int *data = new int[5];
    while (true)
    {
        cout << "新线程: " << (char *)args << " running" << endl;
        sleep(1);
        data[i] = i;
        if (i++ == 5)
            break;
    }
    cout << "new thread quit" << endl;
    return (void *)data;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void *)"thread1");
    int *ret = nullptr;
    pthread_join(tid, (void **)&ret);
    cout << "main thread wait done, main quit: new thread quit: " << endl;
    for (int i = 0; i < 5; i++)
    {
        cout << ret[i] << " ";
    }
    cout << endl;
    return 0;
}

首先看threadRoutine函数:

  1. 在线程函数threadRoutine内部,首先分配了一块动态内存空间用于存储一个大小为5的整型数组data
  2. 然后进入一个无限循环,每秒打印一次提示信息,并将数组data的第i个元素赋值为i,直到i增加到5为止,此时跳出循环。
  3. 循环结束后,线程函数返回一个指向data数组的指针,转换为void *类型。

现在来看main函数:

  1. 主线程通过pthread_create函数创建了一个新的线程,并将threadRoutine函数作为新线程的入口点,同时传递一个指向字符串"thread1"的指针作为参数。
  2. 主线程调用pthread_join函数来等待新线程执行完毕。这里的关键是pthread_join的第二个参数,它是一个指向void *类型的指针的指针,用于接收线程结束时返回的值。因此,传入的是(void **)&ret,这样ret就会被赋值为线程函数threadRoutine的返回值,也就是指向data数组的指针。
  3. 主线程等待完成后,输出提示一行信息。
  4. 接下来,主线程尝试通过ret指针访问数组data的内容。由于ret实际上是threadRoutine返回的data数组的指针,因此可以通过ret[i]的方式访问数组中的元素,并将其输出。
[hbr@VM-16-9-centos thread]$ ./mytest 
主进程获取新进程退出结果
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
new thread quit
main thread wait done, main quit: new thread quit:
0 1 2 3 4
[hbr@VM-16-9-centos thread]$

3、线程异常导致进程终止

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

void *threadRoutine(void *args)
{
    int i = 0;
    int *data = new int[10];
    while (true)
    {
        cout << "新线程: " << (char *)args << " running" << endl;
        sleep(1);
        data[i] = i;
        if (i++ == 5)
            break;
        int a = 10;
        a /= 0;
    }
    cout << "new thread quit" << endl;
    return (void *)data;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void *)"thread1");
    int *ret = nullptr;
    pthread_join(tid, (void **)&ret);
    cout << "main thread wait done, main quit: new thread quit: " << (long long)ret << endl;
    for (int i = 0; i < 5; i++)
    {
        cout << ret[i] << " ";
    }
    cout << endl;
    return 0;
}
[hbr@VM-16-9-centos thread]$ ./mytest 
新线程: thread1 running
Floating point exception
[hbr@VM-16-9-centos thread]$
  1. 线程调度: 在操作系统层面,线程的启动并不意味着立即执行,而是由线程调度器决定何时切换到该线程。在上述中,pthread_create函数创建了一个新的线程,但它何时开始执行(即首次获取CPU时间片)取决于操作系统的线程调度策略。实际上,新线程可能在主线程之后执行,也可能在主线程之前或者同时执行,具体顺序不确定。

  2. 线程异常与进程退出: 在threadRoutine函数中,有一行int a = 10; a /= 0;,试图执行除以零的操作,这会导致浮点异常(在某些系统上表现为SIGFPE信号)。在单线程程序中,这样的异常通常会导致程序终止。在多线程环境中,如果某个线程因异常而终止,除非异常得到妥善处理(例如通过设置线程的异常处理器pthread_setsockopt配合PTHREAD_CANCEL_ASYNCHRONOUSsigaction等方法),否则操作系统可能会终止整个进程,因为线程是进程内的执行单元,一个线程的致命错误可能导致整个进程不可恢复。

  3. 线程的输入和返回值问题: 该代码通过pthread_create的最后一个参数传递了一个指向字符串"thread1"的指针作为线程的输入参数。线程函数threadRoutine通过强制类型转换接收并打印这个参数。线程函数最后返回一个指向int数组的指针,主线程通过pthread_join获取这个返回值,并将其转换回int *类型,以便访问数组内容。

  4. 线程异常退出的理解: 由于上述除以零操作触发了浮点异常,threadRoutine线程没有机会执行到最后的cout语句输出"new thread quit",也没有机会正常返回data数组的指针。因此,在实际运行中,你只看到了"新线程: thread1 running"的消息,紧接着出现了“Floating point exception”错误,表明线程在尝试除以零时立刻终止。主线程由于等待pthread_join返回而被阻塞,但由于线程异常退出,pthread_join最终无法正确返回线程的返回值,程序也就提前结束了。

理想情况下,应该在线程函数内部捕获此类异常,或者为线程设置适当的异常处理器,以确保线程异常时能做出合适的响应(如记录错误信息、清理资源等),而不是让整个进程崩溃。在C++/POSIX线程模型中,则需要借助信号处理或其他机制来捕获和处理这类线程异常。

4、pthread_exit

在新线程的while循环结束后调用exit(10)退出线程,实际上并不能真正意义上仅退出当前线程,因为exit函数会结束整个进程,而非单个线程。在多线程程序中,exit会导致进程中所有线程全部停止运行,包括主线程。

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

void *threadRoutine(void *args)
{
    int i = 0;
    int *data = new int[10];
    while (true)
    {
        cout << "新线程: " << (char *)args << " running" << endl;
        sleep(1);
        data[i] = i;
        if (i++ == 5)
            break;
    }
    exit(10);
    cout << "new thread quit" << endl;
    return (void *)data;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void *)"thread1");
    int *ret = nullptr;
    pthread_join(tid, (void **)&ret);
    cout << "main thread wait done, main quit: new thread quit " << endl;
    for (int i = 0; i < 5; i++)
    {
        cout << ret[i] << " ";
    }
    cout << endl;
    return 0;
}

 一旦新线程调用了exit(10),整个进程将立即终止,不会执行到主线程的pthread_join和后续输出data数组内容的部分。主线程也无法有机会得知新线程的退出状态,因为进程已经结束了。在多线程编程中,我们通常不希望使用exit来结束线程,而是应该使用线程特有的终止机制,例如在POSIX线程中,可以使用pthread_exit函数来终止线程,而不会影响其他仍在运行的线程。

[hbr@VM-16-9-centos thread]$ ./mytest 
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
[hbr@VM-16-9-centos thread]$

 pthread_exit() 是POSIX线程接口中的一个函数,用于终止当前线程的执行,并可以选择传递一个退出状态给其他线程。函数原型如下:

void pthread_exit(void *retval);

参数说明:

  • void *retval:一个指向返回值的指针。这个参数是可选的,它可以是一个任意类型数据的地址,用来向其他线程返回一个特定的退出状态。当其他线程调用 pthread_join() 等待这个线程结束时,可以通过 pthread_join() 的第二个参数获取到这个退出状态。

使用pthread_exit()的主要场景包括:

  1. 当线程完成其预设任务后,主动退出,释放系统资源,不再参与执行。
  2. 如果线程在执行过程中遇到了无法恢复的错误或者满足了某种退出条件,可以通过调用 pthread_exit() 来立即终止线程。

举例来说,在一个多线程程序中,当某个工作线程完成了其计算任务后,可以通过调用 pthread_exit(NULL) 来告知系统该线程已结束。主线程或者其他关心该线程状态的线程可以调用 pthread_join() 函数来等待该线程的退出,并检查其退出状态。

需要注意的是,一旦线程调用了 pthread_exit(),该线程所拥有的所有资源(除了那些被显式加入到分离(detached)线程中的资源外)将不再可用,直到有其他线程通过 pthread_join() 成功地与其同步。此外,线程在退出后,其栈上的局部变量和自动对象将被销毁,不过通过 pthread_exit() 设置的返回值会保持有效,直到被其他线程取走。

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

void *threadRoutine(void *args)
{
    int i = 0;
    int *data = new int[10];
    while (true)
    {
        cout << "新线程: " << (char *)args << " running" << endl;
        sleep(1);
        data[i] = i;
        if (i++ == 5)
            break;
    }
    pthread_exit((void *)11);
    //exit(10);
    cout << "new thread quit" << endl;
    return (void *)data;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void *)"thread1");
    int *ret = nullptr;
    pthread_join(tid, (void **)&ret);
    cout << "main thread wait done, main quit: new thread quit "<< (long long)ret << endl;
    cout << endl;
    return 0;
}
[hbr@VM-16-9-centos thread]$ ./mytest 
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
新线程: thread1 running
main thread wait done, main quit: new thread quit 11

5、pthread_cancel

pthread_cancel()是POSIX线程API中的一个函数,用于取消(terminate)一个活动的线程。函数原型如下:

int pthread_cancel(pthread_t thread);

参数说明:

  • pthread_t thread:代表你想取消的线程的ID,这个ID通常是在调用pthread_create()创建线程时得到的。

返回值:

  • 在POSIX线程(pthreads)编程中,当一个线程被显式取消(通过调用pthread_cancel函数)时,被取消线程的状态会被标记为已取消,而pthread_join函数在等待该线程时,如果线程是因为被取消而结束的,那么pthread_join将返回PTHREAD_CANCELED,这是一个预定义的宏,其值为((void *) -1)

函数功能:

  • 调用pthread_cancel()会发送一个取消请求到指定的线程,目标线程在接收到取消请求后,可以通过响应特定的取消点(cancellation points)来终止其执行。取消点通常是那些可能导致线程阻塞并可能被取消的函数调用,例如pthread_cond_wait()pthread_join()sem_wait()等。

  • 默认情况下,线程在接收到取消请求后并不会立即停止,而是会在下一个取消点处检测到请求并进行清理操作,然后退出。这种行为可以通过设置线程的取消类型(cancelability type)为异步取消(asynchronous cancellation)来改变,这样线程在收到取消请求后会立即停止,但这种方式可能引发资源泄漏或数据不一致的问题,因此一般较少采用。

  • 另外,线程可以通过调用pthread_setcancelstate()pthread_setcanceltype()来管理和控制其取消状态和取消类型,还可以通过在关键区域设置取消屏蔽(cancellation mask)暂时阻止取消请求的生效。

  • 被取消的线程最终会以特殊的取消状态退出,调用pthread_join()可以获取到这个状态。同时,线程可以通过在代码中设置清理函数(cleanup handler)来确保在取消发生时进行必要的资源释放或清理工作。

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

void *threadRoutine(void *args)
{
    int i = 0;
    int *data = new int[10];
    while (true)
    {
        cout << "新线程: " << (char *)args << " running" << endl;
        sleep(1);
    }
    cout << "new thread quit" << endl;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void *)"thread1");

    int count = 0;
    while (true)
    {
        cout << "main线程:"
             << " running... " << endl;
        sleep(1);
        count++;
        if (count >= 5)
            break;
    }
    pthread_cancel(tid);
    cout << "pthread cancel:" << tid << endl;
    int *ret = nullptr;
    pthread_join(tid, (void **)&ret); // 默认会阻塞等待新线程退出
    cout << "main thread wait done ... main quit ...: new thead quit : "
         << (long long)ret << "\n ";
    sleep(5);

    return 0;
}

6、主线程可以取消新线程,新线程可以取消主线程吗

在POSIX线程模型中,任何线程都可以发起对其他线程的取消请求,包括主线程可以取消新创建的子线程,理论上新线程也可以尝试取消主线程。但是,是否允许新线程取消主线程以及取消操作的实际效果,通常需要根据程序设计和实际需求来考虑:

  1. 主线程取消子线程:主线程可以通过调用pthread_cancel()函数来取消任何一个子线程,包括新创建的线程。被取消的线程将在遇到取消点时被终止(除非设置了特定的取消类型和状态)。

  2. 子线程取消主线程:在技术层面上,子线程同样可以调用pthread_cancel()尝试取消主线程,但这在实际应用中往往不是一个好的做法。主线程通常是负责程序总体控制和资源管理的,如果主线程被意外取消,可能会导致程序无法正确清理资源、关闭文件、释放内存等,甚至引发不可预见的错误或崩溃。通常建议主线程自己管理其生命周期,而不是由子线程取消。

另外,主线程取消与否还取决于程序的逻辑设计,如果主线程等待所有子线程完成任务(通常通过pthread_join()函数),那么主线程在子线程完成前不应该提前结束;反之,如果主线程需要在任何时刻能够取消子线程,应当在设计时考虑到线程取消的处理和资源清理问题。在实际编程中,应尽量避免线程间的相互取消,尤其主线程的取消,以免产生难以调试和预料的结果。

二、如何管理线程

        在现代操作系统中,尤其是基于POSIX线程库(如pthreads)的多线程编程环境中,线程的创建和管理涉及到内存中多个数据结构的维护。每个线程除了有自己的栈空间之外,还会有一系列与线程相关的属性和控制块(Thread Control Block, TCB),其中就包括线程ID(TID)。

        主线程(或者说初始线程)在创建时由操作系统为其分配内核级栈结构,供其执行环境使用。当创建新的用户级线程时,新线程会复用父线程的地址空间,并且会拥有独立的栈结构,以保证多线程执行流之间的数据隔离。这里的“共享区”通常指的是进程的所有线程共享的地址空间,而每个线程各自的栈空间则位于各自独立的内存区域,互不影响。

        在多执行流环境下,应用程序无法直接操作内核栈,而是通过线程库(如POSIX线程库pthreads)提供的接口创建和管理线程。线程库会在用户态与内核态之间进行切换,协调线程的创建、调度、同步等相关操作,确保各个线程能在共享的虚拟地址空间中独立、并发地执行,而不至于相互冲突。通过线程库创建的线程,其栈结构是由库本身在进程的地址空间内合理安排的,从而避免了对单执行流(即主线程)使用的内核栈区的干扰。

pthread_self函数: 

  • pthread_self函数用于获取当前执行线程的线程ID,即pthread_t类型的数据。在NPTL下,这个函数返回的实际上是当前线程在进程地址空间中对应的线程控制块(TCB)的地址。通过这个地址,线程库可以访问和操作当前线程的各种属性和状态。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

void *threadRoutine(void *args)
{
    int i = 0;
    int *data = new int[10];
    while (true)
    {
        cout << "新线程: " << (char *)args << " running" << endl;
        sleep(1);
    }
    cout << "new thread quit" << endl;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void *)"thread1");
    printf("%lu, %p\n", tid, tid);
    int count = 0;
    while (true)
    {
        cout << "main线程:"
             << " running... main tid: " << pthread_self() << endl;
        sleep(1);
        count++;
        if (count >= 5)
            break;
    }
    pthread_cancel(tid);
    cout << "pthread cancel:" << tid << endl;
    int *ret = nullptr;
    pthread_join(tid, (void **)&ret); // 默认会阻塞等待新线程退出
    cout << "main thread wait done ... main quit ...: new thead quit : "
         << (long long)ret << "\n ";
    sleep(5);

    return 0;
}
[hbr@VM-16-9-centos thread]$ ./mytest 
139972892075776, 0x7f4dfa82a700
main线程: running... main tid: 139972909463360
新线程: thread1 running
main线程: running... main tid: 139972909463360
新线程: thread1 running
main线程: running... main tid: 139972909463360
新线程: thread1 running
main线程: running...main tid: 139972909463360
新线程: thread1 running
main线程: running... main tid: 139972909463360
新线程: thread1 running
pthread cancel:139972892075776
main thread wait done ... main quit ...: new thead quit : -1
 [hbr@VM-16-9-centos thread]$

操作系统层面的线程ID(TID): 在操作系统层面,线程ID(TID)是一个数值,用于在系统范围内唯一标识一个线程。在多线程环境中,操作系统需要一个机制来区分不同的线程,以便进行调度和管理。操作系统内核为每个线程分配一个TID,它是进程调度的基本单位,当线程创建时,操作系统会为该线程生成一个TID,这个ID在系统中是唯一的,即使在不同的进程中也有唯一性。

NPTL线程库层面的线程ID: 在Linux中,使用Native POSIX Thread Library (NPTL) 实现的线程库中,线程ID的表示形式有所不同。pthread_create函数在创建线程时,会返回一个pthread_t类型的线程ID,这个ID在NPTL实现中实际上是进程地址空间内的一个地址。pthread_t类型的线程ID不同于操作系统内核直接分配的TID,它是由线程库自身管理的一个数据结构的地址,这个数据结构中包含了线程的状态、调度信息以及其他必要的元数据。

  • 总结来说,操作系统层面的线程ID(TID)是一个纯粹的数字标识符,而NPTL线程库中的pthread_t类型的线程ID则是一个地址,它指向存储线程相关信息的数据结构。这两者虽然都用于标识线程,但层次和用途不同,前者是系统级别的标识,后者是线程库内部使用的标识。在实际编程中,我们通常通过线程库提供的接口来管理和操作线程,如创建、销毁线程,同步线程等操作。

三、多线程共享进程资源

多线程共享进程资源是多线程编程中的常见场景。在同一个进程中,所有线程都共享同一块内存空间,这意味着它们可以访问相同的全局变量、静态变量、堆区分配的内存以及打开的文件描述符等资源。以下是一个简单的示例来阐述这一概念:

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

// 共享资源:全局变量
int global_count = 0;

// 线程函数,负责更新全局变量
void* increment_count(void* unused) {
    for(int i = 0; i < 1000000; ++i) {
        // 不使用锁的情况下,多个线程同时修改global_count可能导致数据竞争
        ++global_count;
    }
    return nullptr;
}

int main() {
    pthread_t threads[2];

    // 创建两个线程,它们都会访问和修改同一个全局变量global_count
    for(int i = 0; i < 2; ++i) {
        if(pthread_create(&threads[i], NULL, increment_count, NULL)) {
            std::cerr << "Error creating thread." << std::endl;
            return 1;
        }
    }

    // 等待两个线程都结束
    for(int i = 0; i < 2; ++i) {
        pthread_join(threads[i], NULL);
    }

    // 输出最终的global_count值,理论上应该是2000000,但如果存在数据竞争,则可能不是这个值
    std::cout << "Final count: " << global_count << std::endl;

    return 0;
}

        在这个例子中,我们创建了两个线程,它们各自执行increment_count函数,该函数会递增全局变量global_count一千万次。由于全局变量是进程范围内的共享资源,所以两个线程都可以访问和修改它。然而,由于没有采取任何同步措施(例如互斥锁pthread_mutex_t),当两个线程同时尝试修改global_count时,可能出现数据竞争(race condition),导致最终的计数结果不准确。

为了确保线程安全地共享和修改资源,实际编程中通常会使用各种同步机制,如互斥锁、条件变量、信号量等,以防止多个线程同时访问和修改同一资源引发的问题。

四、分离线程

在多线程编程中,当创建一个线程后,默认情况下它是可加入(joinable)的。

  • 这意味着当线程结束执行时,它的资源(如栈空间和其他相关的内核资源)并不会立即释放,而是保持挂起状态,直到有其他线程通过调用pthread_join函数与其进行连接(join)。
  • 若不进行pthread_join操作,这些资源将得不到释放,久而久之,特别是在程序长时间运行且频繁创建销毁线程的情况下,可能会累积大量的资源泄露,对系统性能产生负面影响。

然而,在某些情况下,开发者可能并不关心线程的执行结果(即线程的返回值),只需要线程独立运行并在结束后自行释放资源。这时,就可以使用pthread_detach函数来对线程进行分离(detachment)。分离线程意味着系统在该线程终止时,会自动回收其占用的所有资源,无需其他线程执行pthread_join操作。

有两种方式可以分离线程:

  1. 其他线程调用pthread_detach(pthread_t thread)函数来分离目标线程,其中pthread_t thread是要分离的线程ID。

  2. 线程自身可以在运行时调用pthread_detach(pthread_self())函数来自我分离。pthread_self()函数返回当前线程的ID,调用pthread_detach(pthread_self())则表示当前线程在执行完自身逻辑后,将自动释放资源。

需要注意的是,一个线程不能同时处于可加入和分离两种状态。一旦线程被分离,就不能再对其执行pthread_join操作。也就是说,线程要么是可加入的(等待其他线程通过pthread_join来回收资源),要么是分离的(线程终止后系统自动回收资源)。一旦线程被分离,其终止时就不会有任何返回值可供其他线程获取。

线程自身可以在运行时调用的情况:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

// 定义线程执行函数
void *thread_run(void *arg)
{
    // 在线程开始执行时立即执行分离操作
    pthread_detach(pthread_self());

    // 输出传递给线程的参数
    printf("%s\n", static_cast<char*>(arg));

    // 线程执行结束,返回NULL(此处无实际意义,因为在detach状态下,线程的返回值不会被保留)
    return NULL;
}

int main()
{
    pthread_t tid;

    // 创建新线程
    if (pthread_create(&tid, NULL, thread_run, const_cast<char*>("thread1 run...")) != 0) {
        printf("Failed to create the thread.\n");
        return EXIT_FAILURE;
    }

    // 让新创建的线程有机会先执行并完成分离操作
    sleep(1);

    // 由于线程已经被detach,此处调用pthread_join不是必须的,也不期望成功
    // 但为了展示逻辑,依然尝试join,实际运行时会返回错误
    int join_result = pthread_join(tid, NULL);

    // 根据pthread_join的结果输出相应信息
    if (join_result == 0) {
        printf("The call to pthread_join unexpectedly succeeded.\n");
        // 此处假设join成功是意外情况,返回成功代码(但这在detach线程上不应发生)
        return EXIT_SUCCESS;
    } else {
        printf("pthread_join failed as expected due to detached thread.\n");
        // 实际上,对于detach线程,失败才是预期结果,但仍返回0代表程序本身执行无误
        return EXIT_SUCCESS;
    }
}
thread1 run...
pthread_join failed as expected due to detached thread.

注意,上面的代码中pthread_join在分离线程上调用并不会成功,因为我们已经通过pthread_detach让线程变为分离状态,故不应该尝试去join它。在实际情况中,如果你知道线程已被detach,就不必调用pthread_join。不过,上述代码是为了说明detach线程的概念和行为而设计的。在实际开发中,应在分离线程后跳过join步骤。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

vivado中移位寄存器的优化(二)

移位寄存器优化用于改善移位寄存器单元&#xff08;SRLs&#xff09;与其他逻辑单元之间的负裕量路径的时序。如果存在对移位寄存器单元&#xff08;SRL16E或SRLC32E&#xff09;的时序违规&#xff0c;优化会从SRL寄存器链的开始或结束位置提取一个寄存器&#xff0c;并将其放…

linux学习:gcc编译

编译.c gcc hello.c -o hello 用gcc 这个工具编译 hello.c&#xff0c;并且使之生成一个二进制文件 hello。 其中 –o 的意义是 output&#xff0c;指明要生成的文件的名称&#xff0c;如果不写 –o hello 的话会生成默 认的一个 a.out 文件 获得 C 源程序经过预处理之后的文…

【深度学习】StableDiffusion的组件解析,运行一些基础组件效果

文章目录 前言vaeclipUNetunet训练帮助、问询 前言 看了篇文&#xff1a; https://zhuanlan.zhihu.com/p/617134893 运行一些组件试试效果。 vae 代码&#xff1a; import torch from diffusers import AutoencoderKL import numpy as np from PIL import Image# 加载模型…

【Redis 知识储备】读写分离/主从分离架构 -- 分布系统的演进(4)

读写分离/主从分离架构 简介出现原因架构工作原理技术案例架构优缺点 简介 将数据库读写操作分散到不同的节点上, 数据库服务器搭建主从集群, 一主一从, 一主多从都可以, 数据库主机负责写操作, 从机只负责读操作 出现原因 数据库成为瓶颈, 而互联网应用一般读多写少, 数据库…

zdpdjango_argonadmin Django后台管理系统中的常见功能开发

效果预览 首先&#xff0c;看一下这个项目最开始的样子&#xff1a; 左侧优化 将左侧优化为下面的样子&#xff1a; 代码位置&#xff1a; 代码如下&#xff1a; {% load i18n static admin_argon %}<aside class"sidenav bg-white navbar navbar-vertical na…

SpringCloud Alibaba Sentinel 创建流控规则

一、前言 接下来是开展一系列的 SpringCloud 的学习之旅&#xff0c;从传统的模块之间调用&#xff0c;一步步的升级为 SpringCloud 模块之间的调用&#xff0c;此篇文章为第十四篇&#xff0c;即介绍 SpringCloud Alibaba Sentinel 创建流控规则。 二、基本介绍 我们在 senti…

Golang | Leetcode Golang题解之第16题最接近的三数之和

题目&#xff1a; 题解&#xff1a; func threeSumClosest(nums []int, target int) int {sort.Ints(nums)var (n len(nums)best math.MaxInt32)// 根据差值的绝对值来更新答案update : func(cur int) {if abs(cur - target) < abs(best - target) {best cur}}// 枚举 a…

2024/4/1—力扣—最小高度树

代码实现&#xff1a; /*** Definition for a binary tree node.* struct TreeNode {* int val;* struct TreeNode *left;* struct TreeNode *right;* };*/ struct TreeNode* buildTree(int *nums, int l, int r) {if (l > r) {return NULL; // 递归出口}struct…

加州大学欧文分校英语基础语法专项课程01:Word Forms and Simple Present Tense 学习笔记

Word Forms and Simple Present Tense Course Certificate 本文是学习Coursera上 Word Forms and Simple Present Tense 这门课程的学习笔记。 文章目录 Word Forms and Simple Present TenseWeek 01: Introduction & BE VerbLearning Objectives Word FormsWord Forms (P…

云原生安全当前的挑战与解决办法

云原生安全作为一种新兴的安全理念&#xff0c;不仅解决云计算普及带来的安全问题&#xff0c;更强调以原生的思维构建云上安全建设、部署与应用&#xff0c;推动安全与云计算深度融合。所以现在云原生安全在云安全领域越来受到重视&#xff0c;云安全厂商在这块的投入也是越来…

工业网络自动化控制赛项分析

时间过去很久了,我突然想起来这篇文章还没写… 设备 它实际上是一个药盒装盖然后再进行一个归类码垛 左侧是供料,主要将盒子推出然后传送带送至中间工作站 中间工作站进行对料盒进行钢珠装填 再通过图像处理,判断大小,然后将数据传送到云服务器,最后通过伺服电机进行分类 …

飞书文档如何在不同账号间迁移

今天由于个人需要新建了一个飞书账号&#xff0c;遇到个需求就是需要把老帐号里面的文档迁移到新的账号里面。在网上搜了一通&#xff0c;发现关于此的内容似乎不多&#xff0c;只好自己动手解决&#xff0c;记录一下过程以便分享&#xff0c;主要有以下几个步骤。 1. 添加新账…

蓝桥杯 历届真题 双向排序【第十二届】【省赛】【C组】

资源限制 内存限制&#xff1a;256.0MB C/C时间限制&#xff1a;1.0s Java时间限制&#xff1a;3.0s Python时间限制&#xff1a;5.0s 改了半天只有60分&#xff0c;还是超时&#xff0c;还不知道怎么写&#xff0c;后面再看吧┭┮﹏┭┮ #include<bits/stdc.h> …

Java | Leetcode Java题解之第16题最接近的三数之和

题目&#xff1a; 题解&#xff1a; class Solution {public int threeSumClosest(int[] nums, int target) {Arrays.sort(nums);int n nums.length;int best 10000000;// 枚举 afor (int i 0; i < n; i) {// 保证和上一次枚举的元素不相等if (i > 0 && nums…

YoloV8改进策略:Neck改进改进|ELA

摘要 本文使用最新的ELA注意力机制改进YoloV8&#xff0c;实现涨点&#xff01;改进方式简单易用&#xff0c;涨点明显&#xff01;欢迎大家使用。 大家在订阅专栏后&#xff0c;记着加QQ群啊&#xff01;有些改进方法确实有难度&#xff0c;大家在改进的过程中遇到问题&#…

【QT+QGIS跨平台编译】063:【qca-softstore+Qt跨平台编译】(一套代码、一套框架,跨平台编译)

点击查看专栏目录 文章目录 一、qca-softstore介绍二、QCA下载三、文件分析四、pro文件五、编译实践5.1 windows下编译5.2 linux下编译5.3 macos下编译一、qca-softstore介绍 QCA-Softstore 是一个软件证书存储插件,它是为 QCA 框架设计的。这个插件提供了一个简单的持久化证书…

SAP 资产管理中如何调整折旧(摊销)金额

在资产管理的日常中可能涉及资产折旧金额的调整&#xff08;或者需要调增折旧&#xff0c;或者调减折旧额&#xff09;。这是需要使用到事务代码ABAA或者ABMA。在SAP中&#xff0c;ABAA和ABMA是两个不同的事务代码&#xff0c;它们都与固定资产折旧相关&#xff0c;但用途和处理…

分表?分库?分库分表?实践详谈 ShardingSphere-JDBC

如果有不是很了解ShardingSphere的可以先看一下这个文章&#xff1a; 《ShardingSphere JDBC?Sharding JDBC&#xff1f;》基本小白脱坑问题 阿丹&#xff1a; 在很多开发场景下面&#xff0c;很多的技术难题都是出自于&#xff0c;大数据量级或者并发的场景下面的。这里就出…

【二分查找】Leetcode 搜索插入位置

题目解析 35. 搜索插入位置 这道题就是寻找target的目标位置&#xff0c;如果nums中包含target直接返回索引&#xff1b;如果不包含&#xff0c;需要返回target存放的合适位置 注意这道题有一个细节地方需要注意&#xff1a;如果现在target没有在nums中出现&#xff0c;并且目…

lua学习笔记9(字典的学习)

print("********************字典的学习***********************") a{["凌少"]"傻逼",["我"]"天才",["age"]24,["daihao"]114514,["8848"]20000} --访问单个变量 print(a["凌少"])…