* 多线程的管理
* 多线程的互斥锁和条件变量的使用!
什么是进程
进程(process )是一个已经开始执行但还没终止的程序实例。
Linux系统下使用ps 命令可以查看到当前正在执行的进程。每个进程包含有进程运行环境、内存地址空间、进程ID、和至少一个被称为线程的执行控制流等资源。同一个程序可以实例化为多个进程实体。操作系统中所有进程实体共享着计算机系统的 CPU、外设等资源。什么是线程
线程(thread)是包含在进程内部的顺序执行流,是进程中的实际运作单位,也是操作系统能够进行调度的最小单位。一个进程中可以并发多条线程,每条线程并行执行不同的任务。
所以进程和线程的关系如下:线程与进程的关系
线程与进程的关系可以归结为以下几点:
一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个主线程;
资源分配给进程,同一进程的所有线程共享该进程的所有资源;
线程作为调度和分配的基本单位,进程作为拥有资源的基本单位;
进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源;
在创建或撤销进程时,由于系统要为之分配和回收资源,导致系统的开销大于创建或撤销线程时的开销。
为什么要使用多线程?
多进程程序结构和多线程程序结构有很大的不同,多线程程序结构相对于多进程程序结构有以下的优势:
(1 )方便的通信和数据交换
线程间有方便的通信和数据交换机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。
(2 )更 高 效的利用 CPU
使用多线程可以加快应用程序的响应。这对图形界面的程序尤其有意义,假如一个操作耗时很长,那么整个系统都会等它操作,此时程序不会响应键盘、鼠标、菜单等操作,而使用多线程技术,将耗时长的操作置于一个新的线程,就可以避免这种尴尬情况的发生。同时多线程使多 CPU 系统更加有效。操作系统会保证当线程数不大于 CPU 数目时,不同的线程运行于不同的 CPU 上。
Pthreads定义了创建和操纵线程的一套 API 接口,一般用于 Unix-like POSIX 系统中(如 FreeBSD、GNU/Linux、OpenBSD、Mac OS 等系统)。
Pthreads 接口可以根据功能划分四个组:
线程管理
互斥量
条件变量
同步
(1)线程的应用和编译:
编写 Pthreads 多线程的程序时,源码只需要包含 pthread.h 头文件就可以使用Pthreads库中的所有类型及函数:
#include
在编译 Pthread 程序时在编译和链接过程中需要加上-pthread 参数:
LDFLAGS += -pthread
(2)线程的管理--对id的创、终、等、分、设的管理操作:
1/5).创建线程(线程被创建后将立即运行)
在进程中创建一个新线程的函数是 pthread_create(),原型如下:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
参数说明:
thread 用来指向新创建线程的 ID;
attr 用来表示一个封装了线程各种属性的属性对象,如果 attr为NULL,新线程就使用默认的属性;
start_routine 是线程开始执行的时候调用的函数的名字,start_routine 函数有一个指向 void 的指针参数,并有 pthread_create 的第四个参数 arg 指定值,同时 start_routine函数返回一个指向 void 的指针,这个返回值被 pthread_join 当做退出状态处理;
arg 为参数 start_routine 指定函数的参数。
2/5).终止线程(区分终止线程和终止进程的差别)
终止线程:如果主线程在创建了其它线程后没有任务需要处理,那么它应该阻塞等待直到所有线程都结束为止,或者应该调用 pthread_exit(NULL)。
[ 终止进程:进程的终止可以通过直接调用 exit()、执行 main()中的 return、或者通过进程的某个其它线程调用 exit()来实现。在以上任何一种情况发生时,所有的线程都会被终止。]
调用 exit()函数会使整个进程终止,而调用pthread_exit()只会使得调用线程终止,同时在创建的线程的顶层执行 return 线程会隐式地调用 pthread_exit()。pthread_exit()函数原型如下:void pthread_exit(void *retval);
retval 是一个 void 类型的指针,可以将线程的返回值当作 pthread_exit()的参数传入,这个值同样被 pthread_join()当作退出状态处理。如果进程的最后一个线程调用了 pthread_exit(),进程会带着状态返回值 0 退出;
3/5).线程的分离与连接-线程可以分为 分离线程(DETACHED)和 非分离线程(JOINABLE)两种:
分离线程是退出时会释放它的资源的线程;
非分离线程退出后不会立即释放资源,需要另一个线程为它调用 pthread_join函数或者进程退出时才会释放资源。
只有非分离线程才是可连接的,分离线程退出时不会报告它的退出状态。
a.线程分离-pthread_detach()函数可以将非分离线程设置为分离线程,函数原型如下:
int pthread_detach(pthread_t thread);//参数 thread 是要分离的线程的 ID。线程可以自己来设置分离,也可以由其它线程来设置分离,以下代码线程可设置自身分离:
pthread_detach( pthread_self() );//成功返回 0;失败返回一个非 0 的错误码
b.线程连接-如果一个线程是非分离线程,那么其它线程可调用 pthread_join()函数对非分离线程进行连接。
pthread_join()函数原型如下:int pthread_join(pthread_t thread, void **retval);
pthread_join()函数将调用线程挂起,直到参数 thread 指定的目标线程终止运行为止。
参数 retval 的作用是为指向线程的返回值的指针提供一个位置,这个返回值是目标线程调用 pthread_exit()或者 return 后所返回的值。当目标线程无需返回时可使用 NULL 值,调用线程如果不需对目标线程的返回状态进行检查可直接将 retval 赋值为NULL。
如果 pthread_join()//成功调用,它将返回 0 值,如果不成功,pthread_join()返回一个非 0的错误码;
为了防止内存泄露,长时间运行的程序最终应该为每个线程调用 pthread_detach()或者被 pthread_join !!!
4/5).设置操作线程的属性、状态和线程栈
属性对象
(1 )初始化属性对象-pthread_attr_init()函数用于将属性对象使用默认值进行初始化,函数原型如下:
int pthread_attr_init(pthread_attr_t *attr);//函数只有一个参数,是一个指向 pthread_attr_t 的属性对象的指针。成功返回 0,否则返回一个非 0 的错误码。
(2 ) 销毁属性对象-销毁属性对象使用 pthread_attr_destroy()函数,函数原型如下:
int pthread_attr_destroy(pthread_attr_t *attr);//函数只有一个参数,是一个指向 pthread_attr_t 的属性对象的指针。成功返回 0,否则返回一个非 0 的错误码。
线程状态-线程可以有两种状态,分别是:
PTHREAD_CREATE_JOINABLE——非分离线程;
PTHREAD_CREATE_DETACHED——分离线程。
(3 )获取线程状态-获取线程状态的函数是 pthread_attr_getdetachstate(),原型如下:
int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);//参数 attr 是一个指向已初始化的属性对象的指针,detachstate 是所获取状态值的指针。成功返回 0,否则返回一个非 0 的错误码。
(4 )设置线程状态--设置线程状态的函数是 pthread_attr_setdetachstate(),原型如下:
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);//参数 attr 是一个指向已初始化的属性对象的指针, detachstate 是要设置的值。成功返回0,否则返回一个非 0 的错误码。
线程栈
每个线程都有一个独立的调用栈,线程的栈大小在线程创建的时候就已经固定下来,Linux 系统线程的默认栈大小为 8MB,只有主线程的栈大小会在运行过程中自动增长。用户可以通过属性对象来设置和获取栈大小。
(5 )获取线程栈-获取线程栈大小的函数是 pthread_attr_getstacksize(),原型如下:
int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);//参数 attr 是一个指向已初始化的属性对象的指针,stacksize 是保存所获取栈大小的指针。成功返回 0,否则返回一个非 0 的错误码。
(6 ) 设置线程栈--设置线程栈大小的函数是 pthread_attr_setstacksize(),原型如下:
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);//参数 attr 是一个指向已初始化的属性对象的指针,stacksize 是需要设置的栈大小。成功返回 0,否则返回一个非 0 的错误码。
5/5)等待-阻塞等待和非阻塞等待
3.创建互斥量
pthreads 使用 pthread_mutex_t 类型的变量来表示互斥量,同时在使用互斥量进行同步前
需要先对它进行初始化,可以用静态或动态的方式对互斥量进行初始化。
(1 )静态初始化-对于静态分配的 pthread_mutex_t 变量来说,只要将 PTHREAD_MUTEX_INITIALIZER赋给变量就行了。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
(2 )动态初始化-对于动态分配或者不使用默认属性的互斥变量来说,需要调用 pthread_mutex_int()函数来执行初始化工作。pthread_mutex_int()函数原型如下:
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
参数 mutex 是一个指向要初始化的互斥量的指针;参数 attr 传递 NULL 来初始化一个带有默认属性的互斥量,否则就要用类似于线程属性对象所使用的方法,先创建互斥量属性对象,再用该属性对象来创建互斥量。函数成功返回 0,否则返回一个非 0 的错误码;
静态初始化程序通常比调用 pthread_mutex_init 更有效,而且在任何线程开始执行之前,确保变量被初始化一次!!!
(1).创建互斥量-pthreads 使用 pthread_mutex_t 类型的变量来表示互斥量,同时在使用互斥量进行同步前
需要先对它进行初始化,可以用静态或动态的方式对互斥量进行初始化。
1 )静态初始化-对于静态分配的 pthread_mutex_t 变量来说,只要将 PTHREAD_MUTEX_INITIALIZER赋给变量就行了。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
2 )动态初始化-对于动态分配或者不使用默认属性的互斥变量来说,需要调用 pthread_mutex_int()函数来执行初始化工作。pthread_mutex_int()函数原型如下:
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
参数 mutex 是一个指向要初始化的互斥量的指针;参数 attr 传递 NULL 来初始化一个带有默认属性的互斥量,否则就要用类似于线程属性对象所使用的方法,先创建互斥量属性对象,再用该属性对象来创建互斥量。函数成功返回 0,否则返回一个非 0 的错误码
(2).销毁互斥量使用 pthread_mutex_destroy()函数,原型如下:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//----eg.动态创建锁------
int error;
pthread_mutex_t mylock;
if (error = pthread_mutex_destroy(&mylock))
fprintf(stderr, "Failed to destroy mylock : %s\n", strerror(error));
//----eg.销毁锁-----
int error;
pthread_mutex_t mylock;
if (error = pthread_mutex_init(&mylock, NULL))
fprintf(stderr, "Failed to initialize mylock : %s\n", strerror(error));
//--------
(3).加锁与解锁
1)加锁-线程试图锁定互斥量的过程称之为加锁。
pthreads 中有两个试图锁定互斥量的函数,pthread_mutex_lock()和 pthread_mutex_
trylock()。pthread_mutex_lock()函数会一直阻塞到互斥量可用为止,而 pthread_mutex_trylock()
则尝试加锁,但通常会立即返回。函数原型如下:
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
参数 mutex 是需要加锁的互斥量。函数成功返回 0,否则返回一个非 0 的错误码,其中
在另一个线程已持有锁的情况下,调用 pthread_mutex_trylock()函数时错误码为 EBUSY。
2)解锁-解锁是线程将互斥量由锁定状态变为解锁状态。
pthread_mutex_unlock()函数用来释放指定的互斥量。函数原型如下:int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数 mutex 是需要解锁的互斥量。函数成功返回 0,否则返回一个非 0 的错误码。
只有在线程进入临界区之前正确地获取了适当的互斥量,才能在离开临界区时释放互斥
量。以下伪代码展示了互斥量保护临界区的基本用法:
pthread_mutex_t mylock = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mylock);
....临界区代码.....
pthread_mutex_unlock(&mylock);
(4.)死锁和避免
1).什么是死锁 ?
死锁是指两个或两个以上的执行序列在执行过程中,因争夺资源而造成的一种互相等待的现象。例如:一个线程 T1 已锁定了一个资源 R1,又想去锁定资源 R2,而此时另一个线程 T2 已锁定了资源 R2,却想去锁定资源 R1。这两个线程都想得到对方的资源,而又不愿释放自己的资源,结果就是两个线程都在等待而无法执行
2).避免死锁的方式
当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生,如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。例如,规定程序内有三个互斥锁的加锁顺序为 mutexA->mutexB->mutexC,则线程 t1、t2、t3 线程操作伪代码如下所示:(注意,获取锁的方式在锁的队列中时使用的阻塞获取,所以一个时刻只有一个线程拿到一个互斥锁,)
t1 t2 t3
lock(mutexA) lock(mutexA) lock(mutexB)
lock(mutexB) lock(mutexC) lock(mutexC)
lock(mutexC)
顺序排列抑制就可以避免死锁的情况!
目的:为已解决某情况下,互斥锁带来的资源浪费(缩减CPU 处理时间)和效率降低(提高线程的处理效率)
创建与销毁
(1).创建条件变量 -
Pthreads 用 pthread_cond_t 类型的变量来表示条件变量。程序必须在使用 pthread_cond_t变量之前对其进行初始化。
1 ) 静态初始化-
对于静态分配的变量可以简单地将 PTHREAD_COND_INITIALIZER 赋值给变量来初始化默认行为的条件变量。
pthread_cond_t cond = PTHREAD_COND_INITIALIZER
2 )动态初始化
对动态分配或者不使用默认属性的条件变量来说可以使用 pthread _cond_init()来初始化。
函数原型如下:
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数 cond 是一个指向需要初始化 pthread_cond_t 变量的指针,参数 attr 传递 NULL 值时,pthread_cond_init()将 cond 初始化为默认属性的条件变量。函数成功将返回 0;否则返回一个非 0 的错误码。
静态初始化程序通常比调用 pthread_cond_init()更有效,而且在任何线程开始执行之前,确保变量被执行一次。以下代码示例了条件变量的初始化。
pthread_cond_t cond;
int error;
if (error = pthread_cond_init(&cond, NULL));
fprintf(stderr, "Failed to initialize cond : %s\n", strerror(error));
(2).销毁条件变量-函数 pthread_cond_destroy()用来销毁它参数所指出的条件变量,函数原型如下:
int pthread_cond_destroy(pthread_cond_t *cond);
函数成功调用返回 0,否则返回一个非 0 的错误码。以下代码演示了如何销毁一个条件变量。
pthread_cond_t cond;
int error;
if (error = pthread_cond_destroy(&cond))
fprintf(stderr, "Failed to destroy cond : %s\n", strerror(error));
(3).等待-
条件变量是与条件测试一起使用的,通常线程会对一个条件进行测试,如果条件不满足就会调用条件等待函数来等待条件满足。条件等待函数有 pthread_cond_wait()pthread_cond_timedwait()和两个,函数原型如下:
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);
pthread_cond_wait()函数在条件不满足时将一直等待,而 pthread_cond_timedwait()将只等待一段时间。
参数 cond 是一个指向条件变量的指针,参数 mutex 是一个指向互斥量的指针,线程在调用前应该拥有这个互斥量,当线程要加入条件变量的等待队列时,等待操作会使线程释放这个互斥量。pthread_timedwait()的第三个参数 abstime 是一个指向返回时间的指针,如果条件变量通知信号没有在此等待时间之前出现,等待将超时退出,abstime 是个绝对时间,而不是时间间隔。
以上函数成功调用返回 0,否则返回非 0 的错误码,其中 pthread_cond_timedwait()函数如果 abstime 指定的时间到期,错误码为 ETIMEOUT。
以下代码使得线程进入等待,直到收到通知并且满足 a 大于等于 b 的条件。
pthread_mutex_lock(&mutex)
while(a pthread_cond_wait(&cond, &mutex)
pthread_mutex_unlock(&mutex)
(4).通知
当另一个线程修改了某参数可能使得条件变量所关联的条件变成真时,它应该通知一个或者多个等待在条件变量等待队列中的线程。
条件通知函数有 pthread_cond_signal()和 pthread_cond_broadcast()函数,其中 pthread_cond_signal 函数可以唤醒一个在条件变量等待队列等待的线程,而 pthread_cond_broadcast函数可以所有在条件变量等待队列等待的线程。函数原型如下:
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
参数 cond 是一个指向条件变量的指针。函数成功返回 0,否则返回一个非 0 的错误码。