多线程之同步与互斥

1.线程的基本概念

  多线程可以将计算密集型(实时)和I/O密集型(实际中会等待)应用分配到多个处理器上执行,提高执行效率,但是会增加调度成本。

  Linux没有专门的TCB(Thread Control Block),而是复用了进程的PCB;不同的PCB指向同一个地址空间,并将地址空间的数据段和代码段进行划分分配给不同的PCB

  进程是资源分配的基本单位,线程是运算调度的基本单位。Linux线程创建时不用像进程那样创建PCB、地址空间、页表、构建物理内存的映射,而只需要创建一个PCB,并将进程的资源分配给线程。

结构图

线程的大部分资源是共享的,但是必须私有栈(临时数据等)、上下文标志(调度信息)。

用户级线程库:

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

void *mythread(void *arg)
{
    while(1)
    {   
        printf("new thread is running : arg val = %s; PID:%d\n", (char*)arg, getpid());
        sleep(1);
    }   
}
int main()
{
    pthread_t pid;
    //注意makefile编写包含thread 库
    pthread_create(&pid, NULL, mythread, (void*)"thread 1");

    while(1)
    {   
        printf("Hold thread; PID:%d\n",getpid());
        sleep(1);
    }   
    return 0;
}
main:main.c                                                                             
    gcc -o $@ $^ -lpthread
    
.PHONY:clean
clean:
    rm -f main

使用ps -aL查看轻量级进程(LWP):

test1

  可以看到这两个线程具有相同的PID,但是LWP的ID是不同的。Linux系统调度是根据LWP执行的,在只有一个进程的情况下PID = LWP

  另外多线程会降低代码健壮性(一个线程崩溃,产生的信号直接发给进程,造成进程崩溃)、缺乏访问控制、增加编程难度。

以下代码将演示线程的创建、等待(类似进程waitpid,回收系统资源)、线程终止、线程分离

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

void* mythread(void* arg)   //多个执行流共同执行,被重入的函数
{
    //pthread_detach(pthread_self()); 
    //线程分离,新线程不需要再等待了(不能pthread_join),业务处理完直接退出
    while (1)
    {
        printf("thread handler : \n");
        //打印线程号,这个ID号是线程库分配的(是一个虚拟内存地址,在栈和堆之间的共享区,里面存储着LWP),与OS内核的LWP号不同。
        printf("new thread ID:%lu\n", pthread_self()); 
    
        sleep(1);
        break;
    }
    //三种方式终止线程 : 其中一种为pthread_cancel(),在m主线程中调用
    //return (void*)("thread return\n");
    pthread_exit((void*)("thread return\n")); //与exit不同,使用exit会直接终止整个进程
}
int main()
{
    int i;
    pthread_t pid[3];
    void* status = NULL;

    for (i = 0; i < 3; i++)
    {
        //创建线程
        pthread_create(pid + i, NULL, mythread, (void*)"thread 0");
    }

    //pthread_cancel(tid[0]);//线程的退出码为-1, 其为宏定义:PTHREAD_CANCELED 
    //若取消主线程后不主动join回收,则会出现僵尸线程

    //int pthread_join(pthread_t thread, void **retval); //线程等待,对于异常退出的线程进行等待,回收资源
    pthread_join(pid[0], &status);      //不会处理异常信号,异常由进程处理
    printf("ret : %s\n", (char*)status);//获取线程退出的返回值
    while (1)
    {
        printf("\n main thread: \n");
        printf("---------------------------\n");
        for (i = 0; i < 3; i++)
        {
            printf("线程号[%d]:", i);
            printf("main thread ID:%lu; pthread_self: %lu\n", pid[i], pthread_self());
            sleep(1);
        }
        printf("---------------------------\n");
    }

    return 0;
}

运行结果:

在这里插入图片描述

2.线程的互斥和同步

2.1 基本概念

  • 临界资源:多线程执行流共享的资源
  • 临界区:访问临界资源的代码
  • 原子性:只有完成和未完成两种状态。
  • 互斥:同一时间只能允许一个线程访问临界资源,可以加锁实现。加锁可以保证单个线程对临界资源的访问是原子的。
  • 同步:让线程顺序访问临界资源,避免某个线程因抢占独占临界资源
  • 线程安全:多线程执行同一代码,若执行结果是一致的就是线程安全的。
  • 可重入:函数被多个执行流调用,其执行结果一致。可重入一定是线程安全的,线程安全不一定是可重入的。

2.2 互斥的实现

一段加锁的代码:

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

int count = 0; //临界资源
pthread_mutex_t lock;// 锁也是临界资源,但是锁的申请是原子的

void *mythread(void *arg)
{
    while(1)
    {   
        pthread_mutex_lock(&lock);//线程加锁
        //若在执行锁内的代码发生了线程切换,其他线程也无法访问临界资源,因为之前的线程是带锁的                                                                                             
        if(count < 100) 
        {   
            printf("[%s]:count = %d\n",arg, count);
            count++;
            usleep(100);
        }   
        else
        {   
            pthread_mutex_unlock(&lock);//线程解锁
            break;
        }   
        pthread_mutex_unlock(&lock);//线程解锁

    }   
    return (void*)("end\n");

}
int main()
{
    int i;
    pthread_t pid[3];

    pthread_mutex_init(&lock,NULL);//初始化锁
        
    pthread_create(pid, NULL, mythread, (void*)"thread 0");
    pthread_create(pid+1, NULL, mythread, (void*)"thread 1");
    pthread_create(pid+2, NULL, mythread, (void*)"thread 2");
    for(i = 0; i < 3; i++)
    {
        pthread_join(pid[i],NULL);
    }
    pthread_mutex_destroy(&lock); //销毁锁
    return 0;
}

加锁的理解:

  mutex(值为1)在内存中只保存了一份,当其中一个线程拿走后,内存中的mutex会被置0;其他线程拿走0的mutex后就会被挂起。CPU寄存器不是被所有线程共享的,他只保存了当前线程的上下文信息。mutex与CPU寄存器的值交换是通过一条指令实现的,不存在中间过程,所以在一个时钟周期内,一个线程拿走mutex后就不会被另一线程拿走了,因此加锁是原子的。

  加锁解决了临界资源的访问问题,但是造成了性能损失,在一些情况下还会出现死锁使得程序卡死。

死锁的四个必要条件:

  • 互斥:同一时间,一分资源只能被一个执行流访问
  • 占有并等待:一个执行流占有一份资源(锁资源),即使自己阻塞了也不释放,使得另一个执行流无法申请该资源只能处于等待状态
  • 无法抢占:执行流还未完成当前的任务,此资源无法被另一个执行流抢占
  • 循环等待:对于资源的申请是无序的,每份资源被不同的执行流占有,每个执行流都在等待其他执行进行资源释放。

破坏以上任一条件都可避免死锁

2.3 同步的实现

  当某个线程对锁的竞争能力较强,会出现该线程一直重复申请和占有资源,造成其他线程无法访问的情况。因此引入同步,让每个线程都能有效访问临界资源。

实现线程同步可以使用条件变量或者信号量。

条件变量实现同步:

#include <iostream>
#include <cstdlib>
#include <queue>
#include <pthread.h>
#include <ctime>
#include <unistd.h>

template<class T>
class blockchain
{
public:
    blockchain(const int capacity = 64)
        :_capacity(capacity)
    {
        pthread_mutex_init(&_lock, nullptr);
        pthread_cond_init(&_condFull, nullptr);
        pthread_cond_init(&_condEmpty, nullptr);
    }

    ~blockchain()
    {
        pthread_cond_destroy(&_condFull);
        pthread_cond_destroy(&_condEmpty);
        pthread_mutex_destroy(&_lock);
    }
    bool queFull()
        return _que.size() == _capacity;
 
    bool queEmpty()
        return _que.empty();
    
    void Push(const T& x) //生产过程
    {
        pthread_mutex_lock(&_lock);
        //1.需要循环检测条件就绪的状态,可以防止wait调用失败(应该挂起却没有挂起)后代码继续执行
        //2.若生产者处于生产阶段,生产一个数据后向消费者发生一个signal唤醒消费,若无while,假设是if。
        //  if在进入后只判断了一次,之后即使条件满足仍然会跳出判断执行后面的代码,执行完消费后又发送了signal给
        //  生产者,而生产者本就处于唤醒状态。不应唤醒的线程被唤醒了称为伪唤醒。
        //3.在消费过程中,每一次消费完成都会发送一个signal通知生产者生产,
        //  此时本因继续消费的,却又开始生产,所以使用while阻塞询问是否生产就绪,就绪了再执行生产的代码
        while(queFull()) //缓冲区满了就要阻塞等待
        {
            pthread_cond_wait(&_condFull, &_lock);
            //第二个参数作用:1.判断等待的时候,线程是拿着锁的资源挂起的,为了防止死锁,需要将_lock释放
            //2.若该条件被唤醒,此函数会为线程自动竞争锁,原因:
            //挂起时是在代码运行到此数被挂起的,寄存器会记录当前函数的上下文信息,被唤醒时继续在此处向后执行
            //线程原来是拥有锁的资源的,因此在唤醒后要重新获取到锁资源,然后再继续执行。
        }
        _que.push(x);     
        pthread_cond_signal(&_condEmpty); //生产者线程唤醒消费者
        pthread_mutex_unlock(&_lock); 
    }

    void Pop(T& ret) //消费过程
    {
        pthread_mutex_lock(&_lock);
        while(queEmpty()) //缓冲区空了就要阻塞等待
        {
            //若直接return,就相当于是消费者轮询,消费者一直在询问是否有数据
            //若消费者竞争锁的能力较强,则会一直保有锁的资源,使得生产者无法生产
            //因此当队列为空时,就要让消费者线程链接到等待队列,等待生产者生产
            //生产者生产了一定数据后,可以向消费者线程发送signal,让消费者继续消费
            //生产者部分同理。
            pthread_cond_wait(&_condEmpty, &_lock);             
        }
        ret = _que.front();
        _que.pop();
        pthread_cond_signal(&_condFull);
        pthread_mutex_unlock(&_lock);
    }
private:
    std::queue<T> _que;
    int _capacity;
    pthread_mutex_t _lock;
    pthread_cond_t _condFull; //队列为满标志,需要消费者消费
    pthread_cond_t _condEmpty;//队列为空标志,需要生产者生产
};


void* ConsumerBuy(void *arg)
{
    blockchain<int> *q = (blockchain<int>*)(arg);
    while (1)
    {
        int out;
        q->Pop(out);
        std::cout << "Data Consumed:" << out << std::endl;
    }
}

void* ProducerMake(void* arg)
{
    blockchain<int> *q = (blockchain<int>*)(arg);
    while (1)
    {
        int tmp = rand() % 100;
        std::cout << "Data Produced:" << tmp << std::endl;
        q->Push(tmp);
        sleep(1);
    }
}

int main()
{
    srand((unsigned int)time(nullptr));
    blockchain<int> *p = new blockchain<int>();
    pthread_t consumer;
    pthread_t producer;

    pthread_create(&consumer, nullptr, ConsumerBuy, (void*)p);
    pthread_create(&producer, nullptr, ProducerMake, (void*)p);

    pthread_join(consumer, nullptr);
    pthread_join(producer, nullptr);

}

以上代码是基于生产消费者模型,使用条件变量实现线程同步操作的例子。

补充:生产消费者模型

  • 生产消费者模型是代码解耦的过程,使得一个任务可以并行执行,而本来的函数调用是强耦合,只能由同一线程顺序执行。而在生产消费者模型中,生产者生产一些数据后,将数据保存在一段缓冲区(临界资源)以供消费者获取,这样同一段代码可由不同的线程合作完成,提高了代码运行效率
  • 生产者和消费者都会对临界资源进行访问,因此需要加锁实现互斥。单就生产者或者单消费者来说,由于实现的任务是类似的,其存在对锁资源的竞争;而在生产者和消费者之间,为了保证生产者有空间可以存储生产的数据,消费者有资源进行消费,需满足一定的条件,可以用同步实现。

参考资料

[1]: 《Computer Systems:A Programmer’s Perspective》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值