什么是多线程
进程是正在运行的程序的实例,而线程(thread)是进程中的一个执行路线。一个进程可以拥有多个线程。从程序的角度上来说,线程是一个独立运行程序的片段。当程序运行时,进程把大部分资源合理分配给每个执行流(线程),且所有线程共享进程的地址空间,所以线程实际上就是一个轻量级的进程。下面给出进程中的线程示意图:
值得注意的是,windos下的线程是有线程控制块(TCB)的。而linux下的进程控制块和线程控制块都是task_struct
。
为什么称linux下的线程是轻量级进程呢?
这是因为linux内核并没有单独为线程设计一套管理方案,而是通过相同的机制来管理进程和线程,只不过线程拥有的资源是进程的一部分,所以称linux下的线程是轻量级进程。并且,Linux内核中的调度器并不区分线程和进程,可以是单线程的进程,也可以只是一个线程。为了让用户使用起来区分线程和进程,linux向上(用户态)提供了POSI标准的线程接口(如pthread库),在内核用
clone
系统调用创建和管理线程。尽管有线程这个模型,但底层还是轻量级的进程。
总结:==线程是共享同一进程的地址空间和资源的执行单元=。
线程的优点
- 共享资源:同一进程内的线程共享地址空间,能访问相同的全局变量,堆和文件描述符等。这种共享使得线程间通信变得更加高效。
- 独立的执行流:每个线程都有自己的程序计数器、寄存器和栈,这使得线程可以独立执行。线程的独立性使得多个线程并行执行多个任务,提高了此程序的响应性和吞吐量。
- 轻量级:相比于进程,线程的创建开销会小很多,且不需要分配独立的地址空间。上下文切换也比进程快,因为不涉及地址空间的切换。
4.并发执行:在多核处理器上,不同的线程可以做到真正的并行执行
线程的缺点
- 同步复杂性:由于共享进程空间,多个线程同时访问和修改共享数据时可能会导致数据不一致等问题
- 性能损失:使用锁和其它同步机制会导致性能下降
- 调试难度提高:编写和调试一个多线程程序要比单线程程序困难得多
线程异常
- 单个线程如果出现除0或者访问野指针等问题导致线程崩溃,进程也会随着崩溃
- 进程终止,该进程的所有线程都会终止。这也就意味着,如果某一个线程出了异常进而终止进程的话,其它的线程也都会被终止。这也是线程不安全的原因之一。
线程和进程
- 进程是资源分配的基本单位,而线程是调度的基本单位。
- 具体来说,线程共享以下进程资源:
- 代码段和数据段
- 文件描述符表
- 每种信号的处理方式即handler表
- 环境变量包括当前工作目录
- 用户id和组id
- 虽然线程共享进程的数据,但有属于自己的一些数据:
- 线程ID
- 一组寄存器
- 栈
- errno错误流
- 信号屏蔽字
- 调度优先级
进程和线程的关系如下图:
创建线程
在linux中,通常使用POSIX线程库,即pthread库。pthread库提供了一组用于线程创建、管理和同步的函数,这些函数被包含在pthread.h
头文件中。pthread库的主要包含了线程管理、线程同步、线程属性相关的函数,下面介绍线程管理中的一些常用函数。
1.pthread_create
功能:创建一个新的线程
原型:
#include<pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine)(void*), void *arg);
pthread_t
是一个无符号整数thread
是指向pthread_t
变量的一个指针,用于存储创建线程的标识符ID(输出型参数)pthread_attr_t
类型是一个线程属性的类,该类定义了线程的所有属性,包括分离状态、栈的大小等attr
是一个指向const pthread_attr_t
对象的指针,用于初始化被创建线程的属性。如果设置为NULL
,则使用默认属性start_rountine
是一个函数指针,该函数的参数和返回值类型都是void*
。表示线程线程执行的函数。arg
是传递给线程函数的参数。可以是NUL
L,如果需要传递多个参数,可以将其打包成结构体类型对象传进去。同样如果想返回多个值,可以将值打包成一个结构体再返回。- 创建成功返回0。失败则返回一个非0值,表示错误代码。常见得到错误码有:
EAGAIN:
系统资源不足,无法创建更多线程。EINVAL
:无效的线程属性EPERM
:没有足够的权限设置线程属性
给出代码样例,演示使用pthread_create创建线程:
#include <pthread.h>
#include <iostream>
#include <unistd.h>
#include <string.h>
using namespace std;
void *rout(void *arg)//线程执行函数
{
while (true)
{
cout << "i am thread num: " << *(int *)arg << endl;
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
int num = 10;
int res = pthread_create(&tid, NULL, rout, (void *)(&num));
if (res != 0)
{//错误码检查
fprintf(stderr, "pthread_create: %s\n", strerror(res));
exit(1);
}
while (true)
{
cout << "I am main thread" << endl;
sleep(1);
}
return 0;
}
这样我们就成功的使用pthread_create函数创建了一个线程。值得注意的是,执行main函数的线程我们称为主线程。此外,一个线程可以使用pthread_self函数来获取自己的线程ID.
2.pthread_self
功能:获得当前线程的ID
函数原型:
pthread_t pthread_self(void);
于是我们可以将前面的代码样例改一下,观察结果线程ID:
#include <pthread.h>
#include <iostream>
#include <unistd.h>
#include <string.h>
using namespace std;
void *rout(void *arg)
{
while (true)
{
cout << "thread id : " << pthread_self() << " i am thread num: " << *(int *)arg << endl;
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
int num = 10;
int res = pthread_create(&tid, NULL, rout, (void *)(&num));
if (res != 0)
{
fprintf(stderr, "pthread_create: %s\n", strerror(res));
exit(1);
}
while (true)
{
cout << "tid: " << tid << " I am main thread" << endl;
sleep(1);
}
return 0;
}
我i们可以观察到,线程ID是一个非常复杂的数字,这个具体数值通常是由线程库内部实现的,可能会使用内存地址等机制来生成唯一的线程ID。