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.ccg++ -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 mytestPID   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_countfor(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;}
}

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

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

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

相关文章

Linux 常用命令(持续更新中...)

1. ls 查看文件列表命令 语法&#xff1a; ls [-a -l -h] [Linux路径] -a -l -h 是可选的选项 &#xff08;-h需配合-l命令一起使用&#xff09;Linux路径是此命令可选的参数 ls #查看当前目录所有非隐藏文件(平铺方式显示) ls -a #查看当前目录下所有文件 …

【stm32】I2C通信外设

【stm32】I2C通信外设 概念部分 如果简单应用&#xff0c;选择软件I2C。如果对性能指标要求比较高 选择硬件I2C 有硬件电路自动反转引脚电平&#xff0c;软件只需要写入控制寄存器CR和数据寄存器DR 为了实时监控时序的状态&#xff0c;还要读取状态寄存器SR 写入控制寄存器CR…

力扣刷题 二叉树遍历的统一迭代法

题干 给定一个二叉树的根节点 root &#xff0c;返回 它的 前中后序 遍历 。 示例 1&#xff1a; 输入&#xff1a;root [1,null,2,3] 输出&#xff1a;[1,3,2]示例 2&#xff1a; 输入&#xff1a;root [] 输出&#xff1a;[]示例 3&#xff1a; 输入&#xff1a;root […

数据挖掘入门项目二手交易车价格预测之建模调参

文章目录 目标步骤1. 调整数据类型&#xff0c;减少数据在内存中占用的空间2. 使用线性回归来简单建模3. 五折交叉验证4. 模拟真实业务情况5. 绘制学习率曲线与验证曲线6. 嵌入式特征选择6. 非线性模型7. 模型调参&#xff08;1&#xff09; 贪心调参&#xff08;2&#xff09;…

浮动辊位移测量功能块(CODESYS ST代码)

1、张力测量+标定(ST代码) 张力测量+标定(ST代码)_动态舞轮控制张力-CSDN博客文章浏览阅读804次。跳舞轮对应张力调节范围,我们可以通过改变气缸的气压方式间接改变,张力跳舞轮在收放卷闭环控制上的详细应用,可以参看下面的文章链接,这里我们主要讨论精密可调气阀的模拟量…

每日五道java面试题之ZooKeeper篇(三)

目录&#xff1a; 第一题. 会话管理第二题. 服务器角色第三题. Zookeeper 下 Server 工作状态第四题. 数据同步第五题. zookeeper 是如何保证事务的顺序一致性的&#xff1f; 第一题. 会话管理 分桶策略&#xff1a;将类似的会话放在同一区块中进行管理&#xff0c;以便于 Zoo…

autovacuum

相关查询语句 select relname,reltuples from pg_class where relnamepgbench_accounts; show autovacuum_vacuum_scale_factor ; select count(*) from pgbench_accounts; \dt pgbench_accounts SELECT * FROM pgstattuple(pgbench_accounts); --需要开启插件 SELECT relnam…

微软云学习环境

微软公有云 - Microsoft Azure 本文介绍通过微软学习中心Microsoft Learn来免费试用Azure上的服务&#xff0c;也不需要绑定信用卡。不过每天只有几个小时的时间。 官网 https://docs.microsoft.com/zh-cn/learn/ 实践 比如创建虚拟机&#xff0c;看到自己的账号下多了Learn的…

Javascript/Node.JS中如何用多种方式避免属性为空(cannot read property of undefined ERROR)

>>>>>>问题 "cannot read property of undefined" 是一个常见的 JavaScript 错误&#xff0c;包含我在内很多人都会遇到&#xff0c;表示你试图访问一个未定义&#xff08;undefined&#xff09;对象的属性。这通常是因为你在访问一个不存在的对象…

(ISPRS,2023)深度语义-视觉对齐用于zero-shot遥感图像场景分类

文章目录 相关论文摘要引言类别嵌入局限性——问题1普通ZSL模型局限性——问题2自动属性注释过程——对应问题1深度语义-视觉对齐&#xff08;DSVA&#xff09;模型——对应问题2 基于遥感多模态相似性的自动属性标注属性词汇表构造使用CLIP模型自动标注属性对CLIP模型进行训练…

ideaSSM 网上选课管理系统bootstrap开发mysql数据库web结构java编程计算机网页源码maven项目

一、源码特点 idea 开发 SSM 网上选课管理系统是一套完善的信息管理系统&#xff0c;结合SSM框架和bootstrap完成本系统&#xff0c;对理解JSP java编程开发语言有帮助系统采用SSM框架&#xff08;MVC模式开发&#xff09;&#xff0c;系统具有完整的源代码和数据库&#xff…

哈希-字母异位词分组

字母异位词&#xff0c;词频一样&#xff0c;但是顺序不一样&#xff0c;可以进行排序&#xff0c;获取一个key&#xff0c;放在map中即可。 class Solution {public List<List<String>> groupAnagrams(String[] strs) {Map<String, List<String>> ma…

清明的商标从已注册到不良影响的变化!

清明时节雨纷纷&#xff0c;“清明”可以申请注册商标不&#xff0c;普推知产老杨经检索发现&#xff0c;2022年还有以“清明”通过初审下证的&#xff0c;在这之前更多&#xff0c;还有“清明”在方便食品类别上是著名商标&#xff0c;那个2022年通过初审的正在无效程序中。 2…

mbti,INTJ型人格的心理问题分析

什么是INTJ型人格&#xff1f; INTJ来自mbti职业性格测试&#xff0c;16种人格类型之一&#xff0c;INTJ分别代表内向&#xff0c;直觉&#xff0c;理智&#xff0c;独立&#xff0c;而INTJ型人格是一种以冷静和理性著称的人格&#xff0c;这种人格的人总给人一种比较理智&…

图解大型网站多级缓存的分层架构

前言 缓存技术存在于应用场景的方方面面。从浏览器请求&#xff0c;到反向代理服务器&#xff0c;从进程内缓存到分布式缓存&#xff0c;其中缓存策略算法也是层出不穷。 假设一个网站&#xff0c;需要提高性能&#xff0c;缓存可以放在浏览器&#xff0c;可以放在反向代理服…

【智能排班系统】雪花算法生成分布式ID

文章目录 雪花算法介绍起源与命名基本原理与结构优势与特点应用场景 代码实现代码结构自定义机器标识RandomWorkIdChooseLocalRedisWorkIdChooselua脚本 实体类SnowflakeIdInfoWorkCenterInfo 雪花算法类配置类雪花算法工具类 说明 雪花算法介绍 在复杂而庞大的分布式系统中&a…

小坤二次元导航HTML源码

源码介绍 小坤二次元导航HTML源码&#xff0c;很好看的一个htmlの引导页/导航页&#xff01;需要的上&#xff01; 源码下载 小坤二次元导航HTML源码

游戏攻略|基于Springboot和vue的游戏分享平台系统设计与实现(源码+数据库+文档)

游戏攻略分享平台 目录 基于Springboot和vue的游戏分享平台系统设计与实现 一、前言 二、系统设计 三、系统功能设计 1、前台&#xff1a; 2、后台 管理员功能模块 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a…

python绘制子图(旭日图、渐变堆积面积图、多数据折线图、比例关系图)

大家好&#xff0c;我是带我去滑雪&#xff01; 子图可以更清晰地展示和理解复杂的数据关系&#xff0c;通过将数据分成多个小图&#xff0c;有助于观察数据间的关系和趋势。减少数据之间的重叠和混淆&#xff0c;使得每个子图更易于理解和解释。不同类型的子图可以呈现数据的不…

蓝鲸6.1 CMDB 事件推送的开源替代方案

本文来自腾讯蓝鲸智云社区用户&#xff1a;木讷大叔爱运维 背景 在蓝鲸社区“社区问答”帖子中发现这么一个需求&#xff1a; 究其原因&#xff0c;我在《不是CMDB筑高墙&#xff0c;运维需要一定的开发能力&#xff01;》一文中已经介绍&#xff0c;在此我再简单重复下&#…