1. linux线程的概念
众所周知,进程是计算机分配资源的最小单位,线程是计算机资源调度的最小单位。一个进程中可以有多个线程,但只有一个主线程。
在linux操作系统中,并不存在真正意义上的线程,而是轻量级进程(LWP)。
1.1 线程描述
在linux中,线程使用task_struct
结构来描述,工作线程拷贝主线程的task_struct
结构,然后共用主线程的mm_struct
。其中,pid
表示线程id,tgid
表示线程组id,对于主线程来说,pid与pgid
是一样,平时我们看到的进程id即为线程组id。
获取线程id与主线程id:
用户态 | 系统调用 | mm_struct对应的值 |
---|---|---|
线程id | pid_t gettid(void) | pid_t pid |
进程id | pid_t getpid(void) | pid_t tgid |
1.2 线程栈
主线程与工作线程共用一个mm_struct
结构,在线程压栈时必然会导致栈混乱问题,那么如何解决呢?linux中是根据mm_struct
结构中的共享区来实现的,线程压栈实际上是向各自的共享区压栈。每个线程在共享区所占用的大小默认为8M。
1.3 多线程优点
- 线程之间可以共享内存空间
- 创建线程的时间少于创建进程的时间
- 销毁线程的时间少于销毁进程的时间
- 线程上下文切换的消耗低于进程切换的消耗
- 线程间通信比进程间通信方便
可以充分利用多核cpu的并发能力。(线程数并不是越多,处理效率越高)
1.4 多线程缺点
降低程序的健壮性
- 多线程程序出现问题难以定位、排查。
2. 创建线程
2.1 API
创建线程的API函数声明如下: ```cppinclude
int pthread_create(pthread_t thread, const pthread_attr_t attr, void (start_routine) (void ), void arg);
参数详解:
1. thread:该参数是出参,函数调用成功之后返回线程id。
1. attr:线程属性,用来定制线程的属性,如:栈的大小、调度策略等。若无特殊需求,可以为`NULL`。
1. start_routine:线程函数,为一函数指针,线程创建成功后去执行的函数。
1. arg:线程函数参数,即定义向新线程传递的参数。
1. 返回值:0表示成功,返回其他表示失败。
错误码及描述:
| 错误码 | 描述 |
| --- | --- |
| EAGAIN | 系统资源不够,或者创建线程的个数超过系统对一个进程中线程总数的限制 |
| EINVAL | 第二个参数attr值不合法 |
| EPERM | 没有合适的权限来设置调度策略或参数 |
<a name="A3blX"></a>
#### 2.2 线程ID及进程地址空间
获取本线程id的函数如下:
```cpp
#include <pthread.h>
pthread_t pthread_self(void); // 用于线程执行过程中获取自己的线程id
比较是否是同一线程:
#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
// 返回0表示为同一线程,反之则表示为不同线程
// 要比较的两个线程应该为同一线程组里的线程。
调用pthread_create
函数创建线程时,首先会为线程分配栈空间,此栈空间就位于共享区内,调用mmap
函数分配完栈空间后,返回线程id,线程id对应的便是栈空间的地址。本质上,线程id是一个内存地址。其实际的对应关系如下:
2.3 注意
void thread_func(void arg) { int param = (int )arg;
printf("工作线程运行,线程id:%u, 线程参数:%d \n", pthread_self(), *param);
return NULL;
}
int main () { pthread_t tid_main = pthread_self();
printf("主线程id:%u \n", tid_main);
pthread_t tid_work;
int i = 1;
int result = pthread_create(&tid_work, NULL, thread_func, &i);
pthread_join(tid_work, NULL);
return 1;
}
// 编译命令:g++ -o thread_create thread_create.cpp -lpthread
<a name="nSW7o"></a>
### 3. 线程退出
线程退出,进程不退出的方法有:
- 线程函数调用`return`返回
- 线程函数调用`pthread_exit()`
- 其他线程调用`pthread_cancel()`
<a name="wFqZq"></a>
#### 3.1 pthread_exit
函数原型如下:
```cpp
#include <pthread.h>
void pthread_exit(void *retval);
// retval:为返回参数,不能使用临时变量,可使用全局变量、堆上开辟的地址、字符串字面值。
// 线程函数调用其他函数时,在其他函数中调用pthread_exit也会导致线程退出。
3.2 pthread_cancel
函数原型如下:
#include <pthread.h>
int pthread_cancel(pthread_t thread);
// thread:要结束的线程的线程id,可以“自杀”
// 返回值:0:成功 ESRCH:该线程未找到。
3.3 注意事项
- 进程内任一线程调用
exit()
函数,都会导致进程退出,从而导致所有线程退出 - 主线程调用
return
,进程退出,也会导致所有线程退出4. 线程等待
4.1 函数声明
线程等待退出函数声明如下: ```cppinclude
int pthread_join(pthread_t thread, void **retval);
// thread:要等待线程的线程id // retval:线程结束后的返回值 // 返回值: 0:成功 ESRCH 传入的线程ID不存在,查无此线程 EINVAL 线程不是一个joinable线程 EDEADLK 死锁,如自己等待自己
<a name="k3QPV"></a>
#### 4.2 线程等待的原因
如果不调用`pthread_join`等待线程,会造成资源无法释放。所谓资源是什么:
- 创建线程时开辟的栈空间,其内存没有被释放,仍在进程地址空间中
- 新创建的线程无法复用已经不用的资源。
调用`pthread_join`之后,系统并不会马上释放栈空间,因为若频繁创建、销毁线程,频繁调用mmap、munmap会很浪费效率,因此操作系统会将已经不用的栈空间存入链表,当下次创建新线程时,从链表中寻找合适的内存即可。
<a name="M0n90"></a>
### 5. 线程分离
大多数情况下,我们使用`pthread_join`是为了获取线程的退出状态,同时回收线程资源,若有一些线程我们不关心其执行结果,那么调用`pthread_join`便成为了一种负担,此时便会用到线程分离。<br />何为线程分离?经过分离操作后的线程,会自动释放线程资源,无需使用`pthread_join`进行释放。
<a name="aP9aJ"></a>
#### 5.1 函数声明
```cpp
#include <pthread.h>
int pthread_detach(pthread_t thread);
// thread:待分离线程的id
// 返回值:0:成功
ESRCH 传入线程的ID不存在,无此线程
EINVAL 线程不是一个joinable线程,已经处于分离状态
线程分离操作既可以由线程自己实施,也可以由其他线程实施。