11. 线程

11. 线程

  • 1. 线程概述
    • 1.1 线程概念
      • 1.1.1 什么是线程
      • 1.1.2 线程是如何创建起来的
      • 1.1.3 线程的特点
      • 1.1.4 线程与进程
    • 1.2 并发和并行
  • 2. 线程 ID
  • 3. 创建线程
  • 4. 终止线程
  • 5. 回收线程
  • 6. 取消线程
    • 6.1 取消一个线程
    • 6.2 取消状态以及类型
    • 6.3 取消点
    • 6.4 线程可取消性的检测
  • 7. 分离线程
  • 8. 注册线程清理处理函数
  • 9. 线程属性
    • 9.1 线程栈属性
    • 9.2 分离状态属性
  • 10. 线程与信号
    • 10.1 信号如何映射到线程
    • 10.2 线程的信号掩码
    • 10.3 向线程发送信号
    • 10.4 异步信号安全函数

1. 线程概述

1.1 线程概念

1.1.1 什么是线程

线程是参与系统调度的最小单位,它被包含在进程之中,是进程中的实际运行单位。一个线程指的是进程中的一个单一顺序的控制流,或者说是执行流,一个进程中可以创建多个线程,多个线程实现并发运行,每个线程执行不同的任务。

1.1.2 线程是如何创建起来的

当一个程序启动时,就有一个进程被操作系统创建,与此同时一个线程也立刻运行,该线程通常叫做程序的主线程,因为它是程序一开始就运行的线程。应用程序通常是以 main() 作为入口开始运行的,所以 main() 就是主线程的入口函数。任何一个进程都包含一个主线程,只有主线程的进程称为单线程进程,多线程的其它线程通常是由主线程调用 pthread_create 创建的。主线程通常会在最后结束运行,执行各种清理工作。

1.1.3 线程的特点

线程是程序最基本的运行单位,真正运行的是进程中的线程。当启动应用程序后,系统就创建了一个进程,可以认为进程仅仅是一个容器,它包含了线程运行所需的数据结构、环境变量等信息。同一进程中的多个线程将共享该进程中的全部系统资源,但同一进程中的多个线程有各自的调用栈,自己的寄存器环境、自己的线程本地存储。
线程的特点如下:

  • 线程不单独存在,而是包含在进程中;
  • 线程是参与系统调度的基本单位;
  • 可并发执行。
  • 共享进程资源

1.1.4 线程与进程

进程也可以创建多个进程来并发执行,但是进程间切换开销大,而且进程间通信较为麻烦。线程创建的速度远大于进程创建的速度,多线程在多核处理器上更有优势。

1.2 并发和并行

串行是指必须完成上一个任务才能去做下一个任务;
并行是指多个任务可以同时执行;
并发强调的是时分复用,是指可以打断当前执行的任务而去执行另一个任务

2. 线程 ID

进程 ID 在整个系统中都是唯一的,但线程 ID 只有在它所属的进程上下文中才有意义。

#include 
pthread_t pthread_self();		// 获取自己的线程 ID
int pthread_equal(pthread_t t1,pthread_t t2);	// 检查两个线程 ID 是否相等,相等返回非0,否则返回0

3. 创建线程

#include 
int pthread_create(pthread_t *thread,const pthread_attr_t *attr, void*(*start_routine)(void*),void*arg);
/*
 * thread:函数成功返回时,新创建的线程 ID 会保存在thread指向的内存中
 * attr:指向一块缓冲区,该缓冲区定义里线程的各种属性,如果是NULL,表示以默认属性
 * start_routine:函数指针,指向一个函数,新创建的线程从该函数开始运行。返回值和参数都为void*,参数就是第四个参数arg
 * arg:传递给函数的参数。一般情况下,需要将arg指向一个全局或堆变量,也就是说在线程的生命周期内,该参数必须存在。也可以设置为NULL,表示不需要传参。
 */
static void *routine(void *arg)
{
	cout << "新线程创建成功,进程 ID: "<<getpid()<<" 线程 ID: "<<pthread_self()<<endl;
	return (void*)0;
}
int main()
{
	pthread_t tid;
	pthread_create(&tid,NULL,routine,NULL);
	cout << "我是主线程,进程ID: "<<getpid()<<" 线程 ID: "<< pthread_self()<<endl;
	sleep(1);
	return 0;
}

休眠 1 秒是因为,如果主线程不进行休眠,它就可能立马退出,新线程可能就没有机会运行
运行之后可以发现,两个线程的进程 ID 都是一样的,但是线程 ID 不同

4. 终止线程

  • 在线程的执行函数中调用 return 返回,返回值就是线程的退出码
  • 线程调用 pthread_exit()
  • 调用 pthread_cancel() 取消线程
    在进程的任意位置调用 exit() 或 _exit() 都会导致进程终止
#include 
void pthread_exit(void *retval);

线程退出码可由另一个线程调用 pthread_join() 获取。但是要注意,retval 不能分配在线程栈中,因为不能保证该空间是否有效。

5. 回收线程

#include 
int pthread_join(pthread_t thread,void **retval);
/* 
 * thread:需要等待的线程
 * retval:如果不为NULL,将线程的退出状态保存在*retval中。如果目标线程被取消,将PTHREAD_CANCELED保存。如果对退出状态不关系,可以设置为NULL
 */

该函数会阻塞式等待指定的线程终止。如果该线程已经终止,则立即返回。如果多个线程同时调用,那么结果是不确定的。
若线程未分离(detached),则必须使用该函数来等待线程终止,回收线程资源;如果线程终止后,没有其他线程调用该函数回收该线程,那么该线程将变成僵尸线程。如果僵尸进程过多,会导致系统无法创建新的线程,但是进程终止后,进程会被其父进程回收,所以僵尸进程同样也会被回收。
pthread_join() 和 waitpid() 的区别在于:

  • 线程之间的关系是对等的。进程中的任意线程均可调用 pthread_join() 来等待另一个线程终止。但是父进程如果使用 fork() 创建了子进程,那么它也是唯一能够对子进程调用 wait() 的进程
  • 不能以非阻塞方式调用 pthread_join(),但是 waitpid() 可以实现非阻塞方式等待,也可以是阻塞式等待。

6. 取消线程

向一个线程发送一个信号,要求它立刻退出,就叫做取消线程。

6.1 取消一个线程

#include 
int pthread_cancel(pthread_t thread);

发出取消请求后,函数立即返回,不会等待目标线程的退出。默认情况下,目标线程也会立即退出。但是,线程可以设置自己不被取消或者控制如何被取消,所以该函数并不会等待线程终止,仅仅是提出请求。

6.2 取消状态以及类型

#include 
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);
// 如果对线程取消之前的状态及类型不感兴趣,可以将第二个参数设置为NULL
/* state:
 * PTHREAD_CANCEL_ENABLE:线程可以取消,也是新创建线程取消状态的默认值
 * PTHREAD_CANCEL_DISABLE:线程不可以被取消,接收到取消请求后会将请求挂起,直到取消状态改变
 * /
/* type:
 * PTHREAD_CANCEL_DEFERRED:取消请求到来时,线程还是继续运行,取消请求被挂起,直到线程到达某个取消点,这也是默认类型
 * PTHREAD_CANCEL_ASYNCHRONOUS:可能会在任何时间点取消线程,一般不用。

如果线程的取消状态是默认的,那么对取消请求的操作取决于取决类型。当某个线程调用 fork() 创建子进程之后,子进程会继承调用线程的取消状态和取消类型,而当某线程调用 exec 函数时,会将新程序主线程的取消状态和类型设为默认值。

6.3 取消点

将线程的取消类型设置为默认时,收到其它线程发送来的取消请求时,仅当线程抵达某个取消点时,取消请求才会起作用。
取消点就是一些列函数,当执行到这些函数时,才会真正相应取消请求。在没有出现取消点时,取消请求是无法得到处理的,因为此时正在执行的代码不能被停止。
取消点可以使用man 7 pthreads查看

6.4 线程可取消性的检测

如果正在执行的是一个不含取消点的循环,可以使用 pthread_testcancel() 产生一个取消点,如果已有处于挂起状态的取消请求,那么只要调用该函数,线程就会终止。

#include 
void pthread_testcancel();

7. 分离线程

有时,不关心线程的返回状态,只希望系统在线程终止时能够自动回收线程资源并将其移除。

#include 
int pthread_detach(pthread_t thread);

一个线程可以将别的线程分离,也可以分离自己。一个线程一旦处于分离状态,就不能使用 pthread_join() 来获取终止状态,当其终止后,会自动回收线程资源。

8. 注册线程清理处理函数

与进程不同,一个线程可以注册多个清理函数,这些清理函数记录在栈中,每个线程都可以拥有一个清理函数栈,栈是一种先进后出的数据结构。

#include 
void pthread_cleanup_push(void (*routine)(void*), void *arg);
void pthread_cleanup_pop(int execute);

当线程执行以下动作时,清理函数栈中的函数才会被执行:

  • 线程调用 pthread_exit() 退出时;
  • 线程响应取消请求时;
  • 用非 0 参数调用 pthread_cleanup_pop()
    除了以上三种情况之外,其他方式终止线程将不会执行线程清理函数。
    execute 如果是 0,清理函数不会被调用,只是将清理函数栈中最顶层的函数移除;如果非 0,会将函数执行,并清理。
    在使用时,必须在与线程相同的作用域中以匹配对的形式使用。
    有时,线程清理函数并不一定需要在线程退出时才执行,可以调用 pthread_cleanup_pop() 传入非 0,手动执行清理函数。

9. 线程属性

创建线程时,可以设置属性,当定义 pthread_attr_t 对象之后,需要使用 pthread_attr_init() 和 pthread_attr_destroy() 执行初始化和销毁工作。

#include 
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);

该结构体中的属性比较多,这里不详细介绍。Linux 为该结构体对象的每种属性提供了设置属性的接口以及获取属性的接口。

9.1 线程栈属性

#include 
int pthread_attr_setstack(pthread_attr_t *attr,void *stackaddr,size_t stacksize);
int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr,size_t *stacksize);

// 单独设置或获取大小地址等信息
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr);
int pthread_attr_getstackaddr(const pthread_attr_t *attr, void **stackaddr);

9.2 分离状态属性

#include 
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
/* detachstate:
 * PTHREAD_CREATE_DETACHED:新建线程一开始就处于分离状态,无法被其他线程回收
 * PTHREAD_CREATE_JOINABLE:正常启动线程,可以被其他线程回收

10. 线程与信号

10.1 信号如何映射到线程

信号在一些方面是属于进程层面(由进程中的所有线程共享)的,而在另一些方面是属于单个线程层面的。

  • 信号的系统默认行为是属于进程层面的。当进程中任一线程收到任何一个未经处理(忽略或捕捉)的信号时,会执行该信号的默认操作,通常是停止或终止进程。
  • 信号处理函数属于进程层面,进程中的所有线程共享程序中所注册的信号处理函数
  • 信号的发送既可以针对整个进程,也可以针对某个特定的线程,符合以下任意一个时,是针对某个线程:
    • 产生了硬件异常相关信号,如 SIGBUS、SIGFPE、SIGILL 和 SIGSEGV 信号。这些硬件异常信号在某个线程执行指令的过程中产生,也就是说这些信号是由某些线程引起的,那么在这种情况下,系统会将信号发送给该线程
    • 当线程试图对已断开的管道进行写操作时所产生的 SIGPIPE 信号
    • 由函数 pthread_kill() 或 pthread_sigqueue() 所发出的信号,这些函数运行线程向同一进程下的其他线程发送指定的信号。
  • 当一个多线程进程接收到一个信号时,且该信号绑定了信号处理函数时,内核会任选一个线程来接收这个信号,让进程对单个信号重复接收没有意义
  • 信号掩码其实是属于线程层面的,对于一个多线程来说,各个线程可以调用 pthread_sigmask() 来设置它们各自的信号掩码。
  • 针对整个进程所挂起的信号,以及针对每个线程所挂起的信号,内核都会分别进行维护。

10.2 线程的信号掩码

#include 
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);

每个刚创建的线程,都会从创建者处继承信号掩码,这个新的线程可以通过该函数改变它的信号掩码

10.3 向线程发送信号

#include 
int pthread_kill(pthread_t thread, int sig);

10.4 异步信号安全函数

应用程序中涉及信号处理函数时必须要非常小心,因为信号处理函数可能会在程序执行的任意时间点被调用,从而打断主程序。
前面介绍了线程安全函数,可以被多个线程同时调用,每次都能得到预期的结果,但是这里有前提条件,那就是不能在信号处理函数中调用。
异步信号安全函数(async-signal-safe function)指的是可以在信号处理函数中被安全调用的线程安全函数。可以使用 man 7 signal查看。
对于一个安全的信号处理函数,需要做到以下几点:

  • 首先确保信号处理函数本身的代码是可重入的,且只能调用异步信号安全函数
  • 当主程序执行不安全函数或是去操作信号处理函数也可能会更新的全局数据结构时,要阻塞信号的传递。

你可能感兴趣的:(嵌入式Linux应用开发,嵌入式硬件)