线程的基本定义
线程(Thread)是操作系统能够进行运算调度的最小单位,它被包含在进程(Process)中,是进程中的实际运作单位。一个线程可以与同一进程中的其他线程共享进程的全部资源,包括内存、文件和网络连接等。由于线程之间共享内存,所以线程之间的通信更加高效,在多处理器系统中,多个线程可以并发执行,从而提高了系统的并发性能。
为什么要引入线程
引入线程的主要目的是提高程序的并发性能和效率。在单线程程序中,如果某一个任务阻塞了整个程序,那么整个程序都会停止执行,而多线程程序可以实现并发执行,即使其中的某一个线程被阻塞了,其他线程仍然可以继续执行。
另外,多线程程序也可以更好地利用计算机资源,因为多个线程可以在不同的 CPU 上运行。此外,线程之间可以共享数据,这样在处理一些需要共享数据的任务时,线程间的通信更加高效。
总的来说,引入线程可以提高程序的并发性能和效率,提高系统的资源利用率,并且简化代码编写过程,使得程序更加易于开发和维护。
用户级线程是由用户程序管理的线程,而不是由操作系统内核管理的线程。在使用用户级线程时,应用程序会自己维护线程调度、同步和通信等,并且运行在用户空间中。
相比之下,操作系统内核管理的线程 (也称为内核级线程) 能够充分利用多核处理器。这是因为每个内核都可以独立地执行一个线程,从而使多个线程可以并行执行,提高了系统的并发性能。
然而,用户级线程不能直接利用多核处理器。这是因为用户级线程的调度是由应用程序自己管理的,而应用程序无法控制在哪个核心上运行线程或者将线程分配到哪个核心上。因此,即使有多个核心可用,用户级线程仍然只能在单个核心上运行,无法实现真正的并行执行。
虽然用户级线程不能直接利用多核处理器,但是可以通过一些技术来实现多核并行。例如,可以使用线程池或者任务调度器来管理线程,并将任务分配给不同的线程执行。这些技术可以在用户级别上实现多核并行,但是需要应用程序自己实现线程调度、同步和通信等,复杂度较高。
线程的创建
pthread_create() 是 POSIX 线程库中的一个函数,用于创建新线程。
该函数的原型如下:
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
它需要传入以下四个参数:
- thread:指向 pthread_t 类型的指针,用于存储新线程的 ID。
- attr:指向 pthread_attr_t 类型的指针,用于设置线程属性。如果不需要特别设置线程属性,则可以将该参数设置为 NULL。
- start_routine:指向函数的指针,新线程将从该函数开始执行。该函数必须返回 void * 类型,并且接受一个 void * 类型的参数。
- arg:传递给 start_routine 函数的参数。
当成功创建新线程时,pthread_create() 函数会返回 0,否则返回错误码。在新线程执行完毕之前,创建该线程的线程应该一直等待该线程的结束,以保证不会出现资源泄漏或者竞争问题。可以使用 pthread_join() 函数来等待一个线程完成执行并回收它的资源。
用户级线程和内核级线程
用户级线程和内核级线程是两种不同的线程实现方式。
用户级线程是由应用程序自己创建和管理的线程,操作系统并不感知它们的存在。用户级线程的调度、同步和通信都由应用程序自己负责,因此具有更高的灵活性和效率。但是,由于操作系统无法感知用户级线程的存在,因此在遇到阻塞等问题时,整个进程都会被挂起,导致资源利用率下降。
内核级线程是由操作系统创建和管理的线程,它们直接映射到操作系统的内核线程上,并由操作系统负责调度、同步和通信。由于操作系统可以感知内核级线程的存在,因此在遇到阻塞等问题时,可以将一个线程挂起而不影响整个进程的执行。但由于内核级线程需要频繁地从用户态切换到内核态,因此会带来额外的开销和延迟。
在实际应用中,一般采用混合型线程模型,即在应用程序中使用用户级线程,在操作系统内核中使用内核级线程。这样可以兼顾灵活性和效率,同时也能够利用操作系统的优点来解决阻塞等问题。
进程的创建和终止
线程函数的错误处理
之前的POSIX系统调用和库函数在调用出错的时候,通常会把全局变量 errno 设置为一个特别的数值以
指示报错的类型,这样就可以调用 perror 以显示符合人阅读需求的报错信息。但是在多线程编程之
中,全局变量是各个线程的共享资源,很容易被并发地读写,所以pthread系列的函数不会通过修改
errno 来指示报错的类型,它会根据不同的错误类型返回不同的返回值,使用 strerror 函数可以根据
返回值显示报错字符串。
线程的主动退出
使用 pthread_exit 函数可以主动退出线程,无论这个函数是否是在 start_routine 中被调用,其行为类似于进程退出的 exit 。 pthread_exit 的参数是一个 void * 类型值,它描述了线程的退出状态。在start_routine 中使用return语句可以实现类似的主动退出效果,但是其被动退出的行为有一些问题,所以使用较少。线程的退出状态可以由另一个线程使用 pthread_join 捕获,但是和进程不一样的是,另一个线程的选择是任意的,不需要是线程创建者。
获取线程退出状态
调用 pthread_join 可以使本线程处于等待状态,直到指定的 thread 终止,就结束等待,并且捕获到的线程终止状态存入 retval 指针所指向的内存空间中。因为线程的终止状态是一个 void * 类型的数据,所以 pthread_join 的调用者往往需要先申请8个字节大小的内存空间,然后将其首地址传入,在pthread_join 的执行之后,这里面的数据会被修改。有些时候,内容可能是一片数据的首地址,还有些情况下内容就是简单的一个8字节的整型。
线程的取消
int pthread_cancel(pthread_t thread);
查看线程的状态
$ps -elLf
线程资源清理
在引入线程取消之后,程序员在管理资源回收的难度上会急剧提升。为了简化资源清理行为,线程库引入了 pthread_cleanup_push 和pthread_cleanup_pop 函数来管理线程主动或者被动终止时所申请资源(比如文件、堆空间、锁等等)。
void pthread_cleanup_push(void (*routine)(void *),void *arg);
void pthread_cleanup_pop(int execute);
pthread_cleanup_push 负责将清理函数压入一个栈中,这个栈会在下列情况下弹出:
- 线程因为取消而终止时,所有清理函数按照后进先出的顺序从栈中弹出并执行。
- 线程调用 pthread_exit 而主动终止时,所有清理函数按照后进先出的顺序从栈中弹出并执行。
- 线程调用 pthread_clean_pop 并且 execute 参数非0时,弹出栈顶的清理函数并执行。
- 线程调用 pthread_clean_pop 并且 execute 参数为0时,弹出栈顶的清理函数不执行。
线程的同步和互斥
在多线程编程中,用来控制共享资源的最简单有效也是最广泛使用的机制就是 mutex(MUTualEXclusion) ,即互斥锁。锁的数据类型是pthread_mutex_t ,其本质是一个全局的标志位,线程可以对作原子地测试并修改,即所谓的加锁。当一个线程持有锁的时候,其余线程再尝试加锁时(包括自己再次加锁),会使自己陷入阻塞状态,直到锁被持有线程解锁才能恢复运行。所以锁在某个时刻永远不能被两个线程同时持有。
创建锁有两种形式:直接用 PHTREAD_MUTEX_INITIALIZER 初始化一个 pthread_mutex_t 类型的变量,即静态初始化锁;而使用pthread_mutex_init 函数可以动态创建一个锁。动态创建锁的方式更加常见。
使用 pthread_mutex_destory 可以销毁一个锁。
pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t
*mutexattr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
什么是取消点
在 Linux 中,线程的取消点(Cancellation Point)是指在执行某些操作期间可以被取消的特定点。当一个线程被请求取消时,它会在下一个取消点上停止执行,这使得线程可以更容易地被终止或暂停。
Linux 系统中的许多函数都是取消点,包括 I/O 操作、等待系统调用、锁操作等。当线程执行到这些函数时,它们将检查是否有取消请求,并在必要时允许线程被取消。例如,在使用 pthread 库创建的线程中,pthread_join() 是一个取消点。
如果线程没有在取消点上阻塞,则取消请求将被延迟,直到线程到达下一个取消点,这可能会导致一些延迟。因此,在编写多线程应用程序时,需要考虑取消点的影响,以确保及时响应取消请求。
当调用 pthread_cancel() 函数请求取消一个线程时,操作系统并不会立即终止该线程,而是将一个“取消请求”标记设置为该线程。然后,当线程到达一个取消点时,它会检查是否有取消请求,并在必要时执行线程取消操作。
以下是 pthread_cancel() 函数的大致流程:
pthread_cancel() 函数被调用,将一个“取消请求”标记设置为目标线程。
目标线程在到达取消点时,会检查是否有取消请求,如果有,则开始执行线程取消操作。
线程取消操作分为两个步骤:设置取消状态和执行取消动作。
设置取消状态:线程首先将自己的“取消状态”设置为 DISABLE,以避免在取消过程中被再次取消。此外,如果线程被取消时正在持有锁,则需要释放这些锁,以避免死锁问题。
执行取消动作:线程执行一些特定的操作来实现线程取消,例如释放资源、清理堆栈、发送信号等。
如果线程没有到达取消点,取消请求将被延迟,直到线程到达下一个取消点。在这种情况下,线程将继续运行,除非取消请求被处理或取消点被触发。
当线程完成取消操作后,它将从线程函数中返回,或者通过调用 pthread_exit() 函数来退出线程。
需要注意的是,线程取消操作可能会导致一些副作用,例如未完成的 I/O 操作、资源泄露、死锁等。因此,在使用 pthread_cancel() 函数时,需要谨慎处理取消操作,并确保及时释放资源。
线程安全
线程安全是指在多个线程并发执行的情况下,程序仍然能够正确地工作,而不会出现数据竞争、死锁、活锁等问题。具有线程安全性的程序能够保证每个线程都可以正确、独立地访问共享资源,并且在多个线程同时访问同一共享资源时,结果与串行执行的结果一致。
在一个线程不安全的程序中,如果多个线程同时访问共享资源,则可能会导致数据损坏、内存泄漏、死锁或其他意外行为。例如,在多个线程之间读写共享变量时,如果没有采用适当的同步机制,就可能会出现竞态条件(race condition)和数据竞争(data race),导致程序产生错误的结果。
因此,编写具有线程安全性的程序是非常重要的,特别是在需要处理多任务和并发访问的场景下。常见的提高程序线程安全性的方法包括使用互斥锁、信号量、条件变量、原子操作等技术来实现对共享资源的同步和互斥访问。