• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

Linux 线程———

武飞扬头像
For Nine
帮助2

1、线程的概念 和 基础知识

1.1 什么是线程

线程可看作轻量级进程(light weight process),Linux的线程本质仍然是进程。Linux先有进程后有线程,当创建了一个进程时,系统给他分配一段4G的虚拟内存,并在其内生成进程的PCB,当他调用相关函数创建一个线程时,会为新的线程生成一个PCB也存放在当前的4G虚拟内存中,而原来的进程也沦为一个线程。

所以,进程和线程的区别是:是否共享地址空间。 进程总是独享4G的虚拟内存,而多个线程共享一段4G的空间。
学新通
线程是CPU调度的最小单位,也是CPU分配时间片的单位,所以,线程越多的应用程序获得CPU的概率也就越大,所以使用多线程能够提高程序的执行效率。而进程可看作只有一个线程的进程,所以单进程应用程序 与 多线程应用程序争夺CPU时并不占优势。

进程是资源分配的最小单位。 一个进程独占4G的虚拟内存,而同意进程创建的多个线程共同使用同一片4G的空间。

1.2 进程 和 线程的关系

类UNIX系统中,进程和线程关系密切。

创建线程使用的底层系统调用和进程一样,都是clone(),只不过创建进程时需要新找一片4G空间,并从原来的4G空间拷贝大部分数据,而创建线程则不需要额外开辟地址空间。一个进程创建线程之后就蜕变为线程。

进程和(进程创建的)线程都有各自的PCB,但PCB中指向内存资源的三级页表(虚拟地址到物理地址的映射)是相同的。

1.3 线程共享的资源

① 文件描述符表
② 信号的处理方式
③ 当前工作目录
④ 用户ID 和 组ID
⑤ 全局变量
⑥ 虚拟内存地址空间:.text/.data/.bss/.heap/共享库(其实就是共享0-3G的空间,除了栈空间 和 errno变量

1.4 线程非共享资源

① 线程ID
② 处理器现场和栈指针(内核栈空间)
③ 独立的栈空间(用户栈空间)
errno变量
⑤ 信号屏蔽字
⑥ 线程的调度优先级

1.5 线程的优缺点

优点:
① 提高程序并发性
② 开销小
③ 数据通信、共享数据方便(不同线程可以使用全局变量)
缺点:
① 线程使用第三方库函数,不稳定
② 代码不好调试,没法用gdb调试
③ 对信号的支持不好

2、线程控制原语

2.1 pthread_self

#inlcude<pthread.h>
pthread_t pthread_self(void);		pthread_t 是 unsigned long 类型,打印的时候要用 %lu

此调用永远不会失败,返回调用的线程ID。线程ID用于在进程中区分不同线程。

2.2 pthread_create

#include<pthread.h>
int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void* (*start_routine)(void*), void* arg);

该函数用于创建一个线程,thread是传出参数,传出线程ID。第二个参数用于指定线程属性,传入NULL表示使用默认属性。

第三个参数是个函数指针,是线程的主控函数,第四个参数是该函数的参数。第四个参数要强制转换成泛型(void*)然后进行值传递即可,不能传递地址。因为子线程有自己的虚拟栈空间用于存放函数中的局部变量,如果去访问原来进程的地址空间,地址中存储的数据可能已经变了。

pthread_create成功返回0,失败直接返回错误号,而不是-1。
注意:创建线程之后,有可能创建它的进程先退出了,那么;进程的存储空间将被回收,线程也就无法执行了。

2.3 pthread_exit

#include<pthread.h>
void pthread_exit(void* retval);

调用该函数的线程会直接退出,retval表示线程的退出值,我们必须将该参数强转为泛型void*

pthread_exit()returnexit()的区别:
  • pthread_exit() 只表示退出当前线程。
  • return 表示退出当前函数,当在main函数中return时,表示退出当前进程;当在线程中return时,表示退出当前线程;当在线程调用的其他函数中return时,只是退出那个函数。
  • exit 无论在哪都表示退出当前进程。

编写多线程程序,一定要谨慎使用returnexit,尽量使用pthread_exit退出线程。

2.4 pthread_join函数回收线程

线程中也存在僵尸线程,所以创建线程后必须用pthread_join回收,其定义如下:

#include<pthread.h>
int pthread_join(pthread_t thread, void** retval);

该函数用于回收线程。任意线程都可以调用该函数来回收其他线程,这点与进程不同,子进程只能由父进程回收。

阻塞等待线程退出,获取线程的退出状态,即,pthread_exit函数里的参数,若线程根本没有调用该函数,那么线程默认返回0退出。

thread是我们要等待退出的线程的线程ID,retval是传出参数,用于获取线程的退出值,即,pthread_exit里的那个参数。
在使用该函数时要注意retval参数:① 一定要用void**强制转换为泛型指针 ② 该函数是将pthread_exit里的退出值 复制到 retval所指向的位置。③ 该参数可以置为NULL,表示不需要获取线程退出值。

该函数成功返回0,失败返回错误号,可能的错误号如下:
学新通

2.5 pthread_detach实现线程分离

线程分离状态: 处于线程分离状态的线程结束后,自己自动被回收(自动清理PCB),无需别的线程调用pthread_join来回收,他们也不应该调用pthread_join

若进程有该机制,则不会产生僵尸进程。因为进程的资源都自动回收干净了,不存在残留。

pthread_cdtach函数定义如下:

#include<pthread.h>
int pthread_detach(pthread_t thread);

thread参数是要设置分离状态的线程ID。该函数成功返回0,失败返回错误号。

当一个线程被设置为分离状态后,就不能使用pthread_join回收他了,如果调用了pthread_join,则会返回错误号。

2.6 pthread_cancel杀死线程

pthread_cancel函数用于杀死线程,其定义如下:

#inlcude<pthread.h>
int pthread_cancel(pthread_t thread);

thread是要杀死的线程ID。该函数成功返回0,失败返回错误号。

该函数要注意两点:
若一个线程被杀死,那么他的退出值是-1,使用pthread_join获取到的退出值是-1。
当程序中执行到pthread_cancel函数时,不会立即杀死相关线程,而是有一定延时。需要等到被杀死的线程自己运行到某个检查点(取消点)时,才真正执行杀死线程的动作。检查点基本都是系统调用,如printf底层调用的writesleep底层调用的pause等。使用man 7 pthreads查看所有的检查点。

pthread_testcancel是一个库函数,他也是一个检查点,该函数调用总是成功的,我们可以调用该函数来人为设置一个检查点。

接收到杀死请求的目标线程可以决定是否允许被杀死,以及如何杀死,这分别由如下两个函数完成:

#include<pthread.h>
int pthread_setcancelstate(int state, int* oldstate);
int pthread_setcanceltype(int type, int* oldtype);

学新通

3、进程 和 线程的控制原语对比

fork()			pthread_create()
exit()			pthread_exit()
wait()			pthread_join()
kill()			pthread_cancel()
getpid()		pthread_self()

4、线程属性

4.1 pthread_attr_t结构体

线程的所有属性都封装在pthread_attr_t结构体里,下面是它的定义:

typedef struct
{
	int 	detachstate;	线程的分离状态
	int 	schedpolicy;	线程调度策略
	int 	schedparam;		线程调度参数
	int 	inheritsched;	线程的继承性
	int 	scope;			线程的作用域
	size_t 	guardsize;		线程末尾的警戒缓冲区大小
	int 	stackaddr_set;	线程的栈设置
	void* 	stackaddr;		线程栈的位置
	size_t 	stacksize;		线程栈的大小
}pthread_attr_t;

线程末尾的警戒缓冲区: 指两个线程各自栈空间之间的间隔区域,这个区域用于防止上面的线程发生栈溢出,从而影响到下面线程的栈空间。我们可以使用guardsize来人为更改警戒缓冲区的大小。

线程栈的大小: 默认情况下,一个进程里的所有线程会均分整个进程的栈空间(8192Kb),我们可以人为设置它的大小。

4.2 设置线程属性

4.2.1 线程属性初始化

当你要自己设置线程的属性时,就需要使用如下函数:

#include<pthread.h>
int pthread_attr_init(pthread_attr_t* attr);	初始化线程属性
int pthread_attr_destroy(pthread_attr_t* attr);	销毁线程属性所占用的资源

这两个函数成对出现,有初始化就必有销毁。而且必须先初始化线程属性再调用pthread_create创建线程。

这两个函数成功返回0,失败返回错误号。

4.2.2 设置线程分离状态

其实既可以设置分离状态,也可以获取线程是否处于分离状态:

#include<pthread.h>
int pthread_attr_setdetachstate(pthread_attr_t* attr, int detachstate);			设置
int pthread_attr_getdetachstate(const pthread_attr_t* attr, int* detachstate);	获取

参数 attr 都是已初始化的线程属性。

参数 detachstate 的值只有两种:

PTHREAD_CREATE_DETACHED		分离状态
PTHREAD_CREATE_JOINABLE		不分离状态

在设置函数中,该参数是一个传入参数;在获取函数中,该参数是一个传出参数,用于获取状态。
想要设置分离状态,必须在调用pthread_create创建线程之前设置好。

两个函数成功返回0,失败返回错误号。

如果设置一个线程为分离状态,而这个线程运行又非常快,他可能在pthread_create函数返回线程ID前就终止并自行回收了,他终止后可能将线程ID和系统资源移交给其他线程使用,这种情况下,调用pthread_create就得到错误的线程ID。使用线程同步措施可以避免这种情况,方法之一就是调用pthread_cond_timedwait函数(后面讲)。单不能使用wait函数,他是使整个进程睡眠,并不能解决线程同步的问题。

5、线程同步

线程同步用来使多个线程对共享资源进行协调访问,使共享资源能够按照正常的逻辑被操作,而不是这个线程操作一会那个线程操作一会,造成共享数据被混乱访问,程序的执行结果未知。

5.1 互斥锁 mutex(互斥量)

每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束后解锁。由于锁只有一个,当一个线程持有锁后,其余线程拿不到锁,所以会阻塞等待。但Linux为我们提供的锁是建议锁,并不强制性线程使用锁,所以,如果又来一个线程不去加锁,而是直接访问全局变量,也能够访问,这样又会产生数据混乱。

5.1.1 互斥锁的相关函数

#include<pthread.h>
int pthread_mutex_init(pthread_mutex_t* restrict mutex, const pthread_mutexattr_t* restrict attr);			动态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;		在全局作用域下,执行该语句与执行上面的函数等价,这种初始化方式称为静态初始化
int pthread_mutex_destroy(pthread_mutex_t* mutex);

int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_trylock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread_mutex_t* mutex);
这五个函数成功返回0,失败返回错误号。

pthread_mutex_t 类型,本质是一个结构体类型。该类型的变量就是锁,所以它一般声明为全局变量,我们把它看成一个整数,只有0,1两种取值,分别表示上锁 和 没上锁。

pthread_mutex_init 函数

该函数用于初始化一个锁。声明一个pthread_mutex_t锁之后不能立即使用,必须传他的地址到该函数里进行初始化(初始完是解锁状态)。第二个参数用于设置锁的属性,置为NULL使用默认属性。

pthread_mutex_destroy 函数

该函数用于销毁一个锁。销毁后的锁就变成了一个未初始化的锁,所以可以重新调用pthread_mutex_init初始化并投入使用。

可以销毁一个初始化但未锁定的锁,但销毁一个已锁定的锁、或 线程正在尝试锁定的锁,又或者另一个线程正在调用pthread_cond_timedwaitpthread_cond_wait的锁,都会导致未定义行为。

pthread_mutex_lock 函数

该函数用于锁定一个锁。可理解为令mutex的值-1。若调用该函数时,锁的值已经是0,那么该线程会被阻塞,直到占用锁的线程解锁。

pthread_mutex_trylock 函数

该函数也用于锁定一个锁。但是该函数的锁定行为是非阻塞的,即,有其他线程锁定该锁时,该函数立即返回,不会等待哪个线程解锁。

pthread_mutex_unlock 函数

该函数用于解锁。

在访问共享资源前加锁,访问结束后 立即解锁 。锁的粒度越小越好。

5.1.2 死锁

一个线程对同一个互斥量加锁两次,那第二次加锁就会被阻塞,线程就卡在这里动不了了
解决方法:加锁后立即解锁

线程1拥有了A锁,请求获得B锁;而线程2获得了B锁,请求获得A锁
解决方法:其中一个线程在请求第二把锁的时候调用非阻塞版本,如果请求失败,那么放弃已经获取的锁,即,当不能获取所有锁时,放弃已经占有的锁。

震荡

5.2 读写锁

读写锁也叫共享-独占锁,它用于对全局数据进行读写操作的情景。读写锁有三个状态,读模式下加锁(读锁),写模式下加锁(写锁),和 不加锁状态。

  • 当锁处于读模式下加锁状态时,有其他线程也尝试以读模式状态加锁时,能够枷锁成功,其他线程以写模式试图加锁时被阻塞,即, 读共享
  • 当锁处于写模式下加锁状态时,其他线程以任何模式尝试加锁都会被阻塞,即, 写独占
  • 当锁处于不加锁状态时,既有尝试以读模式加锁的线程,又有尝试以写模式加锁的线程,那么写模式的线程会加锁成功,即, 写锁优先级高

读写锁的相关函数

#include<pthread.h>
int pthread_rwlock_init(pthread_rwlock_t* restrict rwlock, const pthread_rwlockattr_t* restrict attr);
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITITLIZER;
int pthread_rwlock_destroy(pthread_rwlock_t* rwlock);


int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock);

int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock);

int pthread_rwlock_unlock(pthread_rwlock_t* rwlock);
这7个函数成功返回0,失败返回错误号。

这些函数的使用和互斥锁几乎相同,不再赘述。

读写锁适用于对数据读的次数远大于写的次数 的场景。

5.3 条件变量

条件变量并不是锁,但它能阻塞线程,直至接收到 “条件成立” 的信号后,被阻塞的线程才能继续执行。

5.3.1 条件变量的相关函数

#include<pthread.h>
int pthread_cond_init(pthread_cond_t* restrict cond, const pthread_cond_attr* restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_destroy(pthread_cond_t* cond);

int pthread_cond_wait(pthread_cond_t* restrict cond, pthread_mutex_t* restrict mutex);
int pthread_cond_timedwait(pthread_cond_t* restrict cond, 
							pthread_mutex_t* restrict mutex, 
							const struct timespec* restrict abstime);

int pthread_cond_signal(pthread_cond_t* cond);
int pthread_cond_broadcast(pthread_cond_t* cond);
上述6个函数成功返回0,失败返回错误号。

条件变量的详细讲解,参考这篇博客,我愿称之为最强条件变量入门博客。

这里只说一下pthread_cond_wait函数,理解该函数极其重要。wait函数用于阻塞当前线程,“等待条件成立”。说是“等待条件成立”,但是线程调用了wait只会阻塞,根本没法去检查条件,所以,等待条件成立其实是等待“条件成立”的信号,条件成立后会有人主动告诉他条件成立了。

wait函数也不止阻塞线程这一个动作:线程被阻塞后会立即解锁,这合起来是一个原子操作,不会被其他线程打断。

线程执行了wait后的动作可理解为:即使线程拿到锁了,但是没有收到信号,线程就得等待,等待就还必须解锁,好让别人拿到锁。

当收到“条件成立”的信号后,wait也不会立即解除阻塞,而是先重新上锁,再解除阻塞,这也是一个原子操作,不会被其他线程打断。

5.3.2 条件变量的生产者消费者模型

消费者: 调用waittimedwait阻塞等待产品(“条件成立”的信号)
生产者: 一旦生产出了产品,就调用signalbroadcast通知消费者产品生产好了

其中还涉及到很多加锁解锁的细节。

6、进程间同步

6.1 信号量

上面互斥锁、读写锁以及条件变量都只能用于线程间同步,而信号量既可以用于线程间同步,又可用于进程间同步。

不难发现,与线程 以及线程同步 有关的所有函数,(若有返回值)成功都返回0,失败返回错误号。而下面即将介绍的信号量相关函数将回归正常,即,失败返回-1。

6.1.1 信号量的相关函数

#include<semaphore.h>
int sem_init(sem_t* sem, int pshared, unsigned int value);
int sem_destroy(sem_t* sem);

下面四个函数用于对信号量进行 -- 操作,信号量最小是0,若信号量 = 0,则调用wait会阻塞
int sem_wait(sem_t* sem);
int sem_trywait(sem_t* sem);
int sem_timedwait(sem_t* sem, const struct timespec* abs_timeout);

下面四个函数用于对信号量进行    操作
int sem_post(sem_t* sem);
上述6个函数成功返回0,失败返回-1并设置 errno。

sem_init 函数:
该函数用于初始化一个sem_t类型变量,即信号量。第一个参数sem是传出参数。

第二个参数表示这个信号量是在进程间共享还是线程间共享。若pthread的值为0,那么该信号量将在线程间共享。若pthread的值不为0,那么信号量在进程间共享,并且信号量应该位于共享内存(如mmap建立的映射区)中的某个区域

不能初始化一个已经初始化的信号量。
sem_post 函数:
使信号量增加(解锁)。如果信号量的值因此大于0,那么阻塞在sem_wait调用中的另一个进程或线程将被唤醒并继续锁定信号量。

6.1.2 信号量中的生产者消费者模型

需使用两个信号量,一个是空位数,一个是产品数。

生产者: 生产时需要判断空位数信号量是否为0(使用wait函数),不为0才能生产,否则阻塞,成功生产后需要增加产品数信号量(使用post函数)

消费者: 消费时需要判断产品数信号量是否为0(使用wait函数),不为0才能消费,否则阻塞,成功消费后需要增加空位数信号量(使用post函数)。

6.2 互斥锁

其实互斥锁不仅能用于线程间同步,还能用于进程间同步。这需要在pthread_mutex_init初始化之前修改其属性为进程间共享。

6.2.1 修改mutex属性为进程间共享

相关函数:

pthread_mutexattr_t attr;		声明 mutex 属性结构体
int pthread_mutexattr_init(pthread_mutexattr_t* attr);		初始化 mutex 属性
int pthread_mutexattr_destroy(pthread_mutexattr_t* attr);	销毁 mutex 属性

int pthread_mutexattr_setpshared(pthread_mutexattr_t* attr, int pshared);
pshared 的取值如下:
PTHREAD_PROCESS_PRIVATE		线程锁,mutex 的默认属性
PTHREAD_PROCESS_SHARED		进程锁

6.3 文件锁

这是讲解视频链接,啥时候用到啥时候来学吧。

7、子进程如何处理从父进程继承来的锁??

当父进程调用fork创建子进程后:
即使父进程中有多个线程,子进程也只会继承调用fork的那个线程的代码
子进程的地址空间是父进程地址空间的拷贝,所以会继承父进程中所有的互斥锁、读写锁 和 条件变量, 包括其状态
锁的状态虽然会被继承,但子进程没有任何手段得知锁的状态到底是什么
子进程继承到的锁是一个新锁,虽然有初始状态了,但是 和原来的锁没有任何瓜葛 ,即使父进程中,该锁解锁了,子进程中该锁不会有任何变化。

明确以上4点后,就能理解这个问题了。子进程使用从父进程继承而来的锁很容易导致死锁。 因为,从父进程继承而来的锁有可能是已经上锁了的(与父进程的锁毫无瓜葛,不可能被解锁的),如果子进程再调用lock上锁,那就是一个线程上两次锁,造成死锁。

为了解决这种情况,pthread专门提供了一个函数,pthread_atfork,确保fork调用后父进程和子进程都拥有一个清楚的锁状态,函数定义如下:

int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));

该函数的详解看这里,讲得很不错。

8、线程和信号

Linux中的线程和信号,适配的不是很好,一般用的少,要用的时候去看《Linux高性能服务器编程》的第285页。

9、多线程编程的注意事项

① 在多线程中调用库函数时,一定要使用其可重入版本,否则会导致未定义的结果。(Linux的很多库函数都是不可重入的,但大多都提供了可重入版本,可重入版本在原函数名尾部加上 _r

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhfkbikg
系列文章
更多 icon
同类精品
更多 icon
继续加载