linux之线程

文章目录

  • 线程
    • 1. 为什么使用线程
    • 2. 什么是线程
    • 3. 线程的优点、缺点
    • 4. 线程的应用场合
    • 5. 线程的使用API
    • 6. 线程同步
      • 信号量
      • 互斥量
      • 条件变量

线程

1. 为什么使用线程

  • 使用fork创建进程以执行新的任务,该方式的代价很高(会复制父进程的资源)。
  • 多个进程间不会直接共享内存
  • 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行,进程要想执行任务,必须得有线程,进程至少要有一条线程,程序启动会默认开启一条线程,这条线程被称为主线程或 UI 线程

2. 什么是线程

类比:

  • 创建一个进程,类似于“克隆”一个家庭。该“家庭”与原来的家庭完全相同但是新“家庭”和原来的家庭完全独立。

  • 进程包含一个或多个线程。 类似与一个家庭,包含一个或多个家庭成员。家庭内的各成员同时做各自的事情(父亲工作、母亲持家、小孩上学)而对于家庭外部的人来说,这个家庭同时在做多件事情。

  • 家庭内的每个成员,就是一个线程。各个家庭成员有自己的个人资源(线程有自己的局部变量)但是所有家庭成员都能共享这个家庭的资源:房子、汽车、家庭的公共资金。(同一个进程内的各个线程,能够共享整个进程的全局变量,除了线程的局部变量外,其他资源都共享)

3. 线程的优点、缺点

优点: 创建线程比创建进程,开销要小。
缺点: 1)多线程编程,需特别小心,很容易发生错误。
      2)多线程调试很困难。
      3)把一个任务划分为两部分, 用两个线程在单处理器上运行时,不一定更快。
      		除非能确定这两个部分能同时执行、且运行在多处理器上。

4. 线程的应用场合

 1. 当一个应用程序,需要同时处理输入、计算、输出时,
 可开3个线程,分别处理输入、计算、输出。
 让用户感觉不到等待。
 2. 高并发编程。

5. 线程的使用API

1)线程的创建

   原型:int  pthread_create (pthread_t *thread,pthread_attr_t *attr,
                     void *(*start_routine)(void*),  void *arg);                                              
                     
   参数:thread, 指向新线程的标识符。是一个传出参数
           attr, 用来设置新线程的属性。
                  一般取默认属性,即该参数取NULL
           start_routine, 该线程的处理函数
                                该函数的返回类型和参数类型都是void*
           arg, 线程处理函数start_routine的参数
           
   功能:创建一个新线程,
           同时指定该线程的属性、执行函数、执行函数的参数
           通过参数1返回该线程的标识符。
   
   返回值:成功,返回0
              失败,返回错误代码                  
              注意:大部分pthread_开头的函数成功时返回0,失败时返回错误码(而不是-1)
              
   注意:使用fork创建进程后,进程马上就启动,但是是和父进程同时执行fork后的代码。
         使用pthread_create创建线程后,新线程马上就启动,即执行对应的线程处理函数。

2)线程的终止

   原型:void  pthread_exit (void *retval)
   功能:在线程函数内部调用该函数。
            终止该线程,并通过参数retval返回一个指针。
            该指针不能指向该线程的局部变量。   

3)等待指定线程结束

   pthread_join
   功能:类似与进程中的waitpid
            等待指定的线程结束,并使参数指向该线程函数的返回值(用pthread_exit返回的值)
   原型:int  pthread_join  (pthread_t   th,
                             void ** thread_return);
   参数:th,  指定等待的线程
           thread_return, 指向该线程函数的返回值
                                  线程函数的返回值类型为void*,故该参数的类型为void**

4)使用线程程序的编译

  • (1) 编译时,定义宏_REENTRANT
    即: gcc -D_REENTRANT (#define REENTRANT)
    • 功能:告诉编译器,编译时需要可重入功能。即使得,在编译时,编译部分函数的可重入版本。

注:在单线程程序中,整个程序都是顺序执行的,一个函数在同一时刻只能被一个函数调用,但在多线程中,由于并发性,一个函数可能同时被多个函数调用,此时这个函数就成了临界资源,很容易造成调用函数处理结果的相互影响,如果一个函数在多线程并发的环境中每次被调用产生的结果是不确定的,我们就说这个函数是"不可重入的"/"线程不安全"的。

  • (2) 编译时,指定线程库
    • 即: gcc -lpthread
    • 总结:一般使用如下形式即可
gcc   myThreadCode.c  -o  myThreadCode  -D_REENTRANT   -lpthread    

实例 main1.c

这里只是对线程创建,销毁的一个基本运用

#include 
#include 
#include 

int my_global;

void* my_thread_handle(void *arg) 
{
	int val;

	val = *((int*)arg);

	printf("new thread begin, arg=%d\n", val);
	my_global += val;

	sleep(3);

	pthread_exit(&my_global);// 结束线程

	//  不再执行
	printf("new thread end\n");
}

int main(void)
{
	pthread_t  mythread;// 传出参数
	int arg;
	int ret;
	void *thread_return;// 线程返回值

	arg = 100;
	my_global = 1000;

	printf("my_global=%d\n", my_global);
	printf("ready create thread...\n");
	
    // 创建线程   
	ret = pthread_create(&mythread, 0, my_thread_handle, &arg);
	if (ret != 0) {
		printf("create thread failed!\n");
		exit(1);
	}

	printf("wait thread finished...\n");

   // 等待 线程结束  并且获取返回值
	ret = pthread_join(mythread, &thread_return);
	if (ret != 0) {
		printf("pthread_join failed!\n");
		exit(1);
	}
	printf("wait thread end, return value is %d\n", *((int*)thread_return));
	printf("my_global=%d\n", my_global);

	printf("create thread finished!\n");
}

linux之线程_第1张图片

6. 线程同步

  • 线程同步其实实现的是线程排队。
  • 防止线程同步访问共享资源造成冲突。
  • 多个线程访问共享资源的代码有可能是同一份代码,也有可能是不同的代码;无论是否执行同一份代码,只要这些线程的代码访问同一份可变的共享资源,这些线程之间就需要同步。

1. 问题

  • 同一个进程内的各个线程,共享该进程内的全局变量
  • 如果多个线程同时对某个全局变量进行访问时,有可能达不到预期效果。

2. 信号量和互斥量的选择。

  • 互斥量:为协调共同对一个共享资源的单独访问而设计的;因为进入内核模式,所以性能比临界区差;跨进程,可用于防止程序重复打开运行。
  • 信号量:为控制一个具有有限数量用户资源而设计,互斥锁可以理解为1个用户资源的信号量。
    • 使用时,选择更符合语义的手段:
      • 如果要求最多只允许一个线程进入临界区,则使用互斥量
      • 如果要求多个线程之间的执行顺序满足某个约束,则使用信号量

条件变量:条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。

信号量

1)什么是信号量

  • 此时所指的“信号量”是指用于同一个进程内多个线程之间的信号量。即POSIX信号量,而不是System V信号量(用于进程之间的同步)

  • 用于线程的信号量的原理,与用于进程之间的信号量的原理相同。都有P操作、V操作。

  • 信号量的表示:sem_t 类型

    2) 信号量的初始化

      原型:int  sem_init  (sem_t  *sem,int  pshared,  unsigned int value);
      功能:对信号量进行初始化
      参数:sem,  指向被初始化的信号量
           pshared,  0:表示该信号量是该进程内使用的“局部信号量”, 不再被其它进程共享。
           		   非0:该信号量可被其他进程共享,Linux不支持这种信号量
           		   
           value,  信号量的初值。>= 0
      返回值:成功,返回0   失败, 返回错误码
    

    3) 信号量的P操作

      原型:int   sem_wait (sem_t  *sem);
      返回值:成功,返回0  失败, 返回错误码
    

    4) 信号量的V操作

      原型:int sem_post (sem_t  *sem);
      返回值:成功,返回0  失败, 返回错误码
    

    5) 信号量的删除

     原型:int sem_destroy (sem_t  *sem);
     返回值:成功,返回0   失败, 返回错误码
    

    6) 实例
    主线程循环输入字符串,把字符串存放到一个全局缓存中。新线程从全局缓存中读取字符串,统计该字符串的长度。直到用户输入end

main.c

预期结果:主线程每接收终端输入的一个字符串,子线程就打印字符串和输出字符串长度

  • 信号量被初始化为0,主线程接受一个字符串的时候,执行V操作(信号量+1),此时信号量大于1,子线程执行P操作(-1),然后对字符串做出相应的操作
#include 
#include 
#include 
#include 
#include 

#define BUFF_SIZE 80

// 全局变量可以让多个线程访问
char buff[BUFF_SIZE];
sem_t sem;

static void* str_thread_handle(void *arg) 
{
	while(1) {
		//P(sem) -1
		if (sem_wait(&sem) != 0) {
			printf("sem_wait failed!\n");
			exit(1);
		}
		
		printf("string is: %slen=%u\n", buff, (unsigned int)strlen(buff));
		if (strncmp(buff, "end", 3) == 0) {
			break;
		}
	}
}

int main(void)
{
	int ret;
	pthread_t  str_thread;
	void *thread_return;

	// 参数:被初始化信号量 0 给信号量的赋值
	ret = sem_init(&sem, 0, 0);
	if (ret != 0) {
		printf("sem_init failed!\n");
		exit(1);
	}

	// 创建线程
	ret = pthread_create(&str_thread, 0, str_thread_handle, 0);
	if (ret != 0) {
		printf("pthread_create failed!\n");
		exit(1);
	}

	while (1) {
	    // 从终端获取一行输入
		fgets(buff, sizeof(buff), stdin);

		//V(sem) +1
		// 0->1 那么线程就可以执行P操作 1->0
		if (sem_post(&sem) != 0) {
			printf("sem_post failed!\n");
			exit(1);
		}
		
		if (strncmp(buff, "end", 3) == 0) {
			break;
		}
	}

	ret = pthread_join(str_thread, &thread_return);
	if (ret != 0) {
		printf("pthread_join failed!\n");
		exit(1);
	}

	ret = sem_destroy(&sem);
	if (ret != 0) {
		printf("sem_destroy failed!\n");
		exit(1);
	}

	return 0;
}

linux之线程_第2张图片
练习

    创建2个线程(共有主线程、线程1、线程2共3个线程)
    主线程阻塞式等待用户输入字符串
    主线程每接收到一个字符串之后, 线程1就马上对该字符串进行处理。
    线程1的处理逻辑为:统计该字符串的个数,并记录当时的时间。
    线程1把该字符串处理完后,线程2马上就把处理结果写入文件result.txt
    直到用户输入exit.
    multi_pthread.c

互斥量

1)什么是互斥量
     效果上等同于初值为1的信号量
     互斥量的使用:类型为 pthread_mutex_t
     
2)互斥量的初始化
     原型:int  pthread_mutex_init(pthread_mutex_t *mutex,
                                   pthread_mutexattr_t *attr);
     参数:mutex, 指向被初始化的互斥量
             attr,  指向互斥量的属性
                    一般取默认属性(当一个线程已获取互斥量后,该线程再次获取该信号量,将导致死锁!)

3) 互斥量的获取
    原型:int  pthread_mutex_lock (pthread_mutex_t *mutex);   

4)互斥量的释放
     原型:int  pthread_mutex_unlock (pthread_mutex_t  *mutex);         
     
5)互斥量的删除
     int  pthread_mutex_destroy (pthread_mutex_t *mutex);  
  1. 实例
    main3.c

预期实现效果:主线程和子线程依次把全局变量 -1 ,依次往终端输出结果

  • 当没有用互斥量的时候,同时运行主线程和子线程,对全局变量进行 - 1
#include 
#include 
#include 
#include 
#include 
#include 

#define BUFF_SIZE 80

// 全局
int global_value = 1000;
pthread_mutex_t  lock;

static void* str_thread_handle(void *arg) 
{
	int i = 0;

	for (i=0; i<10; i++) {
		//pthread_mutex_lock(&lock);

		if (global_value  > 0) {
			// work
			sleep(1);
			printf("soled ticket(%d) to ChildStation(%d)\n",
				global_value, i+1);
		}
		global_value--;
		
		//pthread_mutex_unlock(&lock);
		sleep(1);
	}
}

int main(void)
{
	int ret;
	pthread_t  str_thread;
	void *thread_return;
	int i;

	

	ret = pthread_mutex_init(&lock, 0);
	if (ret != 0) {
		printf("pthread_mutex_init failed!\n");
		exit(1);
	}

	ret = pthread_create(&str_thread, 0, str_thread_handle, 0);
	if (ret != 0) {
		printf("pthread_create failed!\n");
		exit(1);
	}

	for (i=0; i<10; i++) {
		//pthread_mutex_lock(&lock);
		
		if (global_value  > 0) {
			// work
			sleep(1);
			printf("soled ticket(%d) to MainStation(%d)\n",
				global_value, i+1);
		}
		global_value--;
		
		
		//pthread_mutex_unlock(&lock);
		sleep(1);
	}

	ret = pthread_join(str_thread, &thread_return);
	if (ret != 0) {
		printf("pthread_join failed!\n");
		exit(1);
	}

	ret = pthread_mutex_destroy(&lock);
	if (ret != 0) {
		printf("pthread_mutex_destroy failed!\n");
		exit(1);
	}

	return 0;
}
  • 主线程 -10 , 子线程 -10 最后应该是 981 所以不符合预期
  • 原因 主线程和子线程同时对全局变量进行了 -1
    linux之线程_第3张图片
    使用信号量后
  • 上了两把锁
  • 给主线程和子线程对全局变量进行操作的部分分别上锁
#include 
#include 
#include 
#include 
#include 
#include 

#define BUFF_SIZE 80

// 全局
int global_value = 1000;
pthread_mutex_t  lock;

static void* str_thread_handle(void *arg) 
{
	int i = 0;

	for (i=0; i<10; i++) {

        /*****上锁*****/
		pthread_mutex_lock(&lock);

		if (global_value  > 0) {
			// work
			sleep(1);
			printf("soled ticket(%d) to ChildStation(%d)\n",
				global_value, i+1);
		}
		global_value--;
		
        /*****开锁*****/
		pthread_mutex_unlock(&lock);
		sleep(1);
	}
}

int main(void)
{
	int ret;
	pthread_t  str_thread;
	void *thread_return;
	int i;

	

	ret = pthread_mutex_init(&lock, 0);
	if (ret != 0) {
		printf("pthread_mutex_init failed!\n");
		exit(1);
	}

	ret = pthread_create(&str_thread, 0, str_thread_handle, 0);
	if (ret != 0) {
		printf("pthread_create failed!\n");
		exit(1);
	}

	for (i=0; i<10; i++) {

        /*****上锁*****/
		pthread_mutex_lock(&lock);
		
		if (global_value  > 0) {
			// work
			sleep(1);
			printf("soled ticket(%d) to MainStation(%d)\n",
				global_value, i+1);
		}
		global_value--;
		
		/*****开锁*****/
		pthread_mutex_unlock(&lock);
		sleep(1);
	}

	ret = pthread_join(str_thread, &thread_return);
	if (ret != 0) {
		printf("pthread_join failed!\n");
		exit(1);
	}

	ret = pthread_mutex_destroy(&lock);
	if (ret != 0) {
		printf("pthread_mutex_destroy failed!\n");
		exit(1);
	}

	return 0;
}
  • 结果分析:相比于上次,每一次输出全局变量就 -1 最后结果为 981
    linux之线程_第4张图片

条件变量

1.什么是线程条件变量

与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。

条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。

2. 条件变量初始化

  原型:int pthread_cond_init (pthread_cond_t *cond, const pthread_condattr_t *attr);
    参数:cond: 条件变量指针
         attr:条件变量高级属性

3. 唤醒一个等待线程

原型: int pthread_cond_signal (pthread_cond_t *cond);
参数:cond:条件变量指针

4.唤醒所有等待该条件变量的线程

原型: int pthread_cond_broadcast (pthread_cond_t *cond);
参数:cond,  条件变量指针

5.等待条件变量/超时被唤醒

原型: int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t *mutex, 
									const struct timespec *abstime);
参数:cond,  条件变量指针
      pthread_mutex_t *mutex 互斥量
      const struct timespec *abstime 等待被唤醒的绝对超时时间

6.等待条件变量被唤醒(一般使用这个)

原型: int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mutex);
参数:cond,  条件变量指针
      pthread_mutex_t *mutex 互斥量

常见错误码: [EINVAL] cond或mutex无效,
[EINVAL] 同时等待不同的互斥量
[EINVAL] 主调线程没有占有互斥量

7. 释放/销毁条件变量

pthread_cond_destroy  待销毁的条件变量 
原型: int pthread_cond_destroy (pthread_cond_t *cond);
参数:cond,  条件变量指针

main.c

效果:触发信号,子线程向终端打印数据

#include 
#include 
#include 

// 注意这里一定要定义为全局变量
pthread_mutex_t mutex;
pthread_cond_t cond;

void *thread1(void *arg)
{

	while (1) {

		printf("thread1 is running\n");
		
		// 加锁
		pthread_mutex_lock(&mutex);
		printf("thread1 lock..\n");

		// 解锁-阻塞等待信号-信号来了-加锁-执行任务
		pthread_cond_wait(&cond, &mutex);


		// 执行任务
		printf("thread1 applied the condition\n");


		// 解锁
		printf("thread1 unlock..\n");
		pthread_mutex_unlock(&mutex);

		sleep(4);

	}
}


void *thread2(void *arg)
{

	while (1) {

		printf("thread2 is running\n");

		pthread_mutex_lock(&mutex);
		printf("thread2 lock..\n");

		pthread_cond_wait(&cond, &mutex);

		printf("thread2 applied the condition\n");

		printf("thread2 unlock..\n");
		pthread_mutex_unlock(&mutex);

		sleep(2);

	}

}

int main()
{

	pthread_t thid1, thid2;

	printf("condition variable study!\n");

	// 初始化互斥锁  效果上等同于初值为1的信号量
	// 初始化条件变量
	pthread_mutex_init(&mutex, NULL);
	pthread_cond_init(&cond, NULL);

	// 创建两个线程
	pthread_create(&thid1, NULL, (void *)thread1, NULL);
	pthread_create(&thid2, NULL, (void *)thread2, NULL);

	// 不断发送信号,唤醒一个线程
	do {
		sleep(10);
		pthread_cond_signal(&cond);

	} while (1);


	return 0;

}

结果分析:为什么两个线程可以同时上锁?
因为 pthread_cond_wait(&cond, &mutex); 执行过程中有一个解锁的过程,所以是解锁后,另一个线程拿到锁。但是如果此线程被信号触发,那么也会立即上锁,执行任务。

linux之线程_第5张图片

你可能感兴趣的:(linux,linux,c++)