记一次死锁排查

一、代码场景

将FTP服务器设计为多线程形式。

FTP服务器在处理客户端响应时,对数据连接描述符dataFd和控制连接描述符ctrlFd分别进行集中处理。

因为方便用select()多路复用,开两个线程分发连接到来的事件。

1. 整体框架

void
addToControlServer (int connFd, struct sockaddr_in clientAddr)
{
	static Server *server = new ControlServer();
	server->addClient (connFd, clientAddr);
}

void
addToDataServer (int connFd, struct sockaddr_in clientAddr)
{
	static Server *server = new DataServer();
	server->addClient (connFd, clientAddr);
}

void
solveConrtrolConnection ()
{
	while (1)
	{
		int connfd = accept (listenFd, (struct sockaddr *)&clientAddr, &clientAddrLen);

		addToControlServer (connfd, clientAddr);
		
	}
}

void
solveDataConnection ()
{
	...
	while (1)
	{
		int connfd = accept (listenFd, (struct sockaddr *)&clientAddr, &clientAddrLen);

		addToDataServer (connfd, clientAddr);
	}
}
int main()
{
	std::thread ctrlThread([]{	solveConrtrolConnection(); })
	std::thread dataThread([]{	solveDataConnection(); })
	ctrlThread.join();
	dataThread.join();
	retun 0;
}

2. Server实现

Server底层使用了select()多路复用。

class Server : public ErrorUtil {
public:
	Server();
	void addClient (int fd, struct sockaddr_in addr);
	void removeClient (int fd);
protected:
	virtual void preAdd (int fd, struct sockaddr_in addr);
	virtual void postRemove (int fd);
	virtual void workWhenDataCome (int fd) = 0;
private:

	class Impl;
	std::shared_ptr<Impl> m_pImpl = nullptr;
};

class Server::Impl {
public:
	Impl (Server *server)
	{
		assert (server);
		m_server		= server;
		auto workThread = std::thread (&Impl::threadEntry, this);
		workThread.detach();
		dispatcher.waitForStartCompleted();
	}

	void addClient (int fd, struct sockaddr_in addr)
	{
		m_server->preAdd (fd, addr);
		dispatcher.addFd (fd);

		dispatcher.stopWait();
	}

	void removeClient (int fd)
	{
		dispatcher.removeFd (fd);

		dispatcher.stopWait();
		m_server->postRemove (fd);
	}

private:

	void threadEntry ()
	{
		while (true) {
			std::vector<int> readAbleFds = dispatcher.waitForReadAble();

			std::for_each (readAbleFds.begin(), readAbleFds.end(), [&] (int fd) { m_server->workWhenDataCome (fd); });
		}
	}

	EasySelect dispatcher;
	Server *m_server;
};

ControlServer处理控制连接相关的逻辑:

class ControlServer : public Server {
...
private:
	void stopService (int fd)
	{	
		...
		removeClient (fd);
	}

	void workWhenDataCome (int fd) override
	{
		...
		stopService (fd);
		...
	}

	...
};

二、死锁位置

代码不少,没必要全看。
死锁的位于是ControlServer中对virtual void workWhenDataCome (int fd) = 0;的重实现:

if (nRead == 0) {
	...
	stopService (fd);
	...
}

问题就出现在stopService()

void stopService (int fd)
{
	...
	removeClient (fd);
}

stopService()中调用了removeClient()removeClient()中调用了另一个类EasySelectstopWait()方法

这里EasySelect仅仅是对select()进行了易用性封装。

void
EasySelect::Impl::stopWait()
{
	char ANY_CHAR = 0;
	IOUtil::writen (m_pipeWrite, &ANY_CHAR, 1);

	std::unique_lock<std::mutex> lock (m_notifyMutex);
	m_stopFromPipeCond.wait (lock, [&] { return m_stopFromPipe == true; });
	m_stopFromPipe = false;

	IOUtil::readn (m_pipeRead, &ANY_CHAR, 1);
}

waitForReadable()则相当于select()系统调用,里面利用匿名管道来实现对select()的中断。

std::vector<int>
EasySelect::Impl::waitForReadable()
{
	fd_set fdSet;
	FD_ZERO (&fdSet);
	int fd_limits = -1;
	std::for_each (m_fds.begin(), m_fds.end(), [&] (int fd) {
		FD_SET (fd, &fdSet);
		fd_limits = std::max (fd_limits, fd);
	});
	FD_SET (m_pipeRead, &fdSet); //把管道读端放进去
	fd_limits = std::max (fd_limits, m_pipeRead);
	++fd_limits;
	{
		m_isWaiting = true;
		m_startCompleted.notify_one();
	}
	int nReadAble = ::select (fd_limits, &fdSet, NULL, NULL, NULL);
	if (nReadAble == -1) {
		setError (strerror (errno));
		return {};
	}
	m_isWaiting = false;
	std::vector<int> ret;
	std::for_each (m_fds.begin(), m_fds.end(), [&] (int fd) {
		if (FD_ISSET (fd, &fdSet) && fd != m_pipeRead) {
			ret.emplace_back (fd);
		}
	});

	if (ret.empty()) {
		std::lock_guard<std::mutex> lock (m_notifyMutex);
		m_stopFromPipe = true;
		m_stopFromPipeCond.notify_one();
	}

	return ret;
}

现在的程序实际只有两个线程:主线程(只用来建立连接)、ctrlFd处理线程。

原本的设想是在给每个客户端都再分配一个线程来执行任务,为了测试暂且只用了ctrlFd线程来串行处理所有任务。

下面的workWhenDataCome调用了stopWait(),在ctrlFd线程中执行。
然而stopWait()需要等待条件变量m_stopFromPipeCond的触发,这个条件变量又是在waitForReadAble()时才会产生的,线程现在卡在stopWait()处,根本不会执行waitForReadAble()

void threadEntry ()
{
	while (true) {
		std::vector<int> readAbleFds = dispatcher.waitForReadAble();

		std::for_each (readAbleFds.begin(), readAbleFds.end(), [&] (int fd) { m_server->workWhenDataCome (fd); });
	}
}

于是就产死锁了。

三、吸取教训

一定要搞清楚每个函数在执行时各自跑在哪个线程,就像看到变量就应该明白它在哪片内存空间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

barbyQAQ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值