相关概念
多线程就是一个进程的多个执行路径,可以让一个进程在同一个空间同时执行多个任务
一个进程的所有的线程都是在同一进程空间下运行,可以共享该进程的系统资源(地址空间、文件描述符、信号处理等),相对于多进程避免了复杂的进程间通信,避免了在用户空间和内核空间的频繁切换
采用多线程可以改进程序的各种IO产生堵塞的情况的表现性能,无论是多核环境还是单核环境,都是有改进作用的
每个进程创建后,会首先产生一个缺省的线程,就是主程序(控制进程),主线程从main函数进入线程
最常见的线程模型中,除主线程较为特殊之外,其他线程一旦被创建,相互之间就是对等关系,不存在隐含的层次关系。每个进程可创建的最大线程数由具体实现决定。
注意对于多线程编程在编译时,一定要加上-lpthread 选项告诉链接器在链接的时候要连 接pthread库
无论子线程执行完毕与否,一旦主线程执行完毕退出,所有子线程执行都会终止。这时整个进程结束或僵死,部分线程保持一种终止执行但还未销毁的状态,而进程必须在其所有线程销毁后销毁,这时进程处于僵死状态。线程函数执行完毕退出,或以其他非常方式终止,线程进入终止态,但是为线程分配的系统资 源不一定释放,可能在系统重启之前,一直都不能释放,终止态的线程,仍旧作为一个线程实体存在于操作系统中,什么时候销毁,取决于线程属性。在这种情况下,主线程和子线程通常定义以下两种关系:
- 可会合(joinable):这种关系下,主线程需要明确执行等待操作,在子线程结束后,主线程的等待操作执行完毕,子线程和主线程会合,这时主线程继续执行等待操作之后的下一步操作。主线程必须会合可会合的子线程。在主线程的线程函数内部调用子线程对象的wait函数实现,即使子线程能够在主线程之前执行完毕,进入终止态,也必须执行会合操作,否则,系统永远不会主动销毁线程,分配给该线程的系统资源也永远不会释放。
- 相分离(detached):表示子线程无需和主线程会合,也就是相分离的,这种情况下,子线程一旦进入终止状态,这种方式常用在线程数较多的情况下,有时让主线程逐个等待子线程结束,或者让主线程安排每个子线程结束的等待顺序,是很困难或不可能的,所以在并发子线程较多的情况下,这种方式也会经常使用。线程的分离状态决定一个线程以什么样的方式来终止自己,在默认的情况下,线程是非分离状态的,这种情况下,原有的线程等待创建的线程结束,只有当pthread_join函数返回时,创建的线程才算终止,释放自己占用的系统资源,而分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。
线程的分离状态决定一个线程以什么样的方式来终止自己,在默认的情况下,线程是非分离状态的,这种情况下,原有的线程等待创建的线程结束,只有当pthread_join函数返回时,创建的线程才算终止,释放自己占用的系统资源,而分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。
线程标识
就和每个进程都有进程ID一样,线程也有自己的ID,但是线程ID只有在上下文环境中才有意义,不是完全唯一的
进程ID的数据类型是 pid_t ,而线程ID的类型是 pthread_t 这是一种结构,因而不能把它作为一种整型来处理,需要进行比较时,需要使用函数pthread_equal来比较
linux采用无符号长整型表示pthread_t
获取线程ID pthread_self
原型
pthread_t pthread_self(void);
线程ID比较 pthread_equal
原型
pthread_t pthread_equal(pthread_t tid1, pthread_t tid2);
创建线程 pthread_create
由主线程调用pthread_create()创建的线程称为子线程,子线程也可以有自己的入口函数,该函数由用户 在创建的时候指定。
原型
int pthread_create(pthread_t *tid
, const pthrea_arr_t *attr
, void *(*start_rtn)(void *)
, void *arg );
- 第一个参数tid是一个pthread_t类型的指针,他用来返回该线程的线程ID
- 第三个参数start_rtn是一个函数指针,它指向的函数原型是 void *func(void *),这是所创建的子线程的入口函数
- 第四个参数arg是传给所调用的函数的参数,如果有多个参数需要传递给子线程则需要封装到一个结构体里传进去
- 第二个参数是线程的属性,其类型是pthread_attr_t类型,其定义如下:
typedef struct
{
int detachstate;
int schedpolicy;
struct sched_param schedparam;
int inheritsched;
int scope;
size_t guardsize;
int stackaddr_set;
void * stackaddr;
size_t stacksize;
}pthread_attr_t;
我们经常使用的有,设置分离状态,栈的大小
线程的分离状态,设置线程属性
创建线程默认是可会合的状态,这种状态下,主线程必须调用pthead_join 等待子线程退出,否则可能造成内存泄漏 我们常将线程设置为可分离的状态,有两种方法
- 在线程内调用 pthread_detach(pthread_self()) 这个方法最简单
- 创建线程时,设置线程属性
第2种方法
- 定义属性变量thread_attr
- 在对该属性进行设置前,我们需要先调用pthread_attr_init 函数初始化它
- 设置线程的栈大小为120K,
- 设置线程的属性为分离状态。
- 创建线程时使用该属性创建线程,
这时创建的子进程就是分离状态了。线程属性在使用完之后,我们应该调用pthread_attr_destroy 把他摧毁释放。
实例
pthread_attr_t thread_attr;
pthread_t tid;
if( pthread_attr_init(&thread_attr) )
{
printf("pthread_attr_init() failure: %s\n", strerror(errno));
return -1;
}
if( pthread_attr_setstacksize(&thread_attr, 120*1024) )
{
printf("pthread_attr_setstacksize() failure: %s\n", strerror(errno));
return -1;
}
if( pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED) )
{
printf("pthread_attr_setdetachstate() failure: %s\n", strerror(errno));
return -1;
}
pthread_create(&tid, &thread_attr, thread_worker1, &arg);
printf("Thread worker1 tid[%ld] created ok\n", tid);
pthread_attr_destroy(&thread_attr);
终止线程 pthread_exit 和 pthead_join
在任意线程中调用exit、_exit 都会导致整个进程的终止
单个线程退出有三种方法
- 从线程的函数中返回,返回值就是退出码
- 被同一进程的其他线程取消
- 调用pthread_eixt
原型
void pthread_exit(void *rval_ptr);
rval_ptr是一个无类型指针,其他线程可以调用pthead_join来查看其退出时返回的信息 原型
int pthread_join(pthread_t tid, void **rval_ptr);
当进程调用pthread_join,会进入阻塞状态,直到指定的线程退出。 如果指定的线程是被取消的 ,rval_ptr指向的空间被设置为 PTHREAD_CANCELED
如果对返回值不感兴趣 ,可以将rval_ptr设置为NULL
需要注意的是,rval_ptr是传地址,所以这个结构使用的内存,最好在本线程退出后,依然有效,否则线程退出后内存被回收重新利用,可能下次使用这个值时,这个内存内已经不是我们需要的值 可以使用全局变量或者使用malloc分配
只能对可会合的进程使用pthread_join,且一个线程只能被一个线程等待,不能被多个线程等待
一个线程要么设置为可分离的状态,要么需要调用pthread_join等待其退出,否则可能造成内存泄漏
请求取消进程 pthread_cancel
原型
int pthead_cancel(pthread_t tid);
在默认情况下,该函数会使线程tid,如同调用了pthread_exit 一样退出
但是线程可以选择取消或者控制如何退出 pthead_cancel只提出请求,不会等待线程退出
线程清理函数pthread_cleanup_push 和 pthread_cleanup_pop
类似于进程的atexit,线程也可注册线程清理处理程序,一个线程可注册多个,它们执行的顺序和注册时相反 原型
void pthread_cleanup_push(void (*trn)(void *), void *arg);
void pthread_cleanup_pop(int execute);
pthread_cleanup_push用于注册一个线程清理处理程序,arg是给线程清理函数传递参数的
关于pthread_cleanup_pop,书上的说法一点模糊,我这里写了程序来验证 APUE上的描述:“参数execute为0时,清理函数不会被调用,不管发送上述任何情况,pthread_cleanup_pop都将取消上次pthread_cleanup_push调用建立的清理处理程序。”
但是也一点需要注意每个pthread_cleanup_push必要也一个pthread_cleanup_pop搭配出现,就算你不需要pthread_cleanup_pop,也必须放它在 pthread_exit(0);的后面,否则编译无法通过
这是因为它们可以被实现为宏。所以必须在与线程相同的作用域内以匹配的形式使用push函数和pop函数。pthread_cleanup_push的宏定义可以包含字符{,而pthread_cleanup_pop的宏定义必须有相对应的匹配字符}。
接下来我们看代码
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void *pthr_fun1(void *arg);
void *pthr_fun3(void *arg);
void *pthr_fun2(void *arg);
void cleanup(void *arg);
int main(int argc, char **argv)
{
int rv = -1;
pthread_t tid1,tid2,tid3;
rv = pthread_create(&tid1,NULL,pthr_fun1,(void *)1);
if(rv != 0)
{
printf("create 1 error\n");
return -1;
}
printf("pthread 1 succed\n");
rv = pthread_create(&tid2,NULL,pthr_fun2,(void *)1);
if(rv != 0)
{
printf("create 2 error\n");
return -2;
}
printf("pthread 1 succed\n");
rv = pthread_join(tid1,NULL);
if(rv != 0)
{
printf("join error 1 \n");
}
rv = pthread_create(&tid3,NULL,pthr_fun3,(void *)1);
if(rv != 0)
{
printf("create 3 error\n");
return -3;
}
printf("pthread 3 succed\n");
printf(" pthread 1 exit\n");
rv = pthread_join(tid2,NULL);
if(rv != 0)
{
printf("join error 2 \n");
}
rv = pthread_join(tid3,NULL);
if(rv != 0)
{
printf("join error 3 \n");
}
printf(" pthread 3 exit\n");
return 0;
}
void cleanup(void *arg)
{
printf("cleanup :%s \n",(char *)arg);
}
void *pthr_fun1(void *arg)
{
printf(" pthread 1 start \n");
pthread_cleanup_push(cleanup, "thread 1 : 11");
pthread_cleanup_push(cleanup, "thread 1 : 22");
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
printf("pthread exit 1 \n");
pthread_exit(0);
}
void *pthr_fun2(void *arg)
{
printf(" pthread 2 start \n");
pthread_cleanup_push(cleanup, "thread 2 : 11");
pthread_cleanup_push(cleanup, "thread 2 : 22");
pthread_cleanup_pop(1);
pthread_cleanup_pop(1);
printf("pthread exit 2 \n");
pthread_exit(0);
}
void *pthr_fun3(void *arg)
{
printf(" pthread 3 start \n");
pthread_cleanup_push(cleanup, "thread 3 : 11");
pthread_cleanup_push(cleanup, "thread 3 : 22");
printf("pthread exit 3 \n");
pthread_exit((void *)1);
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
}
运行结果
结合书的描述,可知 pthread_cleanup_pop的参数为0时,将取消上一次注册的线程清理函数 如果参数为非0,将马上调用线程清理函数,同时也取消该线程清理函数
还有一点,线程只有pthread_exit 和 其他线程取消才会调用清理函数,线程自己在启动函数中退出是不会调用的
线程同步 互斥锁
在谈文件io时,我们谈过原子操作,这个问题在线程中同样存在,如果有多个线程访问同一个资源,可能会有不一致问题出现,为了解决这个问题,我们会给这种资源加上“锁”
互斥量
互斥量本质上来说是一把锁,在访问共享资源前给互斥量设置(加锁),访问完成后释放(解锁)。一个线程对一个资源加锁后,其他线程再通过同样的方法访问,就不能访问到(可能会阻塞) 互斥量可以用数据类型:pthread_mutex_t 来表示
在使用互斥量前,需要先使用函数:pthread_mutex_init 对其初始化 如果动态分配互斥量,在释放内存之前需要先调用函数: pthread_mutex_destroy
原型
int pthread_mutex_init( pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
init函数中,attr设为NULL,即可使用默认属性初始化,以后再讨论这个属性的设置
接下来介绍加锁和解锁的函数
int pthread_mutex_lock( pthread_mutex_t *mutex);
int pthread_mutex_trylock( pthread_mutex_t *mutex);
int pthread_mutex_unlock( pthread_mutex_t *mutex);
很明显pthread_mutex_lock是加锁,pthread_mutex_unlock是开锁 而pthread_mutex_trylock是尝试加锁,如果调用时未被上锁,就成功上锁,否则也不阻塞直接失败返回 pthread_mutex_lock如果未能上锁,将会阻塞等待
避免死锁
试想一下,我们寝室/实验室只有一个洗手间,那多个人是怎么解决马桶共享的问题?对,那就是锁的机制!在这里马桶就是临界资源,我们在进入到洗手间(临界区)后,就首先上锁; 然后用完离开洗手间(临界区)之后,把锁释放供别人使用。如果有人想去洗手间时发现门锁上了,他也有两种策略:1,在洗手间那里等(阻塞); 2,暂时先离开等会再过来看(非阻塞)
- 当一个线程试图对一个互斥量加锁两次,那么它自身就会陷入死锁的状态,
- a线程拥有A锁,b线程拥有B锁,这时b去申请A锁,a去申请B锁,那么两个线程将进入死锁状态
死锁产生的4个必要条件
- 互斥:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。
- 占有且等待:一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。
- 不可抢占:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
- 循环等待:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。
当以上四个条件均满足,必然会造成死锁,发生死锁的进程无法进行下去,它们所持有的资源也无法释放。这样会导致CPU的吞吐量下降。所以死锁情况是会浪费系统资源和影响计算机的使用性能的。那么,解决死锁问题就是相当有必要的了。
产生死锁需要四个条件,那么,只要这四个条件中至少有一个条件得不到满足,就不可能发生死锁了。由于互斥条件是非共享资源所必须的,不仅不能改变,还应加以保证,所以,主要是破坏产生死锁的其他三个条件。
- a、破坏“占有且等待”条件
方法1:所有的进程在开始运行之前,必须一次性地申请其在整个运行过程中所需要的全部资源。 优点:简单易实施且安全。 缺点:因为某项资源不满足,进程无法启动,而其他已经满足了的资源也不会得到利用,严重降低了资源的利用率,造成资源浪费。使进程经常发生饥饿现象。 方法2:该方法是对第一种方法的改进,允许进程只获得运行初期需要的资源,便开始运行,在运行过程中逐步释放掉分配到的已经使用完毕的资源,然后再去请求新的资源。这样的话,资源的利用率会得到提高,也会减少进程的饥饿问题。 - b、破坏“不可抢占”条件
当一个已经持有了一些资源的进程在提出新的资源请求没有得到满足时,它必须释放已经保持的所有资源,待以后需要使用的时候再重新申请。这就意味着进程已占有的资源会被短暂地释放或者说是被抢占了。该种方法实现起来比较复杂,且代价也比较大。释放已经保持的资源很有可能会导致进程之前的工作实效等,反复的申请和释放资源会导致进程的执行被无限的推迟,这不仅会延长进程的周转周期,还会影响系统的吞吐量。 - c、破坏“循环等待”条件
可以通过定义资源类型的线性顺序来预防,可将每个资源编号,当一个进程占有编号为i的资源时,那么它下一次申请资源只能申请编号大于i的资源。
|