【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);
#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. 实现多线程
#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);
}
}
可以看到这里右边有了六个线程,一个是我们的主线程。另外五个则是新创建的线程
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。