1.多线程概述
多线程就是允许一个进程内存在多个控制权,以便让多个函数同时处于激活状态,从而让多个函数的操作同时运行。
操作系统一般都有一些系统调用来让一个函数运行成为一个新的线程。
多线程的进程在内存中有多个栈,多个栈之间以一定的空白区域隔开,以备栈的增长。每个线程可调用自己栈最下方的帧中的参数和变量,并与其他线程共享内存中的Text、heap和gloabal data区域。
对于多线程来说,由于同一个进程空间中存在多个栈,任何一个空白区域被填满都会导致栈溢出。多线程与栈密切相关。
2.多线程的创建与结束
多线程函数需要包括头文件:
#include<pthread.h>
1.线程的创建
函数原型:
int pthread_create(pthread_t *thread, const pthread_attr_t attr, void(start_routine) (void), void *arg);
pthread_t的定义是typedef unsigned long int pthread_t。
pthread_t其实是unsigned long int来的,是个无符号的长整型。
pthread_create函数第一个参数为指向线程标识符的指针,第二个参数用来设置线程属性,第三个参数是线程运行函数的起始地址,最后一个参数是运行函数的参数。
pthread_create函数的返回值:若线程创建成功,则返回0;若线程创建失败,则返回出错编号,并且*thread中的内容是未定义的。
如果线程调用的函数是在一个类中时,应该把该函数写成静态成员函数。
如果要传一个以上的参数,则应该把它们放到一个结构体里,然后传递这个结构体。
2.等待一个线程的结束
函数原型:
int pthread_join(pthread_t thread, void **retval);
第一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。
这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止, 当函数返回时,被等待线程的资源被收回。
3.线程的结束
一个线程的结束有两种途径:
1.函数已经结束,调用它的线程也就结束了;
2.通过函数pthread_exit来实现。
函数原型:
void pthread_exit(void **retval);
唯一的参数是函数的返回代码。
pthread_join 和pthread_exit的区别:
1.pthread_join 一般是主线程来调用,用来等待子线程退出,因为是等待,所以是阻塞的,一般主线程会依次添加所有它创建的子线程。
2.pthread_exit 一般是子线程调用,用来结束当前线程。
3.子线程可以通过pthread_exit 传递一个返回值,而主线程通过pthread_join获得该返
回值,从而判断该子线程的退出是正常还是异常。
4.获得线程id
两种方式可以打印线程的id:
1.在线程调用函数中使用pthread_self函数来获得线程id;
2.在创建函数时生成的id。
用pthread_create创建的线程id和用pthread_self函数获得的线程id是一样的。
3.线程的属性
线程有一组属性是可以在线程被创建时指定的。该组属性被封装在一个对象中,该对象可用来设置一个或一组线程的属性。线程属性对象的类型为pthread_attr_t。pthread_attr_t包含在pthread.h头文件中。
线程属性结构:
typedef struct
{
int etachstate; //线程的分离状态
int schedpolicy; //线程调度策略
structsched_param schedparam; //线程的调度参数
int inheritsched; //线程的继承性
int scope; //线程的作用域
size_t guardsize; //线程栈末尾的警戒缓冲区大小
int stackaddr_set; //线程的栈设置
void* stackaddr; //线程栈的位置
size_t stacksize; //线程栈的大小
}pthread_attr_t;
属性值不能直接设置,必须使用相关函数进行操作。
初始化的函数为pthread_attr_init,且这个函数必须在pthread_create函数之前调用,之后必须用pthread_attr_destroy函数来释放资源。默认的属性为非绑定、非分离、默认1MB大小的堆栈、与父进程同样级别的优先级。
POSIX.1指定了一系列方法获取和设置pthread_attr_t结构里面的各个属性:
1.分离状态(detached state):若线程终止时,线程处于分离状态,系统将不保留线程终止的状态; 当不需要线程的终止状态时,可以分离线程(调用pthread_detach函数);若在线程创建的时候,就已经知道以后不需要使用线程的终止状态,可以在线程创建属性里面指定该状态,那么线程一开始就处于分离状态。通过下面两个函数,设置和获取线程的分离属性:
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *state);
int pthread_attr_setdetachstate(pthread_attr_t *attr, int state);
该属性的可选值有: PTHREAD_CREATE_DETACHED、PTHREAD_CREATE_JOINABLE。
2.栈地址(stack address):当进程栈地址空间不够用时,指定新建线程使用由malloc分配的空间作为自己的栈空间。通过pthread_attr_setstackaddr和pthread_ attr_getstackaddr两个函数分别设置和获取线程的栈地址。传给pthread_attr_setstackaddr函数的地址是缓冲区的低地址(不一定是栈的开始地址,找可能从高地址往低地址增长)
int pthread_attr_getstackaddr(const pthread_attr_t *attr, void **addr);
int pthread_attr_setstackaddr(pthread_attr_t *attr, void *addr);
3.栈大小(stack size):当系统中有很多线程时,可能需要减小每个线程栈的默认大小,防止进程的地址空间不够用;当线程调用的函数会分配很大的局部变量或者函数调用层次很深时,可能需要增大线程栈的默认大小。
int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *size);
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t size);
函数pthread_attr_getstack和pthread_attr_setstack函数可以同时操作栈地址和栈大小两个属性:
int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr, size_t *size);
int pthread_attr_setstack(pthread_attr_t *attr, size_t size);
4.栈保护区大小(stack guard size):在线程栈顶留出一段空间,防止栈溢出。当栈指针进入这段保护区时,系统会发出错误提示,通常会发送信号给线程。该属性默认值是PAGESIZE的大小,该属性被设置时,系统会自动将该属性大小补齐为页大小的整数倍。当改变栈地址属性时,找保护区大小通常清零。
int pthread_attr_getguardsize(const pthread_attr_t *attr, size_t *guardsize);
int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);
5.线程优先级(priority):新线程的优先级为0。
int pthread_attr_getschedparam(const pthread_attr_t *restrict attr, struct sched_param *restrict param);
int pthread_attr_setschedparam(pthread_attr_t *restrict attr, const struct sched_param *restrict);
6.继承父进程优先级(inheritsched):新线程不继承父线程调度优先级。
7.调度策略(schedpolicy):新线程使用SCHED_OTHER调度策略。线程一旦开始运行, 直到被抢占或者直到线程阻塞或停止为止。
int pthread_attr_setschedpolicy(pthread_attr_t* attr, int policy);
int pthread_attr_setschedparam(pthread_attr_t* attr, struct sched_param* param);
8.争用范围(scope):使用PTHREAD_SCOPE_SYSTEM时,此线程将与系统中的所有线程进行竞争,使用PTHREAD_SCOPE_PROCESS 时,此线程将与进程中的其他线程进行竞争,又称为绑定状态 。具有不同范围状态的线程可以在同一个系统甚至同一个进程中共存。进程范围只允许这种线程与同一进程中的其他线程争用资源,而系统范围则允许此类线程与系统内的其他所有线程争用资源。实际上,从Solaris 9 发行版开始,系统就不再区分这两个范围。
int pthread_attr_getscope(const pthread_attr_t *restrict attr, int *restrict contentionscope);
int pthread_attr_setscope(pthread_attr_t *attr, int contentionscope);
9.线程并行级别(concurrency):应用程序使用pthread_setconcurrency()通知系统其所需的并发级别。
int pthread_getconcurrency(void);
int pthread_setconcurrency(int new_level);
4.多线程同步
1.互斥锁
互斥锁是一个特殊的变量,它有锁上(lock) 和打开(unlock)两个状态。互斥锁一般被设置成全局变量。打开的互斥锁可以由某个线程获得。一旦获得,这个互斥锁会锁上,此后只有该线程有权打开,其他想要获得互斥锁的线程, 会等待直到互斥锁再次打开的时候。
线程在pthread_mutex_lock()和pthread_mutex_unlock()之间操作时,不会被其他线程影响,就构成了一个原子操作。
互斥锁机制需要程序员自己来写出完善的程序来实现互斥锁的功能。
锁的创建有两种方式,静态和动态:
静态方式:
pthread_mutex_t mutex_x = PTHREAD_MUTEX_INITIALIZER;
动态方式:
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
加锁:
int pthread_mutex_lock(pthread_mutex_t *mutex);
解锁:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
测试加锁:
int pthread_mutex_trylock(pthread_mutex_t *mutex);
pthread_mutex_trylock()语义与pthread_mutex_lock()类似,不同的是在锁已经被占据时返回EBUSY,而不是挂起等待。
2.条件变量
条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其他的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程,这些线程将重新锁定互斥锁并重新测试条件是否满足。
条件变量相关函数:
1)创建:条件变量和互斥锁一样,都有静态、动态两种创建方式。
静态:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER
动态方式:
int pthread_cond_init(pthread_cond_t *cond,pthread_cond_attr_t *cond_attr)
使用cond_attr指定的属性初始化条件变量cond时,当cond_attr为NULL时,使用默认的属性。LinuxThreads实现条件变量不支持属性,因此cond_attr参数实际被忽略。
2)注销:注销一个条件变量需要调用pthread_cond_destroy(),它的函数原型是:
int pthread_cond_destroy(pthread_cond_t *cond)
只有在没有线程在该条件变量上等待的时候才能注销这个条件变量,否则返回EBUSY。
3)等待:等待条件有两种方式:
条件等待pthread_cond_wait()
计时等待pthread_cond_timedwait()
无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()(或pthread_cond_timedwait())的竞争条件。mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁(PTHREAD_MUTEX_ADAPTIVE_NP),且在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前, mutex需保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。
函数原型:
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime)
4)激发:
激发条件有两种形式:
pthread_cond_signal()激活一个等待该条件的线程,存在多个等待线程时按人队顺序激活其中一个;
pthread_cond_broadcast()则激活所有等待线程。
使用pthread_cond_signal 不会有“ 惊群现象”(每当有资源可用,所有的进程/线程都来竞争资源)产生,它最多只给一个线程发信号。假如有多个线程正在阻塞等待着这个条件变量的话,那么根据各等待线程优先级的高低确定哪个线程会接收到信号并开始继续执行;如
果各线程优先级相同,则根据等待时间的长短来确定哪个线程获得信号。
一个pthread_cond_signal调用最多发一次信号。
条件变量特别适用于多个线程等待某个条件的发生。
3.读写锁
一种是访问必须是排他性的,就是独占的意思,这称作写操作;另一种情况就是访问方式可以是共享的,就是说可以有多个线程同时去访问某个资源,这种就称作读操作。
读写锁比起互斥锁具有更高的适用性与并行性,可以有多个线程同时占用读模式的读写锁,但是只能有一个线程占用写模式的读写锁。
1)当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。
2)当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是以写模式对它进行加锁的线程将会被阻塞。
3)当读写锁在读模式的锁状态时,如果有另外的线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁的请求。
读写锁最适用于对数据结构的读操作次数多于写操作次数的场合,读写锁也可以叫作共享-独占锁。
读写锁机制是由POSIX提供的,如果写者没有持有读写锁,那么所有的读者都可以持有这把锁,而一旦有某个写者阻塞在上锁的时候,那么就由POSIX系统来决定是否允许读者获取该锁。
读写锁相关的函数:
1)初始化和销毁读写锁
一种是通过给一个静态分配的读写锁赋予常值PTHREAD_RWLOCK_INITIALIZER来初始化它;另一种方法就是通过调用pthread_rwlock_init()来动态地初始化。而当某个线程不再需要读写锁的时候,可以通过调用pthread_rwlock_destroy来销毁该锁。函数原型如下:
int pthread_rwlock_init(pthread_rwlock_t *rwptr, const pthread_rwlockattr_t *attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwptr);
这两个函数如果执行成功均返回0,如果出错则返回错误码。
在释放某个读写锁占用的内存之前,要先通过pthread_rwlock_destroy对读写锁进行清理,释放由pthread_rwlock_init所分配的资源。
在初始化某个读写锁的时候,如果属性指针attr是个空指针的话, 表示使用默认属性;
使用非默认属性,使用下面的两个函数:
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
当初始化读写锁完毕以后,该锁就处于一种非锁定状态。
数据类型为pthread_ rwlockattr_t 的某个属性对象一旦初始化了,就可以通过不同的函数调用来启用或禁用某个特定的属性。
2)获取和释放读写锁
pthread_rwlock_rdlock()用来获取读出锁,如果相应的读出锁已经被某个写入者占有,那么就阻塞调用线程。
pthread_rwlock_wrlock()以获取一个写入锁,如果相应的写入锁已经被其他写人者或者读出者占有(一个或多个),那么就阻塞该调用程; pthread_rwlock_unlock()用来释放一个读出或者写入锁。
函数原型:
int pthread_rwlock_rdlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_unlock(pthread_rwlock_t *rwptr);
其中获取锁的两个函数的操作都是阻塞操作,也就是说获取不到锁的话,那么调用线程不是立即返回,而是阻塞执行。
用非阻塞方式获取读写锁的函数:
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwptr);
非阻塞方式下获取锁的时候,如果不能马上获取到,就会立即返回一个EBUSY错误提示,而不是把调用线程投入到睡眠等待。
4.信号量
信号量和互斥锁的区别:
互斥锁只允许一个线程进入临界区,而信号量允许多个线程同时进入临界区。
要使用信号量同步,需要包含头文件semaphore.h。
信号量函数的名字都以”sem_”打头。
将信号量定义为全局变量,方便多个线程共享。
线程使用的基本信号量函数:
1)sem_init函数
该函数用于创建信号量,函数原型:
int sem_init(sem_t *sem, int pshared, unsigned int value);
该函数用于初始化由sem指向的信号对象,设置它的共享选项,并给它一个初始的整数值。pshared 控制信号量的类型,如果其值为0 ,就表示这个信号量是当前进程的局部信号量,否则信号量就可以在多个进程之间共享。value为sem的初始值。调用成功时返回0,失败返回-1。
2)sem_wait函数
该函数用于以原子操作的方式将信号量的值减1。原子操作就是,如果两个线程企图同时给一个信号量加1或减1 ,它们之间不会互相干扰。
函数原型:
int sem_wait(sem_t *sem);
sem指向的对象是由sem_init调用初始化的信号量。调用成功时返回0,失败返回-1。
3)sem_post函数
该函数用于以原子操作的方式将信号量的值加1。
函数原型:
int sem_post(sem_t *sem);
sem指向的对象是由sem_init调用初始化的信号量。调用成功时返回0,失败返回-1。
4)sem_destroy函数
该函数用于对用完的信号量进行清理。
函数原型:
int sem_destroy(sem_t *sem);
成功时返回0,失败时返回-1。
5.多线程重入
可重入函数:是指可以由多于一个任务并发使用,而不必担心数据错误的函数。
不可重入函数:只能由一个任务所占用,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。
可重入函数可以在任意时刻被中断,稍后再继续运行,且不会丢失数据。可重人函数要在使用本地变量或在使用全局变量时保护自己的数据。
可重入函数有以下特点:
1)不为连续的调用持有静态数据。
2)不返回指向静态数据的指针。
3)所有数据都由函数的调用者提供。
4)使用本地数据,或者通过制作全局数据的本地副本来保护全局数据。
5)如果必须访问全局变量,要利用互斥锁、信号量等来保护全局变量。
6)绝不调用任何不可重人函数。
不可重入函数有以下特点:
1)函数中使用了静态变量,无论是全局静态变量还是局部静态变量。
2)函数返回静态变量。
3)函数中调用了不可重人函数。
4)函数体内使用了静态的数据结构。
5)函数体内调用了malloc()或者的free()函数。
6)函数体内调用了其他标准I/O函数。
在一个多线程程序里,默认情况下,只有一个errno变量供所有的线程共享。
可重入代码可以被多次调用而仍然正常工作。
编写的多线程程序,通过定义宏_REENTRANT来告诉编译器需要可重人功能,这个宏的定义必须出现于程序中的任何#include 语句之前。