(一)锁的基本知识
在Linux&Apue(0.3.0)中我们用一个thread编程描述了多个子线程之间共享一个共享资源导致的问题。那么,我们怎么解决这个问题呢?
(1) 临界的定义
临界资源:一个资源会被不同的线程访问修改
临界区:临界资源访问修改相关的代码
(2) 互斥锁(pthread_mutex_t)
互斥锁:引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。(互斥锁并不占用任何资源)
2.1 互斥锁的创建
互斥锁创建有两种方法:静态方式&动态方式。
2.1.1 静态方式
静态方式:POSIX定义了一个宏PTHREAD_MUTEX_INITIALIZER来静态初始化互斥锁。
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
2.1.2 动态方式
动态方式:采用pthread_mutex_init()函数来初始化互斥锁。
#include<pthread.h> //头文件包含
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
mutexattr参数:用于指定互斥锁属性,如果为NULL则使用缺省值。(缺省值:默认选项(c),又称缺省值,是一种计算机术语,指在无决策者干预情况下,对于决策或应用软件、计算机程序的系统参数的自动选择。)
2.2 互斥锁的属性(四个值)
2.2.1 普通锁
普通锁(缺省值):当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
PTHREAD_MUTEX_TIMED_NP //参数值
2.2.2 嵌套锁
嵌套锁:允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
PTHREAD_MUTEX_RECURSIVE_NP //参数值
2.2.3 检错锁
检错锁:如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
PTHREAD_MUTEX_ERRORCHECK_NP //参数值
2.2.4 适应锁
适应锁:仅等待解锁后重新竞争。
PTHREAD_MUTEX_ADAPTIVE_NP //参数值
2.3 互斥锁的销毁
pthread_mutex_destroy ()函数:指向要销毁的互斥锁的指针,成功返回0。(其中含有:检查锁状态)
int pthread_mutex_destroy(pthread_mutex_t *mutex)
2.4 互斥锁的操作
锁操作主要包括:加锁,解锁,测试加锁。
2.4.1 加锁(阻塞)
int pthread_mutex_lock(pthread_mutex_t *mutex)
2.4.2 解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex)
互斥锁属性 | 解锁者 |
---|---|
普通锁、适应锁 | 进程内任何线程 |
检错锁 | 必须是加锁者解锁才有效,否则返回EPERM |
嵌套锁 | 文档和实现要求必须由加锁者解锁 |
2.4.3 测试加锁(非阻塞)
测试加锁:与加锁操作类似。不同的是在锁已经被占据时返回EBUSY而不是挂起等待。
int pthread_mutex_trylock(pthread_mutex_t *mutex)
(3) 死锁
死锁:如果多个线程要调用多个对象,则在上锁的时候可能会出现“死锁”。
简单来说:线程A持有锁1,线程B持有锁2。但是他们都在等着对方解锁。
3.1 死锁产生必要条件(四个)
必要条件 | 解析 |
---|---|
互斥 | 某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束 |
占有且等待 | 一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源 |
不可抢占 | 别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。 |
循环等待 | 存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。 |
3.2 死锁的应对方法(来源:百度百科)
这里只是粗略的提出,为了对死锁的应对有个基本的了解。具体下来的应对,这里就不做详细说明了。
3.2.1 死锁预防
死锁预防:方法是通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或者几个,来预防发生死锁。预防死锁是一种较易实现的方法,已被广泛使用。
优点:简单,直观
缺点:由于所施加的限制条件往往太严格,可能会导致系统资源利用率和系统吞吐量降低。
3.2.2 死锁避免
死锁避免:系统对进程发出的每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源;如果分配后系统可能发生死锁,则不予分配,否则予以分配。这是一种保证系统不进入死锁状态的动态策略。
3.2.3 死锁检测和解除
死锁检测和解除:
①先检测:这种方法并不须事先采取任何限制性措施,也不必检查系统是否已经进入不安全区,此方法允许系统在运行过程中发生死锁。但可通过系统所设置的检测机构,及时地检测出死锁的发生,并精确地确定与死锁有关的进程和资源。检测方法包括定时检测、效率低时检测、进程等待时检测等。
②解除死锁:采取适当措施,从系统中将已发生的死锁清除掉。
优点:有可能使系统获得较好的资源利用率和吞吐量
缺点:实现上难度也最大。
(二)用锁解决多线程访问修改共享资源问题
在Linux&Apue(0.3.0)中,我们用一个简单例子进行了多线程之间编程,但是也出现了多线程之间共享了一个共享资源的问题。所以,我们现在来对之前的编程进行修改,用锁来解决问题。
(1)设置结构体,动态锁初始化
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
void *thread_worker1(void *args);
void *thread_worker2(void *args);
//设置结构体,将传给子线程的参数放入
typedef struct worker_ctx_s
{
int shared_var;
pthread_mutex_t lock;
} worker_ctx_t;
int main(int argc, char **argv)
{
worker_ctx_t worker_ctx;
pthread_t tid;
pthread_attr_t thread_attr;
//参数值初始化
worker_ctx.shared_var=1000;
//动态锁初始化
pthread_mutex_init(&worker_ctx.lock,NULL);
(2) 设置线程属性
//一、设置线程属性
//线程属性初始化
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 -2;
}
//设置线程关系
if( pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED) )
{
printf("pthread_attr_setdetachstate() failure: %s\n", strerror(errno));
return -3;
}
(3) 创建线程&主线程工作&销毁动态锁初始化
//二、创建线程 与之前不同,这里全设为了相分离
//线程1
pthread_create(&tid, &thread_attr, thread_worker1, &worker_ctx);
printf("Thread worker1 tid[%ld] created ok\n", tid);
//线程2
pthread_create(&tid, &thread_attr, thread_worker2, &worker_ctx);
printf("Thread worker2 tid[%ld] created ok\n", tid);
//主线程工作
while(1)
{
printf("Main/Control thread shared_var: %d\n", worker_ctx.shared_var);
sleep(10);
}
//销毁动态锁初始化
pthread_mutex_destroy(&worker_ctx.lock);
}
(4) 子线程工作&加锁,解锁操作
void *thread_worker1(void *args)
{
worker_ctx_t *ctx=(worker_ctx_t*)args;
if( !args )
{
printf("%s() get invalid arguments\n", __FUNCTION__);
pthread_exit(NULL); //若用exit会退出进程
}
printf("Thread workder 1 [%ld] start running...\n", pthread_self());
//子线程工作
while(1)
{
//加锁(阻塞)
pthread_mutex_lock(&ctx->lock);
printf("+++: %s before shared_var++: %d\n", __FUNCTION__, ctx->shared_var);
ctx->shared_var ++;
sleep(2);
printf("+++: %s after sleep shared_var: %d\n", __FUNCTION__, ctx->shared_var);
//解锁
pthread_mutex_unlock(&ctx->lock);
}
printf("Thread workder 1 exit...\n");
return NULL;
}
void *thread_worker2(void *args)
{
worker_ctx_t *ctx=(worker_ctx_t*)args;
if( !args )
{
printf("%s() get invalid arguments\n", __FUNCTION__);
pthread_exit(NULL);
}
//子线程工作
printf("Thread workder 2 [%ld] start running...\n", pthread_self());
while(1)
{
//测试加锁(非阻塞)
if(0!=pthread_mutex_trylock(&ctx->lock))
{
continue;
}
printf("---: %s before shared_var++: %d\n", __FUNCTION__, ctx->shared_var);
ctx->shared_var++;
sleep(2);
printf("---: %s after sleep shared_var: %d\n", __FUNCTION__, ctx->shared_var);
//解锁
pthread_mutex_unlock(&ctx->lock);
sleep(1);
}
printf("Thread workder 2 exit...\n");
return NULL;
}
(5)运行结果
简析:
worker1:befor 1000,after 1001。
worker2:befor 1001,after 1002。
这说明在worker1获取共享数据的时候,worker2没法获取。