进程中的多个线程间切换开销比较大,你知道吗?

1、什么是线程

线程是参与系统调度的最小单位(线程是程序最基本的运行单位)。它包含在进程中,是进程中实际运行的单元(实际运行的是进程中的线程)。一个进程中可以创建多个线程,多个线程可以并发运行,每个线程执行不同的任务。

注意:可以认为进程只是一个容器,里面包含了线程操作所需的数据结构、环境变量等信息。

同一进程中的多个线程会共享该进程中的所有系统资源,例如虚拟地址空间、文件描述符和信号处理等。但是同一进程中的多个线程有自己的调用栈(调用栈,我们称之为它们是线程堆栈)、它们自己的寄存器上下文(registercontext)和它们自己的线程本地存储(thread-local storage)。

2、主线程和子线程

任何进程都包含一个主线程,只有主线程的进程称为单线程进程。

1)其他新线程(即子线程)由主线程创建;

2)主线程通常在最后结束,执行各种清理任务,比如回收单个子线程。

2.1、单线程程序和多线程进程

单线程程序:只有主线程。

多线程程序:除主线程外,还包含其他线程。其他线程通常由主线程创建(调用

pthread_create 创建一个新线程),新创建的线程是主线程的子线程。

3、多进程编程和多线程编程

方法

优势

缺点

多进程编程

1、各个进程相互独立;

2、可以通过增加CPU轻松扩展性能;

3、可以将线程加锁/解锁的影响降到最低,大大提升性能;

4、每个子进程有2GB地址空间和相关资源,整体性能可以达到上限很大

1、进程间切换成本高。

2、进程间通信比较麻烦。

多线程编程

1、同一进程中多个线程之间的切换开销比较小

2、同一进程中的多个线程线程之间的通信很容易。它们共享进程的地址空间,所以它们都在同一个地址空间,通信方便。

3、创建线程比创建进程快得多。

4、多线程在多核处理器上更有优势

多线程编程很困难。在多线程环境下,需要考虑很多问题,比如线程安全问题、信号处理问题等等。

注意:多进程编程通常用于一些大型应用项目,如网络服务器应用,很少用于中小型应用。

4、线程ID

每个线程也有其对应的标识符,称为线程ID。进程 ID 在整个系统中是唯一的,但线程 ID 是不同的,并且线程 ID 仅在它们所属的进程的上下文中才有意义。进程 ID 使用 pid_t 数据类型表示,它是一个非负整数。线程 ID 使用 pthread_t 数据类型表示。

1) 很多线程相关的函数(pthread_cancel/pthread_detach/pthread_join等)使用线程ID来标识要操作的目标线程;

2) 在某些应用程序中,使用特定线程的线程 ID 作为动态数据结构的标签很有用,既可以标识整个数据结构的创建者或所有者线程,也可以确定后续数据结构执行操作的具体线程。

5、pthread_self() 函数

获取线程 ID。

#include 
pthread_t pthread_self(void);

返回值:返回当前线程的线程ID。

6、pthread_equal() 函数

检查两个线程 ID 是否相等。

#include 
int pthread_equal(pthread_t t1, pthread_t t2);

参数t1:线程ID t1

参数t2:线程ID t2

返回值:如果两个线程ID t1和t2相等,pthread_equal()返回一个非零值;否则返回 0

7、pthread_create() 函数

创建一个新线程,创建的新线程称为主线程的子线程。

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

参数thread:保存新创建线程的线程ID。

参数attr:线程属性。如果参数attr设置为NULL,则表示线程的所有属性都设置为默认值。

参数start_routine:线程入口函数

参数arg:线程入口函数参数。

返回值:成功返回0;失败返回错误号,参数thread指向的内容未定义。

注意:pthread_create() 通常在调用失败时返回错误代码。它不像其他库函数或系统调用那样设置 errno。每个线程都提供了一个全局变量errno的副本,只是为了兼容使用errno的函数。

8、pthread_exit() 函数

调用pthread_exit()相当于在线程函数中执行return语句。该函数调用 pthread_exit() 来终止线程。如果主线程调用pthread_exit(),主线程也会终止,但其他线程仍会正常运行,直到进程中的所有线程都终止后进程才会终止。

#include 
void pthread_exit(void *retval);

参数retval:是一个无类型指针,进程中的其他线程也可以通过调用pthread_join来访问这个指针。指针的内容不应该分配在线程栈上,因为线程终止后,没有办法判断线程栈的内容是否有效;同理,线程函数的返回值也不应该分配在线程栈上。

注意:如果进程中的任何线程调用exit()、_exit()或_Exit(),都会导致整个进程终止。

9、pthread_join() 函数

阻塞并等待线程终止,并获取线程的退出码回收线程资源;

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

参数thread:通过线程ID指定要等待的线程;

参数retval:如果参数retval不为NULL,pthread_join()会将目标线程的退出状态复制到*retval指向的内存区域;如果目标线程被 pthread_cancel() 取消,则将 PTHREAD_CANCELED 放入 *retval。

返回值:成功返回0;失败会返回错误码。

注意:调用 pthread_join() 函数将阻塞等待指定线程终止。如果线程已终止,pthread_join() 将立即返回。如果多个线程同时尝试调用 pthread_join() 来等待指定线程终止,结果将是不确定的。如果线程未分离(detached),则必须使用pthread_join()等待线程终止并回收线程资源;如果线程终止后其他线程不调用pthread_join()函数回收线程,该线程将成为僵尸线程,类似于僵尸进程的概念;同样,如果僵尸线程积累太多,除了浪费系统资源外,僵尸线程将无法创建新线程。当然,如果进程中有僵尸线程没有被回收,当进程终止时,该进程会被其父进程回收,所以僵尸线程也会被回收。

1) 线程之间的关系是点对点的。进程中的任何线程都可以调用 pthread_join() 函数来等待另一个线程的终止。这与进程之间的层次关系不同。如果父进程使用 fork() 创建子进程,它也是唯一可以在子进程上调用 wait() 的进程。线程之间没有这种关系。

2) pthread_join() 不能以非阻塞方式调用。对于一个进程,调用waitpid()可以实现阻塞或非阻塞等待。

10、pthread_cancel() 函数

取消同一进程中的其他线程。

#include 
int pthread_cancel(pthread_t thread);

参数thread:线程ID。

返回值:如果成功,返回0;否则返回错误码。

注意:取消请求发出后,函数 pthread_cancel() 立即返回,无需等待目标线程退出。默认情况下,目标线程也将立即退出,其行为就像调用了带有参数 PTHREAD_CANCELED(实际上是 (void *)-1))的 pthread_exit() 函数。但是,线程可以设置自己不被取消或控制如何取消,因此 pthread_cancel() 不会等待线程终止,它只是发出请求。

11、pthread_setcancelstate()

设置调用线程状态的可取消性。

#include 
int pthread_setcancelstate(int state, int *oldstate);

参数state:设置线程的可取消状态。

PTHREAD_CANCEL_ENABLE

线程可以被取消,这是新建线程的可取消状态的默认值。

PTHREAD_CANCEL_DISABLE

线程不能被取消,如果这样的线程收到取消请求,请求会被挂起,直到线程的取消状态变为PTHREAD_CANCEL_ENABLE

参数oldstate:保存线程之前的取消状态。如果对之前的状态不感兴趣线程标识符 有什么用,可以设置为NULL

返回值:调用成功返回0,否则返回非零错误码。

12、pthread_setcanceltype() 函数

设置调用线程的取消类型。

#include 
int pthread_setcanceltype(int type, int *oldtype);

参数类型:

PTHREAD_CANCEL_DEFERRED

当取消请求到达时,线程继续运行,取消请求被挂起,直到线程这是所有新创建的线程的默认取消类型,包括主线程,直到达到某个取消点。

PTHREAD_CANCEL_ASYNCHRONOUS

可以在任何时候取消(可能立即,但不是必须)取消线程

返回值:调用成功返回0,调用失败返回非零错误码。

注意:当线程调用fork()创建子进程时,子进程会继承调用线程的取消状态和取消类型,当线程调用exec函数时,会重置取消状态并将新程序的主线程类型改为默认值,即PTHREAD_CANCEL_ENABLE和PTHREAD_CANCEL_DEFERRED。

12.1、@ >取消点

如果线程的取消类型设置为PTHREAD_CANCEL_DEFERRED(线程可以取消状态),当接收到其他线程发送的取消请求时,只有当线程到达某个取消点时,才会取消该请求。

取消点其实就是一系列函数。当这些函数被执行时,取消请求将被实际响应。这些函数就是取消点(原因是系统认为当没有达到取消点时,线程正在执行的工作无法停止,并且正在执行关键代码,此时终止线程可能会导致意外的异常)。

12.1.1、查看取消点功能

man 7 pthreads -> 取消点

12.1.2、pthread_testcancel()函数

生成一个取消点。

#include 
void pthread_testcancel(void);

13、线程分离

线程一旦被分离,就不能再使用pthread_join()终止状态访问,这个过程是不可逆的,一旦处于分离状态,就不能恢复到之前的状态。处于分离状态的线程可以在终止时自动回收线程资源。

13.1、pthread_detach() 函数

分离线程的函数。

#include 
int pthread_detach(pthread_t thread);

参数thread:线程ID。

返回值:如果成功,返回0;否则返回错误码。

13.2、注册线程清理处理程序

当线程终止并退出时,应该执行此处理程序。

当出现以下三种情况之一时,执行注册的清理函数:

1)调用 pthread_exit。

2)作为对取消线程请求 (pthread_cancel) 的响应。

3)使用非零参数调用 pthread_cleanup_pop。

13.2.1、pthread_cleanup_push() 函数

注册清理功能。

#include 
void pthread_cleanup_push(void (*function)(void *), void *arg);

参数function:调用线程取消时锁调用的函数的地址。

参数arg:参数。

13.2.2、pthread_clean_pop() 函数

从调用线程的取消清理栈中移除栈顶函数。

#include 
void pthread_clean_pop(int execute);

参数execute:如果不为0,则调用注册函数。

14、线程属性

名字

说明

分离状态

线程属性的分离状态

警卫尺寸

线程栈尾的保护缓冲区大小(字节数)

堆栈地址

线程栈的最低地址

堆栈大小

线程栈的最小长度(字节数)

14.1、pthread_attr_init()函数

初始化一个线程属性。

#include 
int pthread_attr_init(pthread_attr_t *attr);

参数attr:线程属性。

返回值:如果成功,返回0;否则,返回错误号。

14.2、pthread_attr_destroy() 函数

销毁线程属性。

#include 
int pthread_attr_destroy(pthread_attr_t *attr);

参数attr:线程属性。

返回值:如果成功,返回0;否则,返回错误号。

14.3、pthread_attr_setstack()函数

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

参数attr:参数attr指向线程属性对象。

参数stackaddr:设置堆栈起始地址为指定值。

参数stacksize:设置堆栈大小为指定值。

返回值:成功返回0,失败返回0以外的错误码。

14.4、pthread_attr_getstack()函数

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

参数attr:指向线程属性对象。

参数stackaddr:堆栈地址;

参数stacksize:堆栈大小;

返回值:成功返回0,失败返回非零错误码。

14.5、pthread_attr_setstacksize() 函数

设置线程stacksize属性。

#include 
 
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);

参数attr:线程属性。

参数stacksize:设置大小。

返回值:如果成功,返回0;否则,返回错误号。

14.6、pthread_attr_getstacksize()函数

获取线程的stacksize属性。

#include 
 
int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);

参数attr:线程属性。

参数stacksize:大小。

返回值:如果成功,返回0;否则,返回错误号。

14.7、pthread_attr_setstackaddr()函数

获取线程的stackaddr属性。

#include 
 
int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr);

参数attr:线程属性。

参数stackaddr:设置地址。

返回值:如果成功,返回0;否则,返回错误号。

14.8、pthread_attr_getstackaddr()函数

#include 
 
int pthread_attr_getstackaddr(const pthread_attr_t *attr, void **stackaddr);

图片[1]-进程中的多个线程间切换开销比较大,你知道吗?-老王博客

参数attr:线程属性。

参数stackaddr:获取地址。

返回值:如果成功,返回0;否则,返回错误号。

14.9、pthread_attr_setdetachstate()函数

设置线程分离状态。

#include 
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

参数attr:要设置的属性

参数detachstate:可选PTHREAD_CREATE_DETACHED(分离线程)和PTHREAD_CREATE_JOINABLE(非分离线程)

返回值:成功0.任何其他返回值表示发生了错误。

14.10、pthread_attr_getdetachstate() 函数

获取线程分离状态。

#include 
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);

参数attr:要获取的属性

参数detachstate:保存获取到的状态。

返回值:成功返回0.任何其他返回值表示发生了错误。

14.11、为什么要修改线程栈大小

对于一个进程,虚拟地址空间的大小是固定的。由于一个进程中只有一个堆栈,它的大小通常不是问题。但是对于线程来说,相同大小的虚拟地址空间必须被所有线程栈共享。

1)如果你的应用程序使用了太多线程,以至于这些线程堆栈的累积大小超过了可用的虚拟地址空间,你需要减少默认的线程堆栈大小。

2)如果一个线程调用一个分配大量自动变量的函数,或者调用一个涉及很多深栈帧的函数,需要的栈大小可能会大于默认的big。

15、线程安全

15.1、可重入函数

如果一个函数被同一个进程同时调用多个不同的执行流,而每个函数调用总能产生正确的结果(或预期的结果),这样的函数称为可重入函数。

注意:不可重入函数通常存在一定的安全风险。在多线程环境和与信号处理相关的应用中,需要注意不可重入函数的问题。如果多个执行流同时调用一个不可重入函数,可能无法得到预期的结果,甚至可能导致程序崩溃!

15.2、线程安全函数

当一个函数被多个线程同时调用时,它总是会产生正确的结果。函数称为线程安全函数。线程安全函数包括可重入函数,而可重入函数是线程安全函数的真子集,即可重入函数必须是线程安全函数,但线程安全函数不一定是可重入函数。

15.3、判断库函数是否为线程安全函数

man 手动查看库函数ATTRIBUTES信息,如果函数标记为MT-Safe,则表示该函数是线程安全函数,如果标记为MT-Unsafe,则表示该函数是非线程安全函数-线程安全函数。

注意:在多线程编程环境中,需要特别注意。如果一个函数可能被多个线程同时调用,则该函数不能是非线程安全的函数。它必须是线程安全的函数,否则会出现。出乎意料的结果,甚至让整个程序崩溃!

15.4、条件可重入函数的标签

man 7 属性

环境

这个函数会在内部读取进程的一些/一些环境变量,以及这种读取(但不改变)全局变量的可重入函数应该满足的条件;

本地

通常这种类型的函数传入一个指针。上面我们也提到过传入指针的可重入函数需要满足哪些条件才能重入,这里不再赘述

15.5、线程安全函数和可重入函数的判断方法

可重入函数只是从语言语法的角度分析了它的复用性,不涉及一些具体的实现机制,比如线程同步技术,这就是判断可重入函数和线程安全函数的区别。

1)判断一个函数是否是线程安全函数的方法是该函数在被多个线程同时调用时是否总能产生正确的结果,是否每次都能产生预期的结果time, then 表示该函数是线程安全函数。

2)判断一个函数是否为可重入函数的方法是,从语言语法的角度来看,当函数被多个执行流同时调用时,函数是否总能产生正确的结果,如果每个如果每次都产生预期的结果,则该函数是可重入函数。

15.6、查看线程安全函数

man7 pthreads ->线程安全函数

POSIX.1-2001 和 POSIX.1-2008 标准中指定的所有函数必须是线程安全的,但以下函数除外

asctime()

基本名称()

猫()

crypt()

ctermid()

ctime()

dbm_clearerr()

dbm_close()

dbm_delete()

dbm_error()

dbm_fetch()

dbm_firstkey()

dbm_nextkey()

dbm_open()

dbm_store()

目录名()

dlerror()

drand48()

ecvt()

加密()

endgrent()

endpwent()

endutxent()

fcvt()

ftw()

gcvt()

getc_unlocked()

getchar_unlocked()

getdate()

getenv()

getgrent()

getgrgid()

getgrnam()

gethostbyaddr()

gethostbyname()

gethostent()

getlogin()

getnetbyaddr()

getnetbyname()

getnetent()

getopt()

getprotobyname()

getprotobynumber()

getprotoent()

getpwent()

getpwna m()

getpwuid()

getservbyname()

getservbyport()

getservent()

getutxent()

getutxid()

getutxline()

gmtime()

hcreate()

hdestroy()

hsearch()

inet_ntoa()

l64a()

lgamma()

lgammaf()

lgammal()

localeconv()

本地时间()

lrand48()

mrand48()

nftw()

nl_langinfo()

ptsname()

putc_unlocked()

putchar_unlocked()

putenv()

pututxline()

兰德()

readdir()

setenv()

setgrent()

setkey()

setpwent()

setutxent()

strerror()

strsignal()

strtok()

系统()

tmpnam()

ttyname()

unsetenv()

wcrtomb()

wcsrtombs()

wcstombs()

wctomb()

16、一次性初始化16.1、pthread_once()函数

在多线程编程环境下,虽然pthread_once()调用会出现在多个线程中,但是这个函数会保证init_routine()函数只执行一次,并且不确定在哪个线程中执行,这取决于内核调度。

注意:如果一个线程调用 pthread_once() 并且另一个线程也调用了 pthread_once(),则该线程将被阻塞,等待第一个线程完成初始化并返回。也就是说,当pthread_once调用成功返回时线程标识符 有什么用,调用始终可以确定所有状态都已经初始化完毕。

#include 
int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));

参数 once_control:这是一个 pthread_once_t 类型的指针。在调用pthread_once()函数之前,需要定义一个pthread_once_t类型的静态变量并调用pthread_once(),参数once_control指向这个变量。通常变量在定义时使用 PTHREAD_ONCE_INIT 宏进行初始化。

pthread_once_t once_control = PTHREAD_ONCE_INIT;

注意:如果参数once_control指向一个初始值不是PTHREAD_ONCE_INIT的pthread_once_t类型的变量,pthread_once()的行为就会异常。

参数init_routine:函数指针,参数init_routine所指向的函数是只需要执行一次的代码段。次,但它保证 init_routine() 只执行一次。

返回值:调用成功则返回0;如果失败,它会返回一个错误代码来指出错误的原因。

17、线程特定数据

线程特定数据也称为线程私有数据,这意味着为每个调用线程维护变量的副本。 ),当每个线程通过唯一的数据键(key)进行访问时,这个唯一的数据键将获得线程绑定的变量的副本。这样可以避免变量成为多个线程之间的共享数据。

17.1、pthread_key_create() 函数

在给线程分配私有数据区之前,需要调用pthread_key_create()函数来创建唯一的数据键,并且只需要在第一个调用线程中创建一次,通常使用pthread_once()函数.

#include 
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));

参数key:调用该函数会创建一个唯一的数据key,并通过参数key指向的缓冲区返回给调用者。

参数析构函数:调用pthread_key_create()函数可以让调用者指定自定义析构函数,这样

指向带有参数析构函数的函数;该函数通常用于释放与特定数据键相关联的线程私有数据区占用的内存空间。当使用线程特定数据的线程终止时,会自动调用 destructor() 函数。

返回值:成功返回0;失败时返回一个错误号以指示错误的原因。

17.2、pthread_setspecific() 函数

设置调用线程的私有数据区。

#include 
int pthread_setspecific(pthread_key_t key, const void *value);

@​​>

参数key:pthread_key_create()函数创建的唯一数据key。

参数值:指向调用者分配的一块内存,作为线程的私有数据缓冲区。当线程终止时,会自动调用参数key指定的唯一数据key对应的解构函数,释放这块内存动态分配的内存空间。

返回值:成功返回0;失败会返回错误码。

17.3、pthread_getspecific() 函数

返回与当前调用线程的特定数据键关联的私有数据缓冲区。

#include 
void *pthread_getspecific(pthread_key_t key);

参数key:pthread_key_create()函数创建的唯一数据key。

返回值:

1)如果线程私有数据缓冲区设置为关联唯一数据键,则返回调用线程的私有数据区。

2)如果当前调用线程没有设置线程私有数据缓冲区关联唯一数据键,返回值应该为NULL(这个可以在函数中用来判断当前调用线程是否是第一次调用这个函数,如果是第一次调用,必须为线程分配一个私有的数据缓冲区)

17.4、pthread_key_delete()函数

删除先前由 pthread_key_create() 创建的密钥

#include 
int pthread_key_delete(pthread_key_t key);

参数key:要删除的key。

返回值:函数调用成功返回0,失败返回错误号。

注意:调用pthread_key_delete()时,不会检查线程当前是否在使用key关联的线程私有数据缓冲区,因此不会触发key的解构函数,即内存与key关联的线程私有数据区占用的资源不会被释放,调用pthread_key_delete()后,线程终止时不会执行key的解构函数。所以,一般来说,在调用pthread_key_delete()之前,必须保证以下条件:

1) 所有线程都释放了私有数据区(显式调用析构函数或线程终止)。

2)参数key指定的唯一数据key将不再使用。

注意:任何在调用 pthread_key_delete() 后使用键的操作都将导致未定义的行为。

17.5、线程本地存储

通常,程序中定义的全局变量是进程中所有线程共享的,所有线程都可以访问这些全局变量;线程局部存储用于定义全局或静态变量,而 __thread 修饰符用于修改变量。这时,每个线程都会有一个变量的副本(每个线程对自己变量的操作不会影响其他线程)。线程本地存储中的变量一直存在,直到线程终止,此时该存储被自动释放。

注意:线程本地存储的主要优点是它比线程特定的数据更易于使用。要创建线程局部变量,只需在全局或静态变量的声明中包含 __thread 修饰符!

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论