1. linux线程的概念

众所周知,进程是计算机分配资源的最小单位,线程是计算机资源调度的最小单位。一个进程中可以有多个线程,但只有一个主线程。
在linux操作系统中,并不存在真正意义上的线程,而是轻量级进程(LWP)。

1.1 线程描述

在linux中,线程使用task_struct结构来描述,工作线程拷贝主线程的task_struct结构,然后共用主线程的mm_struct。其中,pid表示线程id,tgid表示线程组id,对于主线程来说,pid与pgid是一样,平时我们看到的进程id即为线程组id。
Untitled Diagram.drawio.png
获取线程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函数声明如下: ```cpp

    include

int pthread_create(pthread_t thread, const pthread_attr_t attr, void (start_routine) (void ), void arg);

  1. 参数详解:
  2. 1. thread:该参数是出参,函数调用成功之后返回线程id
  3. 1. attr:线程属性,用来定制线程的属性,如:栈的大小、调度策略等。若无特殊需求,可以为`NULL`
  4. 1. start_routine:线程函数,为一函数指针,线程创建成功后去执行的函数。
  5. 1. arg:线程函数参数,即定义向新线程传递的参数。
  6. 1. 返回值:0表示成功,返回其他表示失败。
  7. 错误码及描述:
  8. | 错误码 | 描述 |
  9. | --- | --- |
  10. | EAGAIN | 系统资源不够,或者创建线程的个数超过系统对一个进程中线程总数的限制 |
  11. | EINVAL | 第二个参数attr值不合法 |
  12. | EPERM | 没有合适的权限来设置调度策略或参数 |
  13. <a name="A3blX"></a>
  14. #### 2.2 线程ID及进程地址空间
  15. 获取本线程id的函数如下:
  16. ```cpp
  17. #include <pthread.h>
  18. pthread_t pthread_self(void); // 用于线程执行过程中获取自己的线程id
  1. 比较是否是同一线程:
  1. #include <pthread.h>
  2. int pthread_equal(pthread_t t1, pthread_t t2);
  3. // 返回0表示为同一线程,反之则表示为不同线程
  4. // 要比较的两个线程应该为同一线程组里的线程。

调用pthread_create函数创建线程时,首先会为线程分配栈空间,此栈空间就位于共享区内,调用mmap函数分配完栈空间后,返回线程id,线程id对应的便是栈空间的地址。本质上,线程id是一个内存地址。其实际的对应关系如下:
Untitled Diagram.drawio (1).png

2.3 注意

  • 线程id是进程地址空间中的一个地址,因此同一进程内的两个线程比较才有意义。
  • 线程id有可能会被复用

    2.4 示例

    ```cpp

    include

void thread_func(void arg) { int param = (int )arg;

  1. printf("工作线程运行,线程id:%u, 线程参数:%d \n", pthread_self(), *param);
  2. return NULL;

}

int main () { pthread_t tid_main = pthread_self();

  1. printf("主线程id:%u \n", tid_main);
  2. pthread_t tid_work;
  3. int i = 1;
  4. int result = pthread_create(&tid_work, NULL, thread_func, &i);
  5. pthread_join(tid_work, NULL);
  6. return 1;

}

// 编译命令:g++ -o thread_create thread_create.cpp -lpthread

  1. <a name="nSW7o"></a>
  2. ### 3. 线程退出
  3. 线程退出,进程不退出的方法有:
  4. - 线程函数调用`return`返回
  5. - 线程函数调用`pthread_exit()`
  6. - 其他线程调用`pthread_cancel()`
  7. <a name="wFqZq"></a>
  8. #### 3.1 pthread_exit
  9. 函数原型如下:
  10. ```cpp
  11. #include <pthread.h>
  12. void pthread_exit(void *retval);
  13. // retval:为返回参数,不能使用临时变量,可使用全局变量、堆上开辟的地址、字符串字面值。
  14. // 线程函数调用其他函数时,在其他函数中调用pthread_exit也会导致线程退出。

3.2 pthread_cancel

函数原型如下:

  1. #include <pthread.h>
  2. int pthread_cancel(pthread_t thread);
  3. // thread:要结束的线程的线程id,可以“自杀”
  4. // 返回值:0:成功 ESRCH:该线程未找到。

3.3 注意事项

  • 进程内任一线程调用exit()函数,都会导致进程退出,从而导致所有线程退出
  • 主线程调用return,进程退出,也会导致所有线程退出

    4. 线程等待

    4.1 函数声明

    线程等待退出函数声明如下: ```cpp

    include

    int pthread_join(pthread_t thread, void **retval);

// thread:要等待线程的线程id // retval:线程结束后的返回值 // 返回值: 0:成功 ESRCH 传入的线程ID不存在,查无此线程 EINVAL 线程不是一个joinable线程 EDEADLK 死锁,如自己等待自己

  1. <a name="k3QPV"></a>
  2. #### 4.2 线程等待的原因
  3. 如果不调用`pthread_join`等待线程,会造成资源无法释放。所谓资源是什么:
  4. - 创建线程时开辟的栈空间,其内存没有被释放,仍在进程地址空间中
  5. - 新创建的线程无法复用已经不用的资源。
  6. 调用`pthread_join`之后,系统并不会马上释放栈空间,因为若频繁创建、销毁线程,频繁调用mmap、munmap会很浪费效率,因此操作系统会将已经不用的栈空间存入链表,当下次创建新线程时,从链表中寻找合适的内存即可。
  7. <a name="M0n90"></a>
  8. ### 5. 线程分离
  9. 大多数情况下,我们使用`pthread_join`是为了获取线程的退出状态,同时回收线程资源,若有一些线程我们不关心其执行结果,那么调用`pthread_join`便成为了一种负担,此时便会用到线程分离。<br />何为线程分离?经过分离操作后的线程,会自动释放线程资源,无需使用`pthread_join`进行释放。
  10. <a name="aP9aJ"></a>
  11. #### 5.1 函数声明
  12. ```cpp
  13. #include <pthread.h>
  14. int pthread_detach(pthread_t thread);
  15. // thread:待分离线程的id
  16. // 返回值:0:成功
  17. ESRCH 传入线程的ID不存在,无此线程
  18. EINVAL 线程不是一个joinable线程,已经处于分离状态
  1. 线程分离操作既可以由线程自己实施,也可以由其他线程实施。