[置顶] Linux C编程--线程操作2--线程同步详解

时间:2022-10-31 15:13:54

linux线程同步之互斥 

在windows中,为了让多个线程达到同步的目的,在对于全局变量等大家都要用的资源的使用上,通常得保证同时只能由一个线程在用,一个线程没有宣布对它的释放之前,不能够给其他线程使用这个变量。在windows里,我们可以用时EnterCriticalSection()和 LeaveCriticalSection()函数.那么在linux里,有什么类似的机制呢?

 

这里介绍互斥锁。

1.申请一个互斥锁

pthread_mutex_t mutex; //申请一个互斥锁

你可以声明多个互斥量。

在声明该变量后,你需要调用pthread_mutex_init()来创建该变量。pthread_mutex_init的格式如下:

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);

第一个参数,mutext,也就是你之前声明的那个互斥量,第二个参数为该互斥量的属性。属性定义如下:

互斥量分为下面三种:

l         快速型(PTHREAD_MUTEX_FAST_NP)。这种类型也是默认的类型。该线程的行为正如上面所说的。

l         递归型(PTHREAD_MUTEX_RECURSIVE_NP)。如果遇到我们上面所提到的死锁情况,同一线程循环给互斥量上锁,那么系统将会知道该上锁行为来自同一线程,那么就会同意线程给该互斥量上锁。

l         错误检测型(PTHREAD_MUTEX_ERRORCHECK_NP)。如果该互斥量已经被上锁,那么后续的上锁将会失败而不会阻塞,pthread_mutex_lock()操作将会返回EDEADLK。

 

可以通过函数

注意以下语句可以做到将一个互斥锁快速初始化为快速型。

pthread_mutex_t  mutex = PTHREAD_MUTEX_INITIALIZER;

 

2.销毁一个互斥锁

pthread_mutex_destroy()用于注销一个互斥锁,API定义如下:

 int pthread_mutex_destroy(pthread_mutex_t *mutex)

销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态。由于在Linux中,互斥锁并不占用任何资源,因此LinuxThreads中的pthread_mutex_destroy()除了检查锁状态以外(锁定状态则返回EBUSY)没有其他动作。

 

3.上锁(相当于windows下的EnterCriticalSection)

在创建该互斥量之后,你便可以使用它了。要得到互斥量,你需要调用下面的函数:

int pthread_mutex_lock(pthread_mutex_t *mutex);

该函数用来给互斥量上锁。互斥量一旦被上锁后,其他线程如果想给该互斥量上锁,那么就会阻塞在这个操作上。如果在此之前该互斥量已经被其他线程上锁,那么该操作将会一直阻塞在这个地方,直到获得该锁为止。

在得到互斥量后,你就可以进入关键代码区了。

 

4.解锁(相当于windows下的LeaveCriticalSection)

在操作完成后,你必须调用下面的函数来给互斥量解锁,也就是前面所说的释放。这样其他等待该锁的线程才有机会获得该锁,否则其他线程将会永远阻塞。

int pthread_mutex_unlock(pthread_mutex_t *mutex);

 

5..pthread_mutex_trylock

如果我们不想一直阻塞在这个地方,那么可以调用下面函数:
int pthread_mutex_trylock(pthread_mutex_t *mutex)
如果此时互斥量没有被上锁,那么pthread_mutex_trylock()将会返回0,并会对该互斥量上锁。如果互斥量已经被上锁,那么会立刻返回EBUSY。

注:

[置顶]        Linux C编程--线程操作2--线程同步详解

[置顶]        Linux C编程--线程操作2--线程同步详解

下面介绍一个实例说明上述函数的用法

这是一个简单的读写程序,在这个程序中,一个线程从共享的缓冲区中读数据,另一个线程向共享的缓冲区中写数据。对共享的缓冲区的访问控制是通过使用一个互斥锁来是实现的。

#include <stddef.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

#define FALSE 0
#define TRUE 1

void readfun();
void writefun();

char buffer[256];
int buffer_has_item=0;
int retflag=FALSE,i=0;
pthread_mutex_t mutex;

int main()
{
void *retval;
pthread_t reader;
pthread_mutex_init(&mutex,NULL);
pthread_create(&reader,NULL,(void *)&readfun,NULL);
writefun();
pthread_join(reader,&retval);

}

void readfun()
{
while(1)
{
if(retflag)
return;
pthread_mutex_lock(&mutex);
if(buffer_has_item==1)
{
printf("%s",buffer);
buffer_has_item=0;
}
pthread_mutex_unlock(&mutex);
}
}
void writefun()
{
int i=0;
while(1)
{
if(i==10)
{
retflag=TRUE;
return;
}
pthread_mutex_lock(&mutex);
if(buffer_has_item==0)
{
sprintf(buffer,"This is %d\n",i++);
buffer_has_item=1;
}
pthread_mutex_unlock(&mutex);
}
}


线程同步之条件变量

与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。

条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。

条件的检测是在互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。

使用条件变量之前要先进行初始化。可以在单个语句中生成和初始化一个条件变量如:pthread_cond_t my_condition=PTHREAD_COND_INITIALIZER;(用于进程间线程的通信)。可以利用函数pthread_cond_init动态初始化。

条件变量分为两部分: 条件和变量. 条件本身是由互斥量保护的. 线程在改变条件状态前先要锁住互斥量. 它利用线程间共享的全局变量进行同步的一种机制。

相关的函数如下:

1 int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);     
2 int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
3 int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
4 int pthread_cond_destroy(pthread_cond_t *cond);  
5 int pthread_cond_signal(pthread_cond_t *cond);
6 int pthread_cond_broadcast(pthread_cond_t *cond);  //解除所有线程的阻塞

简要说明:     

     (1)初始化.init()或者pthread_cond_t cond=PTHREAD_COND_INITIALIER;属性置为NULL      (2)等待条件成立.pthread_wait,pthread_timewait.wait()释放锁,并阻塞等待条件变量为真      timewait()设置等待时间,仍未signal,返回ETIMEOUT(加锁保证只有一个线程wait)      (3)激活条件变量:pthread_cond_signal,pthread_cond_broadcast(激活所有等待线程)      (4)清除条件变量:destroy;无线程等待,否则返回EBUSY 

详细说明

1. 初始化:

    条件变量采用的数据类型是pthread_cond_t, 在使用之前必须要进行初始化, 这包括两种方式:

  • 静态: 可以把常量PTHREAD_COND_INITIALIZER给静态分配的条件变量.
  • 动态: pthread_cond_init函数, 是释放动态条件变量的内存空间之前, 要用pthread_cond_destroy对其进行清理.

[置顶]        Linux C编程--线程操作2--线程同步详解#include <pthread.h>
[置顶]        Linux C编程--线程操作2--线程同步详解
[置顶]        Linux C编程--线程操作2--线程同步详解int pthread_cond_init(pthread_cond_t *restrict cond, pthread_condattr_t *restrict attr);
[置顶]        Linux C编程--线程操作2--线程同步详解int pthread_cond_destroy(pthread_cond_t *cond);

[置顶]        Linux C编程--线程操作2--线程同步详解
[置顶]        Linux C编程--线程操作2--线程同步详解成功则返回0, 出错则返回错误编号.

    当pthread_cond_init的attr参数为NULL时, 会创建一个默认属性的条件变量; 非默认情况以后讨论.

2. 等待条件:

[置顶]        Linux C编程--线程操作2--线程同步详解#include <pthread.h>
[置顶]        Linux C编程--线程操作2--线程同步详解
[置顶]        Linux C编程--线程操作2--线程同步详解int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restric mutex);
[置顶]        Linux C编程--线程操作2--线程同步详解int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict timeout);

[置顶]        Linux C编程--线程操作2--线程同步详解
[置顶]        Linux C编程--线程操作2--线程同步详解成功则返回0, 出错则返回错误编号.

    这两个函数分别是阻塞等待和超时等待.

    等待条件函数等待条件变为真, 传递给pthread_cond_wait的互斥量对条件进行保护, 调用者把锁住的互斥量传递给函数. 函数把调用线程放到等待条件的线程列表上, 然后对互斥量解锁, 这两个操作是原子的. 这样便关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道, 这样线程就不会错过条件的任何变化.

    当pthread_cond_wait返回时, 互斥量再次被锁住.

3. 通知条件:

[置顶]        Linux C编程--线程操作2--线程同步详解#include <pthread.h>
[置顶]        Linux C编程--线程操作2--线程同步详解
[置顶]        Linux C编程--线程操作2--线程同步详解int pthread_cond_signal(pthread_cond_t *cond);
[置顶]        Linux C编程--线程操作2--线程同步详解int pthread_cond_broadcast(pthread_cond_t *cond);

[置顶]        Linux C编程--线程操作2--线程同步详解
[置顶]        Linux C编程--线程操作2--线程同步详解成功则返回0, 出错则返回错误编号.

    这两个函数用于通知线程条件已经满足. 调用这两个函数, 也称向线程或条件发送信号. 必须注意, 一定要在改变条件状态以后再给线程发送信号.


下面给出一个典例,这个例子是一个典型的生产者/消费者的例子。

#include <stdio.h>
#include <pthread.h>
#define BUFFER_SIZE 4
#define OVER (-1)
struct producers //定义生产者条件变量的结构
{
int buffer[BUFFER_SIZE]; //定义缓冲区
pthread_mutex_tlock; //定义访问缓冲区的互斥锁
intreadpos, writepos; //读/写的位置
pthread_cond_tnotempty; //缓冲区中有数据时的标记
pthread_cond_tnotfull; //缓冲区未满的标记
};

//初始化缓冲区void init(struct producers *b){pthread_mutex_init(&b->lock,NULL);pthread_cond_init(&b->notempty,NULL);pthread_cond_init(&b->notfull,NULL);b->readpos=0;b->writepos=0;}
//在缓冲区中存放一个整数void put(struct producers *b, int data){pthread_mutex_lock(&b->lock);
//当缓冲区为满时等待while((b->writepos+1)%BUFFER_SIZE==b->readpos){pthread_cond_wait(&b->notfull,&b->lock);
//在返回之前,pthread_cond_wait需要参数b->lock}
//向缓冲区中写数据,并将写指针向前移动b->buffer[b->writepos]=data;b->writepos++;if(b->writepos>=BUFFER_SIZE) b->writepos=0;
//发送当前缓冲区中有数据的信号pthread_cond_signal(&b->notempty);pthread_mutex_unlock(&b->lock);}
//从缓冲区中读数据并将数据从缓冲区中移走int get(struct producers *b){int data;pthread_mutex_lock(&b->lock);
//当缓冲区中无数据时等待while(b->writepos==b->readpos){pthread_cond_wait(&b->notempty,&b->lock);}
//从缓冲区中读数据,并将指针前移data=b->buffer[b->readpos];b->readpos++;if(b->readpos>=BUFFER_SIZE) b->readpos=0;
//发送当前缓冲区未满的信号pthread_cond_signal(&b->notfull);pthread_mutex_unlock(&b->lock);return data;}struct producers  buffer;void *producer(void *data){int n;for(n=0;n<10;n++){printf("Producer : %d-->\n",n);put(&buffer,n);}put(&buffer,OVER);return NULL;}void *consumer(void *data){int d;while(1){d=get(&buffer);if(d==OVER) break;printf("Consumer: --> %d\n",d);}return NULL;}int main(){pthread_t tha,thb;void *retval;init(&buffer);pthread_create(&tha,NULL,producer,0);pthread_create(&thb,NULL,consumer,0);pthread_join(tha,&retval);pthread_join(thb,&retval);return 0;}

程序说明:

主进程创建两个线程,一个称为producer,另一个称为consumer。producer向缓冲区中写整数,当缓冲区中已经写入数据后,就发送缓冲区中有数据的信号。consumer从缓冲区中读数据,当consumer从缓冲区中读数据后,就发送当前缓冲区未满的信号。


线程同步之信号量

sem_wait函数也是一个原子操作,它的作用是从信号量的值减去一个“1”,但它永远会先等待该信号量为一个非零值才开始做减法。也就是说,如果你对一个值为2的信号量调用sem_wait(),线程将会继续执行,这信号量的值将减到1。如果对一个值为0的信号量调用sem_wait(),这个函数就 会地等待直到有其它线程增加了这个值使它不再是0为止。如果有两个线程都在sem_wait()中等待同一个信号量变成非零值,那么当它被第三个线程增加 一个“1”时,等待线程中只有一个能够对信号量做减法并继续执行,另一个还将处于等待状态。

sem_post函数的作用是给信号量的值加上一个“1”,它是一个“原子操作”---即同时对同一个信号量做加“1”操作的两个线程是不会冲突的;而同 时对同一个文件进行读、加和写操作的两个程序就有可能会引起冲突。信号量的值永远会正确地加一个“2”--因为有两个线程试图改变它。


下面给出实现生产者/消费者的信号量的例子。
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#define BUFFER_SIZE 4
#define OVER (-1)
struct producers
{
int buffer[BUFFER_SIZE];
intreadpos, writepos;
sem_tsem_read;
sem_tsem_write;
};

void init(struct producers *b)
{
sem_init(&b->sem_write,0,BUFFER_SIZE-1);
sem_init(&b->sem_read,0,0);
b->readpos=0;
b->writepos=0;
}

void put(struct producers *b, int data)
{
sem_wait(&b->sem_write);
b->buffer[b->writepos]=data;
b->writepos++;
if(b->writepos>=BUFFER_SIZE) b->writepos=0;
sem_post(&b->sem_read);
}

int get(struct producers *b)
{
int data;
sem_wait(&b->sem_read);
data=b->buffer[b->readpos];
b->readpos++;
if(b->readpos>=BUFFER_SIZE) b->readpos=0;
sem_post(&b->sem_write);
return data;
}

struct producers buffer;
void *producer(void *data)
{
int n;
for(n=0;n<10;n++)
{
printf("Producer : %d-->\n",n);
put(&buffer,n);
}
put(&buffer,OVER);
return NULL;
}

void *consumer(void *data)
{
int d;
while(1)
{
d=get(&buffer);
if(d==OVER) break;
printf("Consumer: --> %d\n",d);
}
return NULL;
}

int main()
{
pthread_t tha,thb;
void *retval;
init(&buffer);
pthread_create(&tha,NULL,producer,0);
pthread_create(&thb,NULL,consumer,0);
pthread_join(tha,&retval);
pthread_join(thb,&retval);
return 0;

}