多线程基础

Posted _Karry

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程基础相关的知识,希望对你有一定的参考价值。

线程概述

线程是轻量级的进程(LWP:light weight process),在 Linux 环境下线程的本质仍是进程。在计算机上运行的程序是一组指令及指令参数的组合,指令按照既定的逻辑控制计算机运行。操作系统会以进程为单位,分配系统资源,可以这样理解,进程是资源分配的最小单位,线程是操作系统调度执行的最小单位

先从概念上了解一下线程和进程之间的区别:

  • 进程有自己独立的地址空间,多个线程共用同一个地址空间
    线程更加节省系统资源,效率不仅可以保持的,而且能够更高
    在一个地址空间中多个线程独享:每个线程都有属于自己的栈区,寄存器 (内核中管理的)
    在一个地址空间中多个线程共享:代码段,堆区,全局数据区,打开的文件 (文件描述符表) 都是线程共享的

  • 线程是程序的最小执行单位,进程是操作系统中最小的资源分配单位
    每个进程对应一个虚拟地址空间,一个进程只能抢一个 CPU 时间片
    一个地址空间中可以划分出多个线程,在有效的资源基础上,能够抢更多的 CPU 时间片
    在这里插入图片描述

  • CPU 的调度和切换:线程的上下文切换比进程要快的多
    上下文切换:进程 / 线程分时复用 CPU 时间片,在切换之前会将上一个任务的状态进行保存,下次切换回这个任务的时候,加载这个状态继续运行,任务从保存到再次加载这个过程就是一次上下文切换。

  • 线程更加廉价,启动速度更快,退出也快,对系统资源的冲击小。

在处理多任务程序的时候使用多线程比使用多进程要更有优势,但是线程并不是越多越好,如何控制线程的个数呢?

文件 IO 操作:文件 IO 对 CPU 是使用率不高,因此可以分时复用 CPU 时间片,线程的个数 = 2 * CPU 核心数 (效率最高)

处理复杂的算法 (主要是 CPU 进行运算,压力大),线程的个数 = CPU 的核心数 (效率最高)

创建线程

线程函数

每一个线程都有一个唯一的线程 ID,ID 类型为 pthread_t,这个 ID 是一个无符号长整形数,如果想要得到当前线程的线程 ID,可以调用如下函数:

pthread_t pthread_self(void); //返回当前线程ID

在一个进程中调用线程创建函数,就可得到一个子线程,和进程不同,需要给每一个创建出的线程指定一个处理函数,否则这个线程无法工作

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);
// Compile and link with -pthread, 线程库的名字叫pthread, 全名: libpthread.so libptread.a
  • 参数:
    thread: 传出参数,是无符号长整形数,线程创建成功,会将线程 ID 写入到这个指针指向的内存中
    attr: 线程的属性,一般情况下使用默认属性即可,写 NULL
    start_routine: 函数指针,创建出的子线程的处理动作,也就是该函数在子线程中执行。
    arg: 作为实参传递到 start_routine 指针指向的函数内部

  • 返回值:线程创建成功返回 0,创建失败返回对应的错误号

创建线程

// pthread_create.c 
#include <stdio.h>
#include <pthread.h>

// 子线程的处理代码
void* working(void* arg)
{
    printf("我是子线程, 线程ID: %ld\\n", pthread_self());
    for(int i=0; i<9; ++i)
    {
        printf("child == i: = %d\\n", i);
    }
    return NULL;
}

int main()
{
    // 1. 创建一个子线程
    pthread_t tid;
    pthread_create(&tid, NULL, working, NULL);

    printf("子线程创建成功, 线程ID: %ld\\n", tid);
    // 2. 子线程不会执行下边的代码, 主线程执行
    printf("我是主线程, 线程ID: %ld\\n", pthread_self());
    for(int i=0; i<3; ++i)
    {
        printf("i = %d\\n", i);
    }
    
    // 休息, 休息一会儿...不然子线程可能无法抢夺到CPU
    sleep(1);
    
    return 0;
}

线程退出

在编写多线程程序的时候,如果想要让线程退出,但是不会导致虚拟地址空间的释放(针对于主线程),我们就可以调用线程库中的线程退出函数,只要调用该函数当前线程就马上退出了,并且不会影响到其他线程的正常运行,不管是在子线程或者主线程中都可以使用。

#include <pthread.h>
void pthread_exit(void* retval);

参数:线程退出的时候携带的数据,当前子线程的主线程会得到该数据。如果不需要使用,指定为 NULL

线程回收

线程回收函数

线程和进程一样,子线程退出的时候其内核资源主要由主线程回收,线程库中提供的线程回收函叫做 pthread_join(),这个函数是一个阻塞函数,如果还有子线程在运行,调用该函数就会阻塞,子线程退出函数解除阻塞进行资源的回收,函数被调用一次,只能回收一个子线程,如果有多个子线程则需要循环进行回收。

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
  • 参数:
    thread: 要被回收的子线程的线程 ID
    retval: 二级指针,指向一级指针的地址,是一个传出参数,这个地址中存储了 pthread_exit () 传递出的数据,如果不需要这个参数,可以指定为 NULL
  • 返回值:线程回收成功返回 0,回收失败返回错误号。

回收子线程数据

在子线程退出的时候可以使用 pthread_exit() 的参数将数据传出,在回收这个子线程的时候可以通过 pthread_join() 的第二个参数来接收子线程传递出的数据。

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

struct Test
{
    int num;
    int age;
};
struct Test t;

void* callback(void *arg)
{
    for(int i = 0; i < 5; ++i)
    {
        printf("zi: i = %d\\n", i);
    }
    printf("zi : %ld \\n", pthread_self());
    t.num = 100;
    t.age = 10;
    pthread_exit(&t);
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, NULL, callback, NULL);
    
    printf("zhu: %ld\\n", pthread_self());
    
    void *ptr;
    pthread_join(tid, &ptr);
    struct Test *t = (struct Test*)ptr;
    printf("num = %d, age = %d\\n", t->num, t->age);
    return 0;
}

线程分离

在某些情况下,程序中的主线程有属于自己的业务处理流程,如果让主线程负责子线程的资源回收,调用 pthread_join() 只要子线程不退出主线程就会一直被阻塞,主要线程的任务也就不能被执行了。

在线程库函数中为我们提供了线程分离函数 pthread_detach(),调用这个函数之后指定的子线程就可以和主线程分离,当子线程退出的时候,其占用的内核资源就被系统的其他进程接管并回收了。线程分离之后在主线程中使用 pthread_join()就回收不到子线程资源了。

#include <pthread.h>
// 参数就子线程的线程ID, 主线程就可以和这个子线程分离了
int pthread_detach(pthread_t thread);
#include <stdio.h>
#include <pthread.h>

void* callback(void* arg)
{
	printf("我是子线程, 线程ID: %ld\\n", pthread_self());
    for(int i=0; i<9; ++i)
    {
        printf("child == i: = %d\\n", i);
    }
    return NULL;
}

int main()
{
	pthread_t pid;
	pthread_create(&pid, NULL, callback, NULL);
    printf("子线程创建成功, 线程ID: %ld\\n", tid);
    // 子线程不会执行下边的代码, 主线程执行
    printf("我是主线程, 线程ID: %ld\\n", pthread_self());

	// 设置子线程和主线程分离
	pthread_detach(tid);
	
	// 主线程退出
	pthread_exit(NULL);
	
	return 0;
}

其它线程函数

线程取消

线程取消的意思就是在某些特定情况下在一个线程中杀死另一个线程。使用这个函数杀死一个线程需要分两步:

  1. 在线程 A 中调用线程取消函数 pthread_cancel,指定杀死线程 B,这时候线程 B 是死不了的
  2. 在线程 B 中进程一次系统调用(从用户区切换到内核区),否则线程 B 可以一直运行。
#include <pthread.h>
// 参数是子线程的ID
int pthread_cancel(pthread_t thread);

线程ID比较

在 Linux 中线程 ID 本质就是一个无符号长整形,因此可以直接使用比较操作符比较两个线程的 ID,但是线程库是可以跨平台使用的,在某些平台上 pthread_t 可能不是一个单纯的整形,这中情况下比较两个线程的 ID 必须要使用比较函数,函数原型如下:

#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
  • 参数:t1 和 t2 是要比较的线程的线程 ID
  • 返回值:如果两个线程 ID 相等返回非 0 值,如果不相等返回 0

总结

#include <pthread.h>
//创建子线程
pthread_t tid;
pthread_create(&tid, NULL, callback, NULL);
//第四个参数也可以是传递给子线程的实参
pthread_create(&tid, NULL, callback, void* arg);

//线程退出
pthread_exit(NULL); 
//或者线程返回的数据的地址
pthread_exit(void* retval);

//线程回收
pthread_join(tid, NULL); 
//或是二级指针,指向子线程pthread_exit(void *retval)的返回参数的指针
pthread_join(tid, void **retval);

参考

线程_爱编程的大丙

以上是关于多线程基础的主要内容,如果未能解决你的问题,请参考以下文章

线程学习知识点总结

号称史上最全Java多线程与并发面试题总结—基础篇

多个请求是多线程吗

java基础入门-多线程同步浅析-以银行转账为样例

python小白学习记录 多线程爬取ts片段

多线程编程