udp
tcp——多进程
tcp——多线程
tcp——线程池
tcp——守护进程
🎆udp
✨pthread_create 错误总结
✨LockGuard错误总结
✨服务端需要写成多线程
✨客户端也需要写成多线程
✨多线程调试工具
🎆tcp
✨tcp独有调试工具——telnet
✨ThreadPool —— 短连接
✨pthread_create —— 长连接
✨fork——长连接
✨进程池版的守护进程
🎆udp
✨1. pthread_create
错误总结
static void* Routine(void* argv)
{
// 线程本身就是运行起来一次就要结束,结束的动作由外部执行
Thread* th = static_cast<Thread*>(argv);
th->_fun(th->_info);// 线程自动调用这个函数,并将参数传递过去
return nullptr;
}
bool Start()
{
cout << _name ;
puts(" 线程将要运行");
int n = pthread_create(&_tid,nullptr,Routine,this);
cout << _name;
puts(" 线程已经运行");
if(n)
return false;
return true;
}
pthread_create
不能使用 bind
进行绑定,只能使用static
函数进行创建
✨2. LockGuard
错误总结
class LockGuard
{
public:
// 锁必须传指针,不能传值/引用
// 传值/引用会造成死锁现象
LockGuard(const std::string& func_name, pthread_mutex_t* mtx):_func_name(func_name),_mutex(mtx)
{
// puts("上锁");
std::cout << "LockGuard:" << _func_name << "上锁" << std::endl;
pthread_mutex_lock(_mutex);// 死锁位置
// puts("上锁完成");
std::cout << "LockGuard:" << _func_name << "上锁完成" << std::endl;
}
~LockGuard()
{
// puts("开始解锁");
std::cout << "LockGuard:" << _func_name << "解锁" << std::endl;
pthread_mutex_unlock(_mutex);
// puts("解锁完成");
}
private:
// pthread_mutex_t _mutex = PTHREAD_MUTEX_INITIALIZER;// 这个锁不能初始化,这个锁用的是外面的锁
pthread_mutex_t* _mutex;
std::string _func_name;
};
必须使用传指针
的方式,如果使用传引用/传值
的方式,就会出现死锁
✨3. 服务端
需要写成多线程
如果写成单线程,可能会出现服务端只为一个服务,另一个发送消息显示不出来的情况;当此连接释放才能将内容显示出来
✨4. 客户端
也需要写成多线程
需要
一个send线程,一个recv线程
如果只有一个线程,线程如果阻塞在send线程,也就是需要从标准输入中读取才能进行发送,那么就无法进行recv;由于udp有自己的缓冲区,所以即使无法运行recv也是可以将少量消息
进行记录
✨5. 多线程调试工具
pstack
—— 查看多进程函数栈
gdb attach
线程id
info threads
——查看线程信息
t 线程编号
——切换线程
info b
——查看线程信息
详细请参考
多进程调试工具
🎆tcp
✨tcp独有调试工具——telnet
在没有写客户端
的情况下还想进行调试
,可以使用telnet
进行调试,telnet只能对tcp
进行使用
✨ThreadPool
—— 短连接
-
什么是短连接
长连接
:将一个接收和发送作为一个任务,一个线程一直做这一个工作,知道客户端关闭,线程结束
短连接
:将一个接收和发送作为一个任务,一个套接字执行一个任务之后立即释放 -
理解为什么
TheadPool
模型必须使用短连接
线程池中线程个数有限
,如果使用长连接
,服务端只能接收有限的用户
,很显然是不合理的 -
使用
function包装函数类型
,不同头文件
中可能出现函数类型重名
的问题
-
灵活规定ThreadPool的任务类型
问题一
:为什么ThreadPool的类型是Task
,为什么不能是一个套接字或者别的
首先我们要明确线程要执行的任务
是:使用套接字进行接收+发送消息
问题二
:accept 函数是在主线程or子线程
肯定是在主线程
中,如果在子线程中还需要考虑访问套接字冲突的问题
所以传递套接字应该是传递的接收消息的套接字
问题三
:为什么要将执行函数封装进去,可不可以在Task类中进行执行
在Task类中实现执行函数完全是OK的
,最关键的一点是要在Task类中重载()
,这样在使用的时候就可以像执行函数一样执行了
注意
:在子线程中,执行完一个任务
需要将这个任务的接收消息的套接字关闭
,防止资源消耗完
✨pthread_create
—— 长连接
- 一定要明确变量是要被
外部使用
还是只在本作用域中使用
如果局部变量需要传给外部进行使用,并且接收的参数是指针,当外部进行使用的时候,当前空间已经被回收,就无法拿到原来的数据
解决方法:
方法一
:将接收参数改为传值接收
方法二
:传递堆空间
- 创建
子线程
需要进行分离
;每个子线程
使用的是不同的描述套接字
,所以不存在访问冲突
的问题,不需要加锁
✨fork
——长连接
- 因为
进程有很强的独立性
,所以可以将接收数据的套接字写成成员变量
;父子进程间会发生写实拷贝
,所有每个进程都会独立的维护自己的接收数据的套接字
,完全不用担心在接收到一个全新的的连接之后会将上一个接收数据的套接字进行覆盖的情况
满满的细节
细节一
:子进程需要关闭父进程中的接收连接的套接字文件描述符
——防止文件描述符的消耗
细节二
:为了不让父进程等待子进程退出,使用孙子进程
的方式解决;子进程会立即结束,父进程也会立即回收子进程并等待下次连接
细节三
:孙子进程连接结束一定要关闭文件描述符
;首先关闭文件描述符是一个好习惯,其次文件描述符是使用引用计数的方式实现的,子进程退出,系统会自动回收子进程的资源,包括文件描述符
,虽然在代码中没有什么危险,但是还是要关注这个细节
细节四
:虽然孙子进程会自动被系统回收,但是孙子进程
也是需要手动退出
,如果孙子进程没有主动退出,那么它将和父进程一样等待下次的连接
✨进程池版的守护进程
- 手写守护进程
将SIGCLD, SIGPIPE, SIGSTOP
忽略——这三个信号在发出后,系统需要进行处理,需要从用户态转变为内核态
,这个过程是很消耗资源
,为了防止这种消耗,就将这三个信号进行忽略,并且这中操作在守护进程中是很常见的
使用setsid来创建守护进程
并将标准输入,标准输出,标准错误重定向到/dev/null文件中
,/dev/null这个文件的内容会被自动丢掉
,就像垃圾桶一样
- 如果
线程池先创建
出来,再创建守护进程
,那么守护进程不会将父进程中的线程继承下来
,就导致了守护进程中虽然能将任务push进线程池,但是线程池中根本没有线程能够完成任务
所以根据他使用的时候创建的特点我们想到可以使用懒汉模式
——完美解决守护进程无法继承父进程的线程的问题
还要注意懒汉模式是有线程安全问题
,需要双层保障