【linux深入剖析】线程控制 | 多线程

CSDN 2024-08-13 15:37:02 阅读 99

🍁你好,我是 RO-BERRY

📗 致力于C、C++、数据结构、TCP/IP、数据库等等一系列知识

🎄感谢你的陪伴与支持 ,故事既有了开头,就要画上一个完美的句号,让我们一起加油


请添加图片描述


目录

1. 创建线程2. POSIX线程库3. 线程ID及进程地址空间布局4. 线程终止4.1 pthread_exit()4.2 pthread_cancel()

5.线程等待6. 分离线程7. 拓展实验--给线程传入结构体8. 实现多线程


1. 创建线程

功能:创建一个新的线程

<code>int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);

在这里插入图片描述

参数

thread:返回线程IDattr:设置线程的属性,attr为NULL表示使用默认属性tart_routine:是个函数地址,线程启动后要执行的函数rg:传给线程启动函数的参数返回值:成功返回0;失败返回错误码

【错误检查】

传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小

Makefile

<code>testThread:testThread.cc

g++ -o $@ $^ -lpthread

.PHONY:clean

clean:

rm -f testThread

testThread.cc

#include <iostream>

#include <unistd.h>

#include <pthread.h>

#include <sys/types.h>

#include <unistd.h>

int gcnt = 100;

// 新线程

void *ThreadRoutine(void *arg)

{

const char *threadname = (const char *)arg;

while (true)

{

std::cout << "I am a new thread: " << threadname << ", pid: " << getpid()<< std::endl;

sleep(1);

}

}

int main()

{

pthread_t tid;

pthread_create(&tid, nullptr, ThreadRoutine, (void *)"thread 1");

while (true)

{

std::cout << "I am main thread"<< std::endl;

sleep(1);

}

return 0;

}

运行结果:

在这里插入图片描述

可以看到确实是有了两个线程都在运行,这里的线程就是一个是我们的main主线程,另一个则是我们新创建的线程。

我们可以看到两个线程的PID是一模一样的,那么CPU如何分辨线程呢?CPU划分线程就是用的后面的LWP,PID我们知道,是之前讲过的进程的唯一标识符LWP英文名为 Light Weight Processes,其意思就是轻量化进程,而线程就是轻量化进程,所以这就是线程

上面我们可以注意到我们的makefile文件里面是使用了Pthread库来对我们的代码进行编译的,如果我们不带库就会出现以下报错。其含义就是找不到Pthread函数调用接口

在这里插入图片描述

接下来我们来认识一下POSIX线程库


2. POSIX线程库

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

POSIX线程库,也称为pthread,是一种跨平台的标准API,它为应用程序提供了一套统一的方式来创建和管理线程。

由于Unix-like操作系统(如Linux、macOS)的广泛使用,POSIX标准允许开发者编写一次代码就能在多种平台上运行,避免了为每种操作系统单独编写和维护线程代码的工作量。

POSIX线程使得程序可以将并发处理的部分封装成独立的模块,便于模块间的通信和协同工作,提高代码的灵活性和效率。

标准化的线程API保证了不同系统之间的线程行为基本一致,有利于开发者理解和预测其行为,同时也有助于优化线程管理,提升系统的整体性能。

POSIX线程库提供了丰富的错误处理机制和同步原语(如互斥锁、条件变量),有助于开发者设计出健壮且安全的多线程应用。

虽然直接操作底层硬件线程可能会更复杂,但是通过POSIX API,开发者能以相对简单的方式实现复杂的并发控制,降低学习曲线。

在这里插入图片描述

开发者将pthread线程库开发成这样就是为了线程安全并且实现跨平台的性能,让OS能通过线程库提供API给用户才能实现线程的操作

另外需要注意的是,在linux中,如何对这些线程进行管理呢?

其实线程是由线程库来统一进行管理的,在引入线程库的时候,他就会在共享区有一块内存空间,然后我们开辟线程的时候,线程库就会为我们开辟空间来管理线程,线程也是先描述,在组织,其本质上也是一个结构体,有pwd、上下文等属性,线程之间是互相独立的


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

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

<code>pthread_t pthread_self(void);

在这里插入图片描述

<code>#include <iostream>

#include <unistd.h>

#include <pthread.h>

#include <sys/types.h>

#include <unistd.h>

// 新线程

void *ThreadRoutine(void *arg)

{

const char *threadname = (const char *)arg;

while (true)

{

std::cout << "I am a new thread: " << threadname << "thread id: " << pthread_self() << std::endl;

sleep(1);

}

}

int main()

{

pthread_t tid;

pthread_create(&tid, nullptr, ThreadRoutine, (void *)"thread 1");

while (true)

{

std::cout << "I am main thread" << "thread id: " << pthread_self()<< std::endl;

sleep(1);

}

return 0;

}

在这里插入图片描述

可以看到main线程id和新线程id是不一样的

pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址

在这里插入图片描述

上图左边是进程创建的地址空间,中间的mmap区域就是我们的共享区,里面便是Pthread库,右边就是Pthread库里有两个线程结构体图

4. 线程终止

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

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

4.1 pthread_exit()

线程终止函数接口

<code>void pthread_exit(void *value_ptr);

参数

value_ptr : value_ptr不要指向一个局部变量。返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

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

测试代码

#include <iostream>

#include <unistd.h>

#include <pthread.h>

#include <sys/types.h>

#include <unistd.h>

// 新线程

void *ThreadRoutine(void *arg)

{

const char *threadname = (const char *)arg;

int cnt = 5;

while (cnt--)

{

std::cout << "I am a new thread: " << threadname << std::endl;

sleep(1);

}

pthread_exit(nullptr);

}

int main()

{

pthread_t tid;

pthread_create(&tid, nullptr, ThreadRoutine, (void *)"thread 1");

while (true)

{

std::cout << "I am main thread" << std::endl;

sleep(1);

}

return 0;

}

在这里插入图片描述

可以看到我们的主线程还在执行输出语句,新线程已经结束生命周期了

4.2 pthread_cancel()

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

原型

<code>int pthread_cancel(pthread_t thread);

参数

thread:线程ID

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

#include <iostream>

#include <unistd.h>

#include <pthread.h>

#include <sys/types.h>

#include <unistd.h>

// 新线程

void *ThreadRoutine(void *arg)

{

const char *threadname = (const char *)arg;

while (1)

{

std::cout << "I am a new thread: " << threadname << std::endl;

sleep(1);

}

}

int main()

{

pthread_t tid;

pthread_create(&tid, nullptr, ThreadRoutine, (void *)"thread 1");

sleep(5);

while (true)

{

std::cout << "I am main thread" << std::endl;

sleep(1);

pthread_cancel(tid);

}

return 0;

}

我们让主线程沉睡了5秒,5秒后新线程成功关闭

在这里插入图片描述


5.线程等待

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

线程等待没有之前的僵尸进程明显,但是每个线程创建后是需要我们对其进行回收的

功能:等待线程结束

原型

<code>int pthread_join(pthread_t thread, void **value_ptr);

参数

thread:线程ID

value_ptr:它指向一个指针,后者指向线程的返回值

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

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

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

在这里插入图片描述

测试代码

<code>#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <unistd.h>

#include <pthread.h>

void *thread1(void *arg)

{

printf("thread 1 returning ... \n");

int *p = (int *)malloc(sizeof(int));

*p = 1;

return (void *)p;

}

void *thread2(void *arg)

{

printf("thread 2 exiting ...\n");

int *p = (int *)malloc(sizeof(int));

*p = 2;

pthread_exit((void *)p);

}

void *thread3(void *arg)

{

while (1)

{ //

printf("thread 3 is running ...\n");

sleep(1);

}

return NULL;

}

int main(void)

{

pthread_t tid;

void *ret;

// thread 1 return

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

pthread_join(tid, &ret);

printf("thread return, thread id %d, return code:%d\n",(int16_t)tid, *(int *)ret);

free(ret);

// thread 2 exit

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

pthread_join(tid, &ret);

printf("thread return, thread id %d, return code:%d\n", (int16_t)tid, *(int *)ret);

free(ret);

// thread 3 cancel by other

pthread_create(&tid, NULL, thread3, NULL);

sleep(3);

pthread_cancel(tid);

pthread_join(tid, &ret);

if (ret == PTHREAD_CANCELED)

printf("thread return, thread id %d, return code:PTHREAD_CANCELED\n", (int16_t)tid);

else

printf("thread return, thread id %d, return code:NULL\n", (int16_t)tid);

return 0;

}

在这里插入图片描述


6. 分离线程

默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。

如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

<code>int pthread_detach(pthread_t thread);

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

pthread_detach(pthread_self());

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

#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", (char *)arg);

return NULL;

}

int main(void)

{

pthread_t tid;

if (pthread_create(&tid, NULL, thread_run,(void *)"thread1 run...") != 0)

{

printf("create thread error\n");

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;

}

在这里插入图片描述


7. 拓展实验–给线程传入结构体

<code>int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);

线程的第一个参数是他的线程ID,第二个是它的属性一般设置为nullptr,第三个为执行的函数,第四个参数则是可以传入线程的参数,其类型是void*的类型,所以我们可以尝试传入任意类型

#include <iostream>

#include <string.h>

#include <unistd.h>

#include <pthread.h>

#include <functional>

#include <string>

#include <ctime>

// typedef std::function<void()> func_t;

using func_t = std::function<void()>;

class ThreadData

{

public:

ThreadData(const std::string &name, const uint64_t &ctime, func_t f)

: threadname(name), createtime(ctime), func(f)

{

}

public:

std::string threadname;

uint64_t createtime;

func_t func;

};

void Print()

{

std::cout << "我是线程执行的大任务的一部分" << std::endl;

}

// 新线程

void *ThreadRountine(void *args)

{

ThreadData *td = static_cast<ThreadData *>(args);

while (true)

{

std::cout << "new thread"

<< " thread name: " << td->threadname << " create time: " << td->createtime << std::endl;

td->func();

sleep(1);

}

}

// 获取返回值

// 主线程

int main()

{

pthread_t tid;

ThreadData *td = new ThreadData("new thread", (uint64_t)time(nullptr), Print);

pthread_create(&tid, nullptr, ThreadRountine, td);

while (true)

{

std::cout << "main thread" << std::endl;

sleep(3);

}

}

在这里插入图片描述


8. 实现多线程

<code>#include <iostream>

#include <string.h>

#include <unistd.h>

#include <pthread.h>

#include <functional>

#include <string>

#include <ctime>

// typedef std::function<void()> func_t;

using func_t = std::function<void()>;

const int threadnum = 5;

//给线程传入的结构体

class ThreadData

{

public:

ThreadData(const std::string &name, const uint64_t &ctime, func_t f)

: threadname(name), createtime(ctime), func(f)

{

}

public:

std::string threadname;

uint64_t createtime;

func_t func;

};

void Print()

{

std::cout << "我是线程执行的大任务的一部分" << std::endl;

}

// 新线程

void *ThreadRountine(void *args)

{

int a = 10;

ThreadData *td = static_cast<ThreadData *>(args); //类型转换为结构体指针

while (true)

{

std::cout << "new thread"

<< " thread name: " << td->threadname << " create time: " << td->createtime << std::endl;

td->func();

sleep(1);

}

}

int main()

{

std::vector<pthread_t> pthreads;

for (size_t i = 0; i < threadnum; i++)

{

//snprintf函数实现输入线程名称

char threadname[64];

snprintf(threadname, sizeof(threadname), "%s-%lu", "thread", i);

pthread_t tid;

//time时间戳函数

ThreadData *td = new ThreadData(threadname, (uint64_t)time(nullptr), Print);

pthread_create(&tid, nullptr, ThreadRountine, td);

pthreads.push_back(tid);

sleep(1);

}

while (true)

{

std::cout << "main thread" << std::endl;

sleep(3);

}

}

在这里插入图片描述

可以看到这里右边有了六个线程,一个是我们的主线程。另外五个则是新创建的线程



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。