线程

发布时间 2023-08-30 19:28:17作者: 若达萨罗

线程

  1. 进程是资源管理的最小单位
  2. 线程是系统调度的最小单位

假设Linux是一个工厂,进程就是一个车间,线程就是车间里面的流水线(线程运行互不干扰,车间资源是共享的)

线程函数的接口特点?

  1. 由于线程函数接口都是封装在一个线程库中,所以我们看不见源码。但我们可以用man 3 xxx命令查看
  2. 所有的线程函数头文件都在include <pthread.h>

系统调度和cpu调度的区别

系统调度是指操作系统决定哪一个进程可以使用cpu,当一个进程需要cpu,操作系统会将该进程加入到就绪列中,然后从就绪列中选择一个进程来使用。系统调度的目的,尽可能提高系统的吞吐量和响应时间,同时保证公平性和稳定性

cpu调度是指操作系统中决定在一个进程中哪一个线程可以使用cpu,在多线程应用程序中,一个进程包括多个线程,每个线程都可以独立运行,cpu调度的目的,尽可能的提高线程的响应时间和吞吐量同时保证公平性和稳定性

线程的创建和回收

  1. 如何创建一个子线程?--------->pthread_create()
#include <pthread.h>

       int pthread_create(
	   pthread_t *thread,                     // 参数1:  存储线程ID的变量的地址
	   const pthread_attr_t *attr,           // 参数2:attribute属性,普通属性填NULL
	   void *(*start_routine) (void *),     // 参数3:线程执行的函数  void* fun(void*)
	   void *arg                           // 参数4:线程执行的函数的参数
	);
	                                     // 返回值:成功返回0,失败返回错误码

       Compile and link with -pthread.     // 编译和链接需要--pthread库

实操:创建一个线程,同时做两件事

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void *func(void* arg)
{
	while(1)
	{
		printf("child-----------学到了学到了-----------\n");
		sleep(1);
	}
}

int main(int argc, char const *argv[])
{
	pthread_t pid;
	pthread_create(&pid, NULL, func, NULL);
	while(1)
	{
		printf("main-我也学到了学到了\n");
		sleep(1);
	}
	
	return 0;
}

终端显示:
image

情况一:主线程的运行时间 > 子线程的运行时间
主线程会与子线程一起运行,子线程结束运行,主线程继续工作,然后就执行return 0,进程退出

情况二:主线程的运行时间 < 子线程的运行时间
主线程工作完毕,执行return 0导致进程退出,主进程退出就会导致所有的进程都退出

练习1;验证一个进程可以同时运行多少条线程?

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <errno.h>
#include <string.h>

void *thread(void* arg) 
{
    while(1)
    {
    	sleep(1);
    }
}

int main()
{
	int err = 0, count = 0;
    
	pthread_t tid;
	while(err == 0)
	{
		err = pthread_create(&tid, NULL, thread, NULL);
		++count;
		printf("count = %d\n", count);
	}
	printf("create pthread error: %s\n", strerror(errno));
	printf("max create pthread num: %d\n", count);

	getchar();

    return 0;
}

终端显示:
image
32 这个系统,用户态的虚拟空间只有 3G,如果创建线程时分配的栈空间是 10M,那么一个进程最多只能创建 300 个左右的线程。
64 系统,用户态的虚拟空间大到有 128T,理论上不会受虚拟内存大小的限制,而会受系统的参数或性能限制。

练习2:验证一个进程中,线程结束后,不回收线程资源,最多能开多少条?

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <errno.h>
#include <string.h>

void *thread(void* arg) 
{
	printf("%d\n", *(int*)arg);
}

int main(int argc, char const *argv[])
{
	int err = 0, count = 0;
	int arg = 10;
    
	pthread_t tid;
	while(err == 0)
	{
		err = pthread_create(&tid, NULL, thread, &arg);
		++count;
		printf("count = %d\n", count);
	}
	printf("create pthread error: %s\n", strerror(errno));
	printf("max create pthread num: %d\n", count);

	getchar();

	return 0;
}

终端显示:
image

如何接合一条线程?(阻塞等待子线程的退出) ------>pthread_join()

#include <pthread.h>

       int pthread_join(
	   pthread_t thread,       // 参数1:需要接合的线程号 
	   void **retval          // 参数2:存储子线程的退出值指针,如果填NULL,代表不关心子线程的退出状态
	);                       // 返回值:成功0,失败非0

       Compile and link with -pthread.

  • 验证主线程和子线程工作时间的长短问题。结果是怎样?
  • 情况1:主线程的工作时间长,去接合--------> pthread_join()会立马返回
  • 情况2:主线程的工作时间短,去接合--------> pthread_join()会阻塞等待子线程的退出
    验证:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>


void *thread(void* arg) 
{
	sleep(5);
	printf("child pthread\n");
}

int main(int argc, char const *argv[])
{
    pthread_t tid;

 	printf("main\n");
	pthread_create(&tid, NULL, thread, NULL);

 	pthread_join(tid, NULL);
 	printf("end\n");


	return 0;
}

终端显示:
image
打印man之后会等待5s再打印后续内容

如何退出线程---------->pthread_exit()

#include <pthread.h>

       void pthread_exit(void *retval);     // 参数:子线程退出值变量的地址--->这个退出值必须是全局变量

       Compile and link with -pthread.

实操:

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
int pthread_status = 10;

void *thread(void* arg) 
{
	int cnt = 1;
	while(1)
	{
		sleep(1);
		printf("child pthread\n");
		if(4 == cnt++)
		{
			break;
		}
	}
	pthread_exit(&pthread_status);
}

int main(int argc, char const *argv[])
{
	pthread_t tid;

 	printf("main\n");
	pthread_create(&tid, NULL, thread, NULL);

	void* p = NULL;
 	pthread_join(tid, &p);
 	printf("thread: %ld retval: %d pthread_status: %d\n", tid, *(int*)p, pthread_status);

 	printf("end\n");


	return 0;
}

终端显示:
image

导致线程退出的原因有哪些?

  1. 当线程调用pthread_exit()
  2. 例程函数返回时
  3. 收到取消请求pthread_cancel()
  4. 任意一个线程调用exit(),main函数结束

线程的属性--分离属性

  • 什么是分离属性?
    首先分离属性是线程的一个属性,有了分离属性的线程,不需要别的线程去接合它(自己回收资源),但虽说是分离的,但是线程结束了,该线程还是会退出。
    设置了分离属性------>不需要pthread_join()
    设置了非分离属性------>需要pthread_join()------->默认创建的线程是非分离属性线程

  • 如何创建分离属性的线程? ------>pthread_attr_setdetachstate()
    方法1:添加一个分离属性到一个属性变量中,然后使用属性变量去创建一个线程,那么创建出来的线程就具有分离属性的线程

  1. 定义一个属性变量--->数据类型pthread_attr_t attr
    pthread_attr_t attr;
  2. 去初始化属性变量。---->pthread_attr_init()
#include <pthread.h>

       int pthread_attr_init(pthread_attr_t *attr);     // 参数1:未初始化的属性变量
	                                                   // 返回值:成功返回0,失败非0错误码
	   
       Compile and link with -pthread.

  1. 设置分离属性到属性变量中
#include <pthread.h>

       int pthread_attr_setdetachstate(
		pthread_attr_t *attr,            // 参数1:已经初始化的属性变量
		int detachstate                 // 参数2:------->PTHREAD_CREATE_DETACHED   分离属性
		                                //       -------->PTHREAD_CREATE_JOINABLE   非分离属性

		);
		int pthread_attr_getdetachstate(
		const pthread_attr_t *attr,
		int *detachstate
		);

       Compile and link with -pthread.
  1. 使用属性变量去创建一个新的线程
    ··

pthread_create(&thread,&attr,start_routine,NULL);

  1. 销毁属性变量
int pthread_attr_destroy(pthread_attr_t *attr);
                                  //参数:attr:已经初始化的属性变量
                                  //返回值:成功返回0
                                 //失败非0错误码

实操:

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <string.h>
#include <sys/msg.h>
#include <stdlib.h>

void *func(void *arg)
{
	for (int i = 0; i < 5; ++i)
	{
		sleep(1);
		printf("child thread\n");
	}
}

int main(int argc, char const *argv[])
{
	pthread_t thread;
	// 创建一条分离属性的线程
	// 1.定义一个属性变量
	pthread_attr_t attr;
	// 2.初始化属性变量
	pthread_attr_init(&attr);
	// 3.设置分离属性到属性变量中
	pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
	// 4.使用属性变量去创建一个新的线程
	pthread_create(&thread, &attr, func, NULL);
	// 5. 销毁属性变量
	pthread_attr_destroy(&attr);

	for (int i = 0; i < 3; ++i)
	{
		sleep(1);
		printf("main thread\n");
	}

	int ret = pthread_join(thread, NULL);
	if(0 != ret)
	{
		perror("join fail");
		return -1;
	}


	return 0;
}

分析:由于线程添加了分离属性,主线程的运行时间比子线程短,这个时候使用主线程调用pthread_join函数阻塞等待回收子线程的资源是没用的,主线程执行完毕就进程结束了

思考:一个分离属性的线程退出,主线程还可以接合它吗? 不可以
一个分离属性的线程,在进程退出时,该分离属性的线程还会不会继续运行? 不会

方法2:先创建一个普通线程,然后在线程中调用一个设置为分离属性的函数,那么该线程就变成了有分离属性的线程

  1. 设置线程本身的分离属性------->pthread_detach()
#include <pthread.h>

       int pthread_detach(pthread_t thread);

       Compile and link with -pthread.

  1. 获取线程的ID号------->pthread_self()
#include <pthread.h>

       pthread_t pthread_self(void);

       Compile and link with -pthread.

实操:

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <string.h>
#include <sys/msg.h>
#include <stdlib.h>

void *func(void *arg)
{
	pthread_detach(pthread_self());
	for (int i = 0; i < 5; ++i)
	{
		sleep(1);
		printf("child pthread\n");
	}
}


int main(int argc, char const *argv[])
{
	pthread_t thread;

	pthread_create(&thread, NULL, func, NULL);

	for (int i = 0; i < 3; ++i)
	{
		sleep(1);
		printf("main pthread\n");
	}

	int ret = pthread_join(thread, NULL);
	if(0 != ret)
	{
		perror("join fail");
		return -1;
	}


	return 0;
}

分析:线程创建后,是并发执行的,无论子线程先打印还是主线程先打印,主线程最后一次打印完成进程就结束,也不会阻塞等待子线程

练习:验证设置了分离属性的线程,在进程中最多能创建多少条? 无限制

线程的取消(杀死一个子线程)

  1. 一般主线程不用于处理任务,只是控制子线程状态,例如取消,接合---->pthread_cancel()主线程--->取消请求--->子线程
#include <pthread.h>

       int pthread_cancel(pthread_t thread);

       Compile and link with -pthread.

注意:线程收到取消请求,就是等价于提前退出
pthread_exit()--->线程主动退出--->pthread_join()
pthread_cancel()--->主线程给子线程发送取消请求

实操:

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <string.h>
#include <sys/msg.h>
#include <stdlib.h>
#include <errno.h>

void *func(void* arg)
{
	int cnt = 10;
	while(cnt--)
	{
		sleep(1);
		printf("child pthread\n");
	}
}


int main(int argc, char const *argv[])
{
	pthread_t thread;

	pthread_create(&thread, NULL, func, NULL);
	sleep(5);

	// 主线程给子线程发送取消请求
	pthread_cancel(thread);

	// 等待子线程退出
	int ret = pthread_join(thread, NULL);
	if(0 != ret)
	{
		printf("error\n");
	}
	
	return 0;
}

终端显示:
image

  1. 设置线程取消的状态----->pthread_
#include <pthread.h>

       int pthread_setcancelstate(
	   int state,                 // 参数1:PTHREAD_CANCEL_ENABLE  能响应(线程默认的属性)
	                             //        PTHREAD_CANCEL_DISABLE  不能响应

	   int *oldstate             // 参数2: 保留之前的状态,如果不关心,填NULL
	                            // 返回值: 成功0,失败非0错误码
	);
	   // 如果一个线程不能响应取消,那么在这个过程中收到取消请求,会直到这个线程能取消为止,收到取消信号才会被响应     
	   // 如果该线程收到取消请求,没法响应,那么会阻塞到这个线程能相应为止
	   
	   
       Compile and link with -pthread.

实操:

#include <sys/types.h>
#include <sys/ipc.h>
#include <string.h>
#include <sys/msg.h>
#include <stdlib.h>
#include <errno.h>

void *func(void* arg)
{
	pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
	int cnt = 10;
	while(cnt--)
	{
		sleep(1);
		printf("child pthread------%d\n", cnt);
	}
	pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
}

int main(int argc, char const *argv[])
{
	pthread_t thread;

	pthread_create(&thread, NULL, func, NULL);
	sleep(5);

	// 主线程给子线程发送取消请求
	pthread_cancel(thread);

	// 等待子线程退出
	int ret = pthread_join(thread, NULL);
	if(0 != ret)
	{
		printf("error\n");
	}
	
	return 0;
}

终端显示:
image

分析:主线程5s给子线程发送取消请求,由于子线程进入就设置不响应,直到cnt加到10,即10s后,取消了子线程的不响应。主线程发给子线程的取消请求,才得以响应

  1. 设置线程响应取消的类型--------->pthread_setcanceltype()
       int pthread_setcanceltype(int type, int *oldtype);
	      // 参数1:PTHREAD_CANCEL_DEFERRED---->延迟响应,即遇到一个取消函数才会响应取消请求,线程默认属性
	     //       PTHREAD_CANCEL_ASYNCHRONOUS------->立即响应
	    // 返回值:成功0,失败非0错误码

注意:线程是遇到取消点函数之后才会去响应取消
取消函数(man 7 pthreads):

  • fprintf()
  • fputc()
  • fputs()
  • sleep()
  • printf()
  • usleep()
给线程设置延迟响应取消请求

pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);

给线程设置立即响应取消请求

pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);

线程取消例程函数

  1. 什么是线程取消例程函数
    当线程收到取消请求时,先不要马上响应取消函数,而先去执行一个例程函数,执行完这个函数再响应取消。
  2. 为什么要使用取消例程函数
    为了防止线程带着一些公共资源而被取消掉
  3. 如何实现
    压栈线程的取消例程函数------->pthread_cleanup_push()
#include <pthread.h>

       void pthread_cleanup_push(
		void (*routine)(void *),        // 参数1:线程取消的例程函数
		void *arg                      // 参数2:传递给取消例程函数的参数
	);
       Compile and link with -pthread.

弹栈线程的取消例程函数----->pthread_cleanup_pop(int execute);

       void pthread_cleanup_pop(int execute);      // 参数:0:删除时,直接删除
	                                          // 非0:删除时,执行一次例程函数,再删除

注意:

  1. 子线程收到取消请求之后会执行取消例程函数,然后执行完相应取消请求,直接退出,不会再往下进行了
  2. 如果子线程没有收到取消请求,而且程序已经执行到了pthread_cleanup_pop函数时,此函数才会执行,并且根据参数决定是否执行线程取消例程函数再退出子线程
  3. 这两个函数必须成对出现