C++多线程系统编程精要(一)

本文摘自陈硕老师的linux多线程服务端编程

学习多线程最大的思想转变有两点:
(1)当前线程可能随时被切换出去,或者被抢占了。
(2)多线程程序中事件的发生顺序不再有全局统一的先后关系。
线程被切换回来的时候,全局变量可能已经变化。多线程程序运行的正确性不能依赖于任何一个线程的执行速度,不能通过原地等待来假定其他线程的事件已经发生,必须 通过适当的同步 来让当前线程能看到其他线程事件的结果。

pthread_t ntid;
bool running = false;

void threadFunc()
{
	while(running)
	{
		//运行的任务
	}
}

void start()
{
		if((pthread_create(&ntid,NULL,threadFun,NULL)) != 0 )
		{
			perror("can't create thread");
			exit(-1);
		}
		running = true;
}
		

由于不能保证是调用线程还是新建线程先运行,新建线程的运行依赖于running 变量的状态,因此很可能新建线程不进入while循环而直接退出了。

1. 基本线程原语

  • 常用的pthreads函数
    2个线程的创建与销毁;4个互斥锁的创建、销毁、加锁和解锁;5个条件变量的创建、销毁、等待、通知和广播。
    用thread、mutex和condition可以完成任何多线程编程任务。

2. Linux上的线程标识

POSIX threads 库提供了pthread_self 函数用于返回当前线程的标识符,其类型为pthread_t,不一定是一个数值类型,也有可能是一个结构体。可以用othread_equal 函数来比较两个线程标识符是否相等。但是存在一下问题:

  • 无法打印输出 pthread_t, 因为不知道其确切的类型,无法再日志中用他表示当前线程的ID。

  • 无法比较pthread_t 的大小或者计算其hash值,无法作为关联容器的key。

  • 无法定义一个非法的pthread_t 值,用来表示绝对不可能的线程ID,因此互斥锁没有办法有效判断当前线程是否已经持有本锁。

  • pthread_t 值只在进程内有意义,与操作系统的任务调度之间无法建立有效关联。

Pthreads 只保证同一进程之内,同一时刻的各个线程的ID不同,不能保证同一进程先后多个线程具有不同的ID,更不要说一台机器不同进程之间了。因为,glibc 的 Pthreads 实现实际上将pthread_t 用作一个结构体指针,指向一块动态分配的内存,而且这块内存是反复使用的,导致 pthread_t 的值很容易重复。

因此,pthread_t 不适合用作程序中对线程的标识符。建议使用 gettid() 系统调用,函数返回值类型为 pid_t , 其值是一个小的整数,便于再日志中输出;其次,在Linux中,它直接表示任务调度的ID;任何时候都是全局唯一的,Linux 分配新的pid采用递增轮回的方式,短时间内多个线程会具有不同的ID。

采用 __thread 变量 缓存gettid() 的返回值,这样可以在第一次调用的时候才进行系统调用,以后可以直接从thread local 中获取线程ID。

善于使用__thread 关键字

  • 使用规则
    只能用于修饰POD(plain old data struct),不能修饰class类型,因为无法调用构造函数和析构函数。可以用于修饰全局变量’函数内的静态变量,但是不能修饰函数的局部变量或者class 的普通成员变量。
    其变量的初始化只能用编译期常量。
  • 使用方法
__thread string obj("YoungSusie");

每个__thread变量是每个线程有一份独立实体,各个线程的变量值互不干扰。
还可以修饰“值可能会变,带有全局性,但是不值得用全局锁”的变量。

进程ID和线程ID

The four threads will have the same PID but only when viewed from above. What you (as a user) call a PID is not what the kernel (looking from below) calls a PID.

In the kernel, each thread has it’s own ID, called a PID (although it would possibly make more sense to call this a TID, or thread ID) and they also have a TGID (thread group ID) which is the PID of the thread that started the whole process.

Simplistically, when a new process is created, it appears as a thread where both the PID and TGID are the same (new) number.
When a thread starts another thread, that started thread gets its own PID (so the scheduler can schedule it independently) but it inherits the TGID from the original thread.
That way, the kernel can happily schedule threads independent of what process they belong to, while processes (thread group IDs) are reported to you.

在这里插入图片描述

2. 线程的创建与销毁

线程的创建与销毁是编写过线程程序的基本要素。

  • 线程的创建需要遵循的原则
    程序库不应该在未提前告知的情况下自己创建自己的背景线程。库不应该偷偷创建线程。
    尽量用相同的方式创建线程。
    在进入主函数前不应该启动线程。
    程序中的线程的创建最好能在初始化阶段全部完成。

线程是稀缺资源,应该合理规划,并为关键任务保留足够的计算资源。
在main函数之前不应该启动线程。最好在程序的初始化阶段创建全部工作线程,在程序运行期间不再创建或者销毁线程。

  • 线程的销毁方式
  • 自然死亡,从线程主函数返回,线程正常退出。
  • 非正常死亡,从线程主函数抛出异常等。
  • 自杀,在线程中调用pthread_exit()来立刻退出线程。
  • 他杀,其他线程调用pthread_cancle()来强制终止某个线程。

强行终止线程(自杀或者他杀)会导致线程没有机会清理资源。

pthread_cancle() 和 pthread_kill() 的区别?

int pthread_kill(pthread_t thread, int sig);
功能是向指定线程发送信号,信号为0时用于检查此线程ID的线程是否存活。

pthread_cancel(pthread_t thread);
功能是给线程发送取消信号,使线程从取消点退出。

3. exit()在C++ 中不是线程安全的

exit() 除了终止程序,还会析构全局对象和已经构造完的函数静态对象。

void funCallExit()
{
	exit(1);
}

class Object{
	public:
			void doit()
			{
				MutexLockGuard lock(mutex_);
				funCallExit();
			}
			
			~Object()
			{
				printf("Object: ~Object\n");
				MutexLockGuard lock(mutex_);
				printf("Object: ~Object cleaning\n");
			}
	private:
			MutexLockGuard mutex_;
		};
Object g_obj;

int main()
{
	g_obj.doit();
}
	

doit() 函数辗转调用了exit() ,从而触发了全局对象g_obj 的析构。析构函数会试图对 mutex_ 加锁,而此时mutex_ 已经被doit() 锁住了,导致死锁。

C++标准没有照顾全局对象在多线程环境下的析构,如果确实需要主动结束进程,可以 使用_exit() 函数 ,它不会试图析构全局对象,也不会执行任何清理工作(比如flush标准输出)。

总结

安全地退出一个多线程程序并不是简单的事情,例如,如何安全地退出其他正在运行的线程,需要精心设计共享对象的析构顺序,防止各个线程在退出时访问已经失效的对象。
编写长期运行的多线程服务程序,可以不必追求安全退出,而是让进程进入拒绝服务的状态,然后就可以直接杀掉。

分类 网络编程 学习笔记

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值