多线程编程(十二)

一 线程分类

  内核线程:运行在内核空间,由内核调度;用户线程:运行在用户空间,有线程库来调度。
  线程的实现分三种模式:

  1. 完全用户空间实现:优点:创建和调度无需内核参与,速度快。不占用额外内核资源;缺点:对于多处理器系统,一个进程的多个线程无法运行在不同的CUP。
  2. 完全由内核调度:与1相反。
  3. 双层调度:兼具1、2优点;充分利用多处理器优势。

二 查看当前系统使用的线程库

$ getconf GNU_LIBPTHREAD_VERSION
  NPTL   2.14.90

三 多线程编程

1 常用函数

  1. pthread_create:获得线程自身的ID。
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);

  第一个参数为指向线程标识符的指针;第二个参数用来设置线程属性;第三个参数是线程运行函数的起始地址;最后一个参数是运行函数的参数。
  新创建的线程是从start_routine函数开始的,此函数只有一个无类型指针参数arg,如果需要向start_routine函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg参数传入。新创建的线程并不能保证新创建线程和调用线程哪个线程会先运行。

  1. pthread_kill:该函数可以用于向指定的线程发送信号。
int pthread_kill(pthread_t threadId,int signal);

  如果线程内不对信号进行处理,则调用默认的处理程式,如SIGQUIT会退出终止线程,SIGKILL会杀死线程等等,可以调用signal(SIGQUIT, sig_process_routine); 来自定义信号的处理程序。
  传递的pthread_kill的signal参数一般都是大于0的,这时系统默认或者自定义的都是有相应的处理程序。signal为0时,是一个被保留的信号,一般用这个保留的信号测试线程是否存在。
  pthread_kill 返回值如下:

0:调用成功。
ESRCH:线程不存在。
EINVAL:信号不合法。
  1. pthread_exit:主线程等待子线程的终止。
void pthread_exit(void * value_ptr);

  线程的终止可以是调用了pthread_exit或者该线程的例程结束。也就是说,一个线程可以隐式的退出,也可以显式的调用pthread_exit函数来退出。
  pthread_exit函数唯一的参数value_ptr是函数的返回代码,只要pthread_join中的第二个参数value_ptr不是NULL,这个值将被传递给value_ptr。

  1. pthread_self:获得线程自身的ID。
pthread_t pthread_self(void);
  1. pthread_join:是一个线程阻塞函数,主线程等待子线程的终止。
int pthread_join(pthread_t thread, void **retval);
  1. pthread_detach:主线程与子线程分离,两者相互不干涉,子线程结束同时子线程的资源自动回收。
      https://blog.csdn.net/qq_29621351/article/details/81948850

2 实例

  多线程程序,一个最简单的模拟售票系统:

#include <stdio.h>
#include <pthread.h>
 
void *ticketsell1(void *);
void *ticketsell2(void *);
int tickets = 20;
 
int main()
{
	pthread_t id1,id2;
	int error;
 
	error = pthread_create(&id1, NULL, ticketsell1, NULL);
	if(error != 0)
	{
		printf("pthread is not created!\n");
		return -1;
	}
 
	error = pthread_create(&id2, NULL, ticketsell2, NULL);
	if(error != 0)
	{
		printf("pthread is not created!\n");
		return -1;
	}
 
	pthread_join(id1,NULL);
	pthread_join(id2,NULL);
	
	return 0;
}
 
void *ticketsell1(void *arg)
{
	while(1)
	{
		if(tickets > 0)
		{
//			usleep(1000);
			printf("ticketse1 sells ticket:%d\n",tickets--);
		}
		else
		{
			break;
		}
	}
	return (void *)0;
}
 
void *ticketsell2(void *arg)
{
	while(1)
	{
		if(tickets > 0)
		{
//			usleep(1000);
			printf("ticketse2 sells ticket:%d\n",tickets--);
		}
		else
		{
			break;
		}
	}
 
	return (void *)0;
}

  执行结果如下:

fs@ubuntu:~/qiang/mthread$ ./mthread1 
ticketse2 sells ticket:20
ticketse2 sells ticket:19
ticketse2 sells ticket:18
ticketse2 sells ticket:17
ticketse2 sells ticket:16
ticketse2 sells ticket:15
ticketse2 sells ticket:14
ticketse2 sells ticket:13
ticketse2 sells ticket:12
ticketse2 sells ticket:11
ticketse2 sells ticket:10
ticketse2 sells ticket:9
ticketse2 sells ticket:8
ticketse2 sells ticket:7
ticketse2 sells ticket:6
ticketse2 sells ticket:4
ticketse2 sells ticket:3
ticketse2 sells ticket:2
ticketse2 sells ticket:1
ticketse1 sells ticket:5

  此时,其实存在一个隐含的问题,就是线程间的切换,在单CPU系统中,CPU是有时间片时间,时间片到了,就要执行其它的线程,假设thread1执行到if里面,但在printf执行前发生了线程切换,那么会发生什么呢?我们在这里用usleep函数(放开程序中的usleep注释行)进行强制模拟切换;

fs@ubuntu:~/qiang/mthread$ gcc -o mthread1 mthread1.c -lpthread
fs@ubuntu:~/qiang/mthread$ ./mthread1 
ticketse2 sells ticket:20
ticketse1 sells ticket:19
ticketse2 sells ticket:18
ticketse1 sells ticket:17
ticketse2 sells ticket:16
ticketse1 sells ticket:15
ticketse2 sells ticket:14
ticketse1 sells ticket:13
ticketse2 sells ticket:12
ticketse1 sells ticket:11
ticketse2 sells ticket:10
ticketse1 sells ticket:9
ticketse2 sells ticket:8
ticketse1 sells ticket:7
ticketse2 sells ticket:6
ticketse1 sells ticket:5
ticketse2 sells ticket:4
ticketse1 sells ticket:3
ticketse1 sells ticket:2
ticketse2 sells ticket:1
ticketse1 sells ticket:0

  运行程序发现竟然有0号票被卖出了,这显然是错误的!当thread1的if里面发生线程切换时,thread2得到运行,把最后一张票卖了,此时thread1恢复运行,结果卖出了0号票,这里我们需要的是火车票的票数数据对于所有线程而言是同步的,所以就要用到线程同步技术了。

四 线程安全

  线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
  确保现程安全:竞争与原子操作;同步与锁;可重入(一个函数被重入,表示这个函数没有执行完成,但由于外部因素或内部因素,又一次进入该函数执行。一个函数称为可重入的,表明该函数被重入之后不会产生任何不良后果。可重入是并发安全的强力保障,一个可重入的函数可以在多线程环境下放心使用。);过度优化(volatile;CPU的乱序执行让多线程安全保障的努力变得很困难,通常的解决办法是调用CPU提供的一条常被称作barrier的指令,它会阻止CPU将该指令之前的指令交换到barrier之后,反之亦然。)。

五 可重入函数

  可重入函数也可以这样理解,重入即表示重复进入,首先它意味着这个函数可以被中断,其次意味着它除了使用自己栈上的变量以外不依赖于任何环境(包括static),这样的函数就是purecode(纯代码)可重入,可以允许有多个该函数的副本在运行,由于它们使用的是分离的栈,所以不会互相干扰。如果确实需要访问全局变量(包括static),一定要注意实施互斥手段。可重入函数在并行运行环境中非常重要,但是一般要为访问全局变量付出一些性能代价。

六 多线程和多进程

1 多线程的优点

  • 无需跨进程边界。
  • 方便高效的内存共享 – 多进程下内存共享比较不便,且会抵消掉多进程编程的好处。
  • 较轻的上下文切换开销 – 不用切换地址空间,不用更改CR3寄存器,不用清空TLB。
  • 程序逻辑和控制方式简单。
  • 线程方式消耗的总资源比进程方式少。

2 多线程缺点:

  • 每个线程与主程序共用地址空间,受限于2GB地址空间。
  • 线程之间的同步和加锁控制比较麻烦。
  • 一个线程的崩溃可能影响到整个程序的稳定性。
  • 到达一定的线程数程度后,即使再增加CPU也无法提高性能,例如Windows Server 2003,大约是1500个左右的线程数就快到极限了(线程堆栈设定为1M),如果设定线程堆栈为2M,还达不到1500个线程总数。
  • 线程能够提高的总性能有限,而且线程多了之后,线程本身的调度也是一个麻烦事儿,需要消耗较多的CPU。

3 多进程的优点

  • 更强的容错性 – 一个进程crash不会导致整个系统崩溃。
  • 更好的多核可伸缩性 – 进程的使用将许多内核资源(如地址空间,页表,打开的文件)隔离,在多核系统上的可伸缩性强于多线程程序。
  • 每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系。
  • 通过增加CPU,就可以容易扩充性能。
  • 可以尽量减少线程加锁/解锁的影响,极大提高性能,就算是线程运行的模块算法效率低也没关系。
  • 每个子进程都有2GB地址空间和相关资源,总体能够达到的性能上限非常大。

4 多进程缺点

  • 逻辑控制复杂,需要和主程序交互。
  • 需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算。
  • 多进程调度开销比较大。
  • 最好是多进程和多线程结合,即根据实际的需要,每个CPU开启一个子进程,这个子进程开启多线程可以为若干同类型的数据进行处理。当然你也可以利用多线程+多CPU+轮询方式来解决问题。
发布了67 篇原创文章 · 获赞 26 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/essity/article/details/85292858