【线程同步2】使用互斥锁实现线程同步


互斥锁基本概念

互斥锁是一种使用频繁的同步手段,也被称为互斥量。对比信号量的使用,我们可以将互斥锁的使用理解为信号量初值仅为1的一种情况。

互斥锁是属于系统的内核级对象,它能够使线程拥有某个资源的绝对访问权,互斥锁主要包括使用数量、线程ID,递归计数器等,其中线程ID表示当前拥有互斥锁的线程,递归计数器表示线程拥有互斥锁的次数。

  • 当互斥锁的线程ID为0时,表示互斥锁不被任何线程所拥有,此时系统会发出该互斥锁的通知信号,等待该互斥锁的其他线程中的某一个线程会拥有该互斥锁,同时,互斥锁的线程ID为当前拥有该互斥锁的线程的线程ID。
  • 当互斥锁的线程ID不为0时,表示当前有线程拥有该互斥锁。系统不会发出互斥锁的通知信号。其他等待互斥锁的线程继续等待,直到拥有改互斥锁的线程释放互斥对象的拥有权。

互斥锁可以进行分类:

分类 函数定义
静态分配互斥锁 pthread_mutex_t mutex= PTHREAD_MUTEX_INITIALIZER;
动态分配互斥锁 pthread_mutex_init(&mutex, *mutexattr);pthread_mutex_destroy(&mutex);

每一个互斥锁都有一个数据类型为pthread_mutex_t的互斥变量,表格中的静态分配互斥锁中的PTHREAD_MUTEX_INITIALIZER是一个结构常量,所以是可以直接创建一个互斥锁用该常量进行初始化。

或者就使用动态的方式初始化一个互斥锁,这种方式的互斥锁在使用结束后一定要进行销毁。其中初始化的pthread_mutex_init的参数中需要传递一个mutexattr的属性值,而这种锁的属性共有四种。

属性值 意义
PTHREAD_MUTEX_TIMED_NP 这是缺省值,相当于NULL。当一个线程加锁后,其他请求这个锁的线程会进入一个队列等待,锁释放后按队列优先级获得锁。具有公平性。
PTHREAD_MUTEX_RECURSIVE_NP 嵌套锁,允许同一个线程多次获得同一个锁,并多次解锁
PTHREAD_MUTEX_ERRORCHECK_NP 检错锁,如果同一个线程请求同一个锁,返回EDEADLK;负责和PTHREAD_MUTEX_TIMED_NP操作一样(进入等待队列)
PTHREAD_MUTEX_ADAPTIVE_NP 适应锁,如果被锁上了,等解锁后重新竞争,不存在等待队列,而是解锁后先到先得。

互斥锁相关函数

操作 函数 意义
阻塞加锁 int pthread_mutex_lock(pthread_t *mutex) 锁空闲,立即加锁否则阻塞等待直至解锁
非阻塞加锁 int pthread_mutex_trylock(pthread_t *mutex) 锁空闲,立即加锁;否则立即返回 EBUSY而非等待
避免死锁的加锁 int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timesec *restrict tsptr); 可设置等待时间,若超时锁未释放,返回ETIMEDOUT,避免两个或多个线程加了锁,又彼此等待锁的释放而产生死锁。
解锁 int pthread_mutex_unlock(pthread_t *mutex) 有加锁必然有解锁
销毁锁 int pthread_mutex_destroy(pthread_mutex *mutex); 互斥锁也是一种资源,使用完毕后需要释放

互斥锁的使用

静态互斥锁的使用

代码如下:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>

using namespace std;

pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;

void* func(void* arg){
    
    
    sem_t* p = (sem_t*)arg;
    for(int i=0; i<10; ++i){
    
    
        usleep(100);
        pthread_mutex_lock(&m);
        cout << pthread_self() << ":before" << endl;
        usleep(100);
        cout << pthread_self() <<  ":exit" << endl;
        pthread_mutex_unlock(&m);
    }
}

int main(){
    
    
    pthread_t tid;
    pthread_create(&tid, NULL, func, NULL);

    usleep(500); // 等待子进程跑起来

    func(NULL);
    pthread_join(tid, NULL);
}

执行部分结果如下:

在这里插入图片描述

动态互斥锁的使用

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>

using namespace std;


void* func(void* arg){
    
    
    pthread_mutex_t* p = (pthread_mutex_t*)arg;
    for(int i=0; i<10; ++i){
    
    
        usleep(100);
        pthread_mutex_lock(p);
        cout << pthread_self() << ":before" << endl;
        usleep(100);
        cout << pthread_self() <<  ":exit" << endl;
        pthread_mutex_unlock(p);
    }
}

int main(){
    
    
    pthread_mutex_t m;
    pthread_mutex_init(&m, NULL);
    pthread_t tid;
    pthread_create(&tid, NULL, func, &m);

    usleep(500); // 等待子进程跑起来

    func(&m);
    pthread_join(tid, NULL);
    pthread_mutex_destroy(&m);
}

执行部分结果如下:

在这里插入图片描述
显然这两种方式的互斥锁都保证在临界区内只有一个线程运行,保证了线程的同步。

C++11标准库中的mutex

这里我们直接使用标准库初始化一个互斥锁,进行加锁和解锁,具体代码如下:

#include <iostream>
#include <thread>
#include <mutex>

using namespace std;

int main(){
    
    
    mutex m;
    auto func = [&m](){
    
      // 互斥锁是一种资源,不能拷贝,只能移动
        for(int i=0; i<5; i++){
    
    
            m.lock();  // 加锁
            cout << this_thread::get_id() << ":before" << endl;
            this_thread::sleep_for(500ms);
            cout << this_thread::get_id() << ":after" << endl;
            m.unlock(); // 解锁
        }
    };

    thread t(func);
    func();
    t.join();
}

除此以外,我们可以不使用函数lock()unlock()函数,而是直接使用lock_guard来实现加锁和解锁的函数,具体代码如下:

#include <iostream>
#include <thread>
#include <mutex>

using namespace std;

int main(){
    
    
    mutex m;
    auto func = [&m](){
    
    
        for(int i=0; i<5; i++){
    
    
            lock_guard<mutex> guard(m);
            cout << this_thread::get_id() << ":before" << endl;
            this_thread::sleep_for(500ms);
            cout << this_thread::get_id() << ":after" << endl;
        }
    };

    thread t(func);
    func();
    t.join();
}

其中的lock_guard<mutex>是守护互斥锁,借助一个对象在离开其作用域时将会自发调用析构函数这个特点,以上的代码块当守护锁离开其所在的作用域时将会解锁。

生产者-消费者实现

猜你喜欢

转载自blog.csdn.net/weixin_50941083/article/details/124928650