多线程下的fork问题(模拟与解决)

前言

有关进程、多线程、fork的概念,请看我之前写的这两篇文章。
Linux:进程控制(进程创建、进程终止、进程等待、进程程序替换)
Linux:详解多线程(线程概念、线程控制—线程创建、线程终止、线程等待)(一)

1. 浅谈在多线程下的fork的问题

当fork函数创建出一个子进程的时候,子进程会拷贝父进程的进程虚拟地址空间,并且也会从父进程中拷贝一份相应的内存数据到子进程中,这是我们所知道的在单进程的情况下它是这样的,但是如果是在多线程中呢?在多线程代码中,如果在某个工作线程中调用fork函数时,fork函数是怎样运作的?是将该程序的整个工作线程都拷贝一份呢,还是只是拷贝当前创建出他的工作线程的虚拟地址空间?

在 POSIX 标准中,fork 的行为是这样的:复制整个用户空间的数据(通常使用 copy-on-write 的策略,所以可以实现的速度很快)以及所有系统对象, 然后仅复制当前线程到子进程。这里:所有父进程中别的线程,到了子进程中都是突然蒸发掉的。其它线程的突然消失,是一切问题的根源

在大多数操作系统上,为了性能的因素,锁基本上都是实现在用户态的而非内核态(因为在用户态实现最方便,基本上就是通过原子操作),所以调用fork的时候,会复制父进程的所有锁到子进程中。

这就造成了,如果线程A在创建子进程之前就有其他线程对当前全局变量中的锁进行了加锁操作(换句话说就是在创建子进程之前对应的锁就已经被拿到了),当创建出子进程之后,子进程和对应父进程就是两个完全不同的进程(进程独立性),但是子进程却是拷贝于父进程的进程虚拟空间,这就造成了如果父进程中已经拿到了锁,子进程中所对应的那把锁也是被加锁状态,但是子进程目前是不知道这把锁的状态的,因此,一旦在子进程中对这把锁进行加锁操作,就会造成死锁的现象产生。

总结一下,出现死锁的原因就是:

  • 父进程的内存数据会原封不动的拷贝到子进程中
  • 子进程在单线程状态下被生成,仅生成fork函数所在的这个线程。
  • 父进程(在创建子进程之前)中的某个锁已经被lock掉了,并且子进程中对这把锁再次进行lock,就会发生死锁

2. 死锁问题的模拟实现

#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>
#include <iostream>
using namespace std;


pthread_mutex_t mt;

void* pthreadFork(void * arg)
{
    pthread_detach(pthread_self());
	//等待2秒的原因是为了先让pthreadLock拿到锁
    sleep(2);
    int ret = fork();
    if(ret < 0)
    {
        cout << "fork failed" << endl;
        return 0;
    }
    else if(ret == 0)
    {
        //child
        cout << "It's Child !" << endl;
        while(1)
        {
            pthread_mutex_lock(&mt);
            cout << "It's test pthreadFork_Child " << endl;
            pthread_mutex_unlock(&mt);
        }
    }
    else
    {
        //father
        cout << "It's father!" << endl;
        wait(NULL);
    }

    return NULL;
}

void* pthreadLock(void * arg)
{
    pthread_detach(pthread_self());
    while(1)
    {
	    pthread_mutex_lock(&mt);
	    cout << "It's pthreadLock" << endl;
	    //等待3秒的原因是为了让pthreadFork线程,在已经拿到锁的情况下创建子进程
	    sleep(3);
	    pthread_mutex_unlock(&mt);
	}
    return NULL;
}

int main()
{
    pthread_t pid;

    pthread_mutex_init(&mt,NULL);
    int ret = pthread_create(&pid,NULL,pthreadFork,NULL);
    if(ret < 0)
    {
        cout << "pthread failed" << endl;
        return 0;
    }

    ret = pthread_create(&pid,NULL,pthreadLock,NULL);
    if(ret < 0)
    {
        cout << "pthread failed" << endl;
        return 0;
    }

    while(1)
    {
        sleep(1);
    }

    pthread_mutex_destroy(&mt);
    return 0;
}

结果验证:
在这里插入图片描述
使用ps -ef | grep xxx得到当前子进程的pid号,再用gdb attach pid对其进行调试

在这里插入图片描述
查看当前锁的状态,发现已经是死锁状态,或者说在刚开始时就拿到了一把废锁(不能进行加锁的锁)。
在这里插入图片描述
对12576这个线程进行调试,要对线程进行调试,首先使用gdb对其进程进行调试
在这里插入图片描述
在这里插入图片描述
这里,我们可以轻而易举的验证出,子进程发生了死锁,进入了阻塞状态,而父进程中的线程是在正常运行的(进一步体现了进程的独立性)。

3. 解决办法

解决①:多线程中在fork出的子进程中立刻调用exec函数(进程程序替换)即可。
有关进程程序替换的概念可以查看我在前言中给的链接。

解决②:使用pthread_atfork函数。

int pthread_atfork(void (*prepare)(void), void (*parent)(void),void (*child)(void));

  • prepare:prepare处理函数由父进程在fork创建子进程前调用,这个函数的任务是获取父进程定义的所有锁。
  • parent:parent处理函数是在fork创建了子进程以后,但在fork返回之前在父进程环境中调用的。它的任务是对prepare获取的所有锁解锁。
  • child:child处理函数在fork返回之前在子进程环境中调用,与parent处理函数一样,它也必须解锁所有prepare中所获取的锁。

代码实现:

#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>
#include <iostream>
using namespace std;


pthread_mutex_t mt;

void* pthreadFork(void * arg)
{
    pthread_detach(pthread_self());

    sleep(2);
    int ret = fork();
    if(ret < 0)
    {
        cout << "fork failed" << endl;
        return 0;
    }
    else if(ret == 0)
    {
        //child
        cout << "It's Child !" << endl;
        while(1)
        {
            pthread_mutex_lock(&mt);
            cout << "It's test pthreadFork_Child " << endl;
            pthread_mutex_unlock(&mt);
        }
    }
    else
    {
        //father
        cout << "It's father!" << endl;
        wait(NULL);
    }

    return NULL;
}

void* pthreadLock(void * arg)
{
    pthread_detach(pthread_self());
    while(1)
    {
        pthread_mutex_lock(&mt);
        cout << "It's pthreadLock" << endl;
        sleep(3);
        pthread_mutex_unlock(&mt);
        //这里睡眠1秒的原因是能够让prepare函数拿到锁
        sleep(1);
    }
    return NULL;
}

void prepare(void)
{
    pthread_mutex_lock(&mt);
    cout << "prepare: Get mutex success" << endl;
}

void child(void)
{
    pthread_mutex_unlock(&mt);
    cout << "child: release mutex success" << endl;
}
void parent(void)
{
    pthread_mutex_unlock(&mt);
    cout << "parent: release mutex success" << endl;
}

int main()
{
    pthread_t pid;

    int ret = pthread_atfork(prepare,parent,child);

    pthread_mutex_init(&mt,NULL);
    ret = pthread_create(&pid,NULL,pthreadFork,NULL);
    if(ret < 0)
    {
        cout << "pthread failed" << endl;
        return 0;
    }

    ret = pthread_create(&pid,NULL,pthreadLock,NULL);
    if(ret < 0)
    {
        cout << "pthread failed" << endl;
        return 0;
    }

    while(1)
    {
        sleep(1);
    }

    pthread_mutex_destroy(&mt);
    return 0;
}

结果验证:
在这里插入图片描述

可以明显的发现子进程可以对这把锁进行相应的操作了,这也就很好的解决此类问题。

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
哲学家就餐问题是一个经典的并发编程问题,用来展示多线程编程中的死锁和竞争条件等问题。下面是一个使用C++多线程解决哲学家就餐问题的示例代码: ```c++ #include <iostream> #include <thread> #include <mutex> using namespace std; const int kNumPhilosophers = 5; // 哲学家数量 mutex forks[kNumPhilosophers]; // 叉子锁 void philosopher(int id) { int left_fork = id; int right_fork = (id + 1) % kNumPhilosophers; // 模拟哲学家思考 cout << "Philosopher " << id << " is thinking." << endl; this_thread::sleep_for(chrono::seconds(1)); // 尝试获得叉子 cout << "Philosopher " << id << " is hungry and wants to eat." << endl; forks[left_fork].lock(); forks[right_fork].lock(); // 开始就餐 cout << "Philosopher " << id << " is eating." << endl; this_thread::sleep_for(chrono::seconds(1)); // 释放叉子 forks[right_fork].unlock(); forks[left_fork].unlock(); // 就餐结束 cout << "Philosopher " << id << " finished eating and is thinking again." << endl; } int main() { // 创建哲学家线程 thread philosophers[kNumPhilosophers]; for (int i = 0; i < kNumPhilosophers; i++) { philosophers[i] = thread(philosopher, i); } // 等待所有哲学家线程结束 for (int i = 0; i < kNumPhilosophers; i++) { philosophers[i].join(); } return 0; } ``` 在这个示例代码中,我们使用了`mutex`来实现叉子的锁机制,每个哲学家线程都会先尝试获得左边和右边的叉子,如果获得成功就开始就餐,否则就会等待。这个示例代码简单易懂,但是仍然存在死锁和竞争条件等问题,需要进一步的优化和改进。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值