线程间通信(互斥量、信号量、条件变量)

41 篇文章 0 订阅
本文深入探讨了线程同步的概念,重点介绍了互斥量(mutex)、读写锁和条件变量等机制。互斥量用于保护共享资源,确保同一时间只有一个线程访问;读写锁允许多个读线程并发访问,而写线程独占资源;条件变量则提供了等待特定条件满足的机制。文中还提到了死锁的预防以及信号量作为另一种同步手段。通过对这些概念和函数的详细解析,读者能更好地理解和应用多线程同步技术。
摘要由CSDN通过智能技术生成

线程同步

  • 同步即协同步调,按预定的先后次序运行。
  • 线程同步:指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回,同时其他线程为保证数据一致性,不能调用该功能。
  • “同步”的目的:为了避免数据混乱,解决与时间有关的错误,实际上,进程间,信号间都要同步机制。
  • 关键字restrict:只用于限制指针,所有对这个指针指向内存的操作只能通过本指针完成,不能通过版指针以外的的其他变量或指针修改。

互斥量mutex(取值:0,1)

Linux 中提供一把互斥锁mutex(也称之为互斥量,初始化完成后mutex=1)。

  • 每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。资源还是共享的,线程间也还是竞争的,但通过“锁”就将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了。
  • 但应注意:同一时刻,只能有一个线程持有该锁。
    当A线程对某个全局变量加锁访问,B在访问前尝试加锁,拿不到锁,B阻塞。C线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱。所以,互斥锁实质上是操作系统提供的一把“建议锁”(又称“协同锁”),建议程序中有多线程访问共享资源的时候使用该机制。但并没有强制限定。因此,即使有了mutex,如果有线程不按规则来访问数据,依然会造成数据混乱。

创建互斥锁pthread_mutex_init(只供一个线程使用)

初始化一个互斥锁(互斥量)-->初值可看作1

#include<pthread.h>
int pthread_mutex_init(pthread_mutext*restrict mutex,const pthread_mutex_attr_t*restrict attr);1:传出参数,调用时应传&mutex
restrict关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改

参2:互斥量属性。是一个传入参数,通常传NULL,选用默认属性(线程间共享)。参APUE.12.4同步属性

1.静态初始化:如果互斥锁mutex是静态分配的(定义在全局,或加了static关键字修饰),
可以直接使用宏进行静态初始化。pthread_mutex_t muetx=PTHREAD_MUTEX_INITIALIZER;

2.动态初始化:局部变量应采用动态初始化。pthread_mutex_init(&mutex,NULL)

销毁互斥锁pthread_mutex_destroy

int pthread_mutex_destroy(pthread_mutex_t*mutex);

成功:返回0,失败:返回错误代码

加锁pthread_mutex_lock

可理解为将mutex--(或-1int pthread_mutex_lock(pthread_mutex_t*mutex);

尝试加锁pthread_mutex_trylock

可用于避免死锁
int pthread_mutex_trylock(pthread_mutex_t*mutex);

解锁pthread_mutex_unlock

可理解为将mutex++(或+1int pthread_mutex_unlock(pthread_mutex_t*mutex);
  • 在访问共享资源之前加锁,访问结束后立即解锁,锁的“粒度”越小越好。
  • 死锁:1.两次加锁,第二次lock被阻塞,导致程序处于等待状态。
    2.线程1拥有a锁,请求获得b锁,线程2拥有b锁,请求获得a锁。
  • 避免死锁:1.加锁之前检查是否解锁。
    2.利用trylock函数,当不能获取所有的锁时,放弃已经占有的锁。

互斥锁属性

#include <pthread.h>
/*初始化互斥锁属性对象*/
int pthread_mutexattr_init( pthread_mutexattr_t* attr ) ;

/*销毁互斥锁属性对象*/
int pthread_mutexattr_destroy( pthread_mutexattr_t* attr );

/*获取和设置互斥锁的pshared属性 */
int pthread_mutexattr_getpshared( const pthread_mutexattr_ t* attr, int* pshared );
int pthread_mutexattr_setpshared ( pthread_ mutexattr_t* attr, int pshared ) ;

/*获取和设置互斥锁的type属性*/
int pthread mutexattr_gettype( const pthread mutexattr_t* attr, int* type) ;
int pthread mutexattr_settype( pthread_mutexattr_t* attr, int type ) ;
  • 互斥锁的两种常用属性: pshared 和type
  • 互斥锁属性pshared指定是否允许进程共享互斥锁,其可选值有两个:
  • PTHREAD_PROCESS_SHARED。互斥锁可以被跨进程共享。
  • PTHREAD PROCESS_PRIVATE。互斥锁只能被和锁的初始化线程隶属于同一个进程的线程共享。
  • 互斥锁属性type指定互斥锁的类型。
  • PTHREAD_MUTEX_NORMAL,普通锁。
  • PTHREAD_MUTEX_ERRORCHECK,检错锁。
  • PTHREAD_MUTEX_RECURSIVE, 嵌套锁。
  • PTHREAD_MUTEX_DEFAULT,默认锁。
  • 例子:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>
#include<string.h>

pthread_mutex_t mutex;

int num = 0;
void *func()
{
        while(1)
        {
                pthread_mutex_lock(&mutex);
                num++;
                printf("num = %d\n",num);
                printf("sleeping......thread...\n");
                sleep(1);
                pthread_mutex_unlock(&mutex);
                sleep(1);
                }
}

int main()
{
        int pmi = pthread_mutex_init(&mutex,NULL);
        if(pmi != 0)
        {
                perror("init error:");
                exit(1);
        }

        pthread_t thread;
        pthread_create(&thread,NULL,func,NULL);

        for(int i = 0;i<5;i++)
        {
                pthread_mutex_lock(&mutex);
                num++;
                printf("num = %d\n",num);
                printf("sleeping......master...\n");
                sleep(1);
                pthread_mutex_unlock(&mutex);
                sleep(1);
        }
        pthread_cancel(thread);
        pthread_join(thread,NULL);
        pthread_mutex_destroy(&mutex);
        printf("num = %d\n",num);

        return 0;
}

读写锁特性:

1.读写锁是“写模式加锁”时,解锁前,所有对该锁加锁的线程都会被阻塞。
2.读写锁是“读模式加锁”时,如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。
3.读写锁是“读模式加锁”时,既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高
4.读写锁也叫共享独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享
5.读写锁非常适合于对数据结构读的次数远大于写的情况。

主要应用函数:

pthread_rwlock_init函数:初始化读写锁
pthread_rwlock_destroy 函数:销毁读写锁
pthread_rwlock_rdlock函数:对读写锁加“读”操作
pthread_rwlock_wrlock函数:对读写锁加“写”操作
pthread_rwlock_tryrdlock函数:尝试加读锁,不成功返回
pthread_rwlock_try_wrlock函数:尝试加写锁,不成功返回
pthread_rwlock_unlock函数:对读写锁解锁

以上7个函数的返回值都是:成功返回0,失败直接返回错误号。

pthread_rwlock_t类型用于定义一个读写锁变量。
pthread_rwlock_t rwlock;
  • 例子:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>
#include<string.h>

int counter = 0;
pthread_rwlock_t rwlock;

void *f_write(void *argc)
{
        int i = (int)argc;
        while(1)
        {
                sleep(1);
                pthread_rwlock_wrlock(&rwlock);

                counter++;
                printf("writing....counter = %d by[%d]\n",counter,i);
                pthread_rwlock_unlock(&rwlock);
        }
}

void *f_read(void *argc)
{
        int i = (int)argc;
        while(1)
        {
                pthread_rwlock_rdlock(&rwlock);

                printf("reading...counter = %d by[%d]\n",counter,i);

                pthread_rwlock_unlock(&rwlock);
                usleep(5000000);
        }

}
int main()
{
        int i;
        pthread_t thread[10];
        pthread_rwlock_init(&rwlock,NULL);

        for(i = 1;i<6;i++)
        {
                printf("craeting [%d] wrlock_thread...\n",i);
                pthread_create(&thread[i],NULL,f_write,(void *)i);
        }

        for(i = 6;i<10;i++)
        {
                printf("craeting [%d] rdlock_thread...\n",i);
                pthread_create(&thread[i],NULL,f_read,(void *)i);
        }

        while(1);
        
        for(i = 1;i<10;i++)
        {
                pthread_cancel(thread[i]);
        }
        pthread_rwlock_destroy(&rwlock);
        return 0;
}

生产者消费者条件变量模型

线程同步典型的案例即为生产者消费者模型,而借助条件变量来实现这一模型,是比较常见的一种方法。假定有两个线程,一个模拟生产者行为,一个模拟消费者行为。两个线程同时操作一个共享资源(一般称之为汇聚),生产向其中添加产品,消费者从中消费掉产品。

条件变量

  • 条件变量本身不是锁!但它也可以造成线程阻塞。通常与互斥锁配合使用。给多线程提供一个会合的场所。
#include <pthread.h>
int pthread_cond_init( pthread_cond_t* cond, const pthread_condattr_t* cond_attr);
int pthread_cond_destroy( pthread_cond_t* cond) ;
int pthread_cond_broadcast( pthread_cond_t* cond ) ;
//释放阻塞的所有线程
int pthread_cond_signal( pthread_cond_t* cond ) ;
//发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行
int pthread_cond_wait( pthread_cond_t* cond, pthread_mutex_t* mutex ) ;
//函数将解锁mutex参数指向的互斥锁,并使当前线程阻塞在cond参数指向的条件变量上。
//被阻塞的线程可以被pthread_cond_signal函数,
//pthread_cond_broadcast函数唤醒,也可能在被信号中断后被唤醒。
int pthread_once(pthread_once_t *once_control, void (*init_routine) (void))//本函数使用初值为PTHREAD_ONCE_INIT的once_control变量保证
//init_routine()函数在本进程执行序列中仅执行一次。
成功返回0,失败直接返回错误号。

pthread_cond_t类型用于定义条件变量
pthread_cond_t cond;

静态初始化:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

pthread_cond_wait函数

阻塞等待一个条件变量
int pthread_cond_wait(pthread_cond_t*restrict cond,pthread_mutex_t*restrict mutex);

函数作用:
1.阻塞等待条件变量cond(参1)满足
2.释放已掌握的互斥锁(解锁互斥量)相当于pthread_mutex_unlock(&mutex)1.2两步为一个原子操作。

3.当被唤醒,pthread_cond_wait 函数返回时,
解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex);

在调用pthread_cond_wait 前,必须确保互斥锁mutex已经加锁。
  • 例子(生产者&消费者):
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>
#include<string.h>

struct container
{
        char str[8];
        struct container *next;
};

struct container *head;
struct container *contain;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond_p = PTHREAD_COND_INITIALIZER;

void *producer()
{
while(1)
        {
                contain = (struct container *)malloc(sizeof(struct container));
                strcpy(contain->str,"huawei");
                printf("product---->%s\n",contain->str);

                pthread_mutex_lock(&mutex);
                contain->next = head;
                head = contain;
                pthread_mutex_unlock(&mutex);

                pthread_cond_signal(&cond_p);
                sleep(1);
        }
}

void *consumer1()
{
        while(1)
        {
        while(1)
        {
                contain = (struct container *)malloc(sizeof(struct container));
                strcpy(contain->str,"huawei");
                printf("product---->%s\n",contain->str);

                pthread_mutex_lock(&mutex);
                contain->next = head;
                head = contain;
                pthread_mutex_unlock(&mutex);

                pthread_cond_signal(&cond_p);
                sleep(1);
        }
}

void *consumer2()
{
        while(1)
        {
        pthread_mutex_lock(&mutex);
                while(head == NULL)
                {
                        printf("consumer2-----nothing...\n");
                        pthread_cond_wait(&cond_p,&mutex);
                }
                contain = head;
                head = contain->next;
                pthread_mutex_unlock(&mutex);
                printf("consume2------>%s\n",contain->str);

                free(contain);
                usleep(500000);
        }
}

int main()
{
        pthread_t thread1,thread2,thread3;
        pthread_create(&thread1,NULL,producer,NULL);
        pthread_create(&thread2,NULL,consumer1,NULL);
        pthread_create(&thread3,NULL,consumer2,NULL);

        while(1);

        return 0;
}

pthread_cond_timedwait 函数

限时等待一个条件变量
int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);

参3:参看man sem timedwait函数,查看 struct timespec.结构体。
struct timespec{
time_t ty_sec;/*seconds*/long ty_nsec;/*nanosecondes*/纳秒
}
形参abstime:绝对时间(1970/1/1  000000)。
如:time(NULL)返回的就是绝对时间。而alarm(1)是相对时间,相对当前时间定时1秒钟。
struct timespect={1,0]pthread_cond_timedwait (&cond,&mutex,&t);只能定时到19701100:00:01(早已经过去)
正确用法:
time_tcur=time(NULL);获取当前时间。
struct timespect;定义timespec结构体变量t
tty_sec=cur+1;定时1pthread_cond_timedwait(&cond,&mutex,&t)
在讲解 setitimer 函数时我们还提到另外一种时间类型:
struct timeval{
time_t ty_sec;/*seconds*/秒
suseconds_t ty_usec;/*microseconds*/微秒
}

条件变量的优点:

  • 相较于mutex而言,条件变量可以减少竞争。
    如直接使用mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争互斥锁是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序效率。

pthread_cond_wait函数

绝对时间:1970/1/1   00:00:00  --->unix 计时元年。

time_t cur=time(NULL);I
strcut timespect;
t.tv_sec=cur+10pthread_cond_timedwait(&cond,&mutex,&t)

信号量(可以使n个线程同时获取锁)

信号量基本操作:

  • sem_wait:信号量大于0,则信号量- -(类比pthread_mutex_lock),
    信号量等于0,造成线程阻塞
  • sem_post:将信号量++,同时唤醒阻塞在信号量上的线程(类比pthread_mutex_unlock)
  • 由于sem_t的实现对用户隐藏,所以所谓的++、- -操作只能通过函数来实现,而不能直接++、- -符号。
  • 信号量的初值,决定了占用信号量的线程的个数

创建并初始化信号量sem_init

#include<semaphore.h>
int sem_init(sem_t *sem,int pshared, unsigned int value);

sem:信号量
pshared:为0(PTHREAD_PROCESS_PRIVATE)用于线程间,
为非0(PTHREAD_PROCESS_SHARED)用于进程间
value:指定型号量初值

互斥量mutex

进程间也可以使用互斥锁,来达到同步的目的。但应在pthread_mutex_init初始化之前,修改其属性为进程间共享。mutex的属性修改函数主要有以下几个。

  • 主要应用函数:
pthread_mutexattr_t mattr类型:用于定义mutex锁的【属性】

pthread_mutexattr_init函数:初始化一个mutex属性对象
int pthread_mutexattr_init(pthread_mutexattrt*attr);

pthread_mutexattr_destroy 函数:销毁mutex属性对象(而非销毁锁)
int pthread_mutexattr_destroy(pthread_mutexattr_t*attr);

pthread_mutexattr_setpshared 函数:修改mutex属性。
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr,int pshared);

pshared取值:
线程锁:PTHREAD_PROCESS_PRIVATE(mutex的默认属性即为线程锁,进程间私有)
进程锁:PTHREAD_PROCESS_SHARED

文件锁(多线程中无法使用文件锁)

借助fcntl函数来实现锁机制。操作文件的进程没有获得锁时,可以打开,但无法执行read、write操作。

fcnt函数:获取、设置文件访问控制属性。
int fcntl(int fd,int cmd../”arg*/);

参2F_SETLK(struct flock*)   设置文件锁(trylock)
F_SETLKW(struct flock*)   设置文件锁(lock)W-->wait(阻塞)
F_GETLK(struct flock*)   获取文件锁

struct flock{
short l_type;锁的类型:F_RDLCK、F_WRLCK、F_UNLCK
short l_whence;偏移位置:SEEK_SETSEEK_CURSEEK_END
off_t l_start;起始偏移:1000
off_t l_len;长度:0表示整个文件加锁
pid_t l_pid;持有该锁的进程ID:(F_GETLK only)

例子:

struct flock lock;

lock.l_type=F_RDLOCK;
fcntl(fd,F_SETLKW,&lock)加锁

lock.l_type=F_UNLOCK
fcnt1(fd,F_SETLKW,&lock)解锁

sigprocmask函数来设置线程信号掩码:

#include <pthread. h>
#include <signal.h>
int pthread_sigmask ( int how, const sigset_t* newmask, sigset_t* oldmask );
  • 该函数的参数的含义与sigprocmask的参数完全相同,
  • 由于进程中的所有线程共享该进程的信号,所以线程库将根据线程掩码决定把信号发送给哪个具体的线程。因此,如果我们在每个子线程中都单独设置信号掩码,就很容易导致逻辑错误。此外,所有线程共享信号处理函数。
  • 在主线程创建出其他子线程之前就调用pthread_sigmask 来设置好信号掩码,所有新创建的子线程都将自动继承这个信号掩码。这样做之后,实际上所有线程都不会响应被屏蔽的信号了。
  • 在某个线程中调用如下函数来等待信号并处理之:
#include <signal.h>
int sigwait( const s1gset_t* set, int* sig );
  • set参数指定需要等待的信号的集合。我们可以简单地将其指定为在第1步中创建的信号掩码,表示在该线程中等待所有被屏蔽的信号。参数sie指向的整数用干存储该函数返回的信号值。
  • 将一个信号发送给指定的线程:
int pthread_kill( pthread_t thread, int sig ) ;
  • 其中,thread 参数指定目标线程,sig 参数指定待发送的信号。如果sig为0,则pthread_kill不发送信号,但它任然会执行错误检查。我们可以利用这种方式来检测目标线程是否存在。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值