线程

1. 线程概述

  • 每一个进程【Process】都是由【线程 Thread 组成】,程序可以利用多线程方式来解决进程中的相关任务内容
  • 线程使用的资源是由进程分配,不会自行申请资源内容,同时 CPU 可以指挥线程进行相关的任务执行。
  • 进程中使用线程来完成多任务执行,可以降低系统资源的占用,但是线程对应进程而言不易过多,会导致线程资源冲突,性能降低。
  • 线程项目中使用情况
    • 文件管理一个线程
    • 网络连接一个线程
    • 进程通讯管理,利用一个线程
    • .......

2. 进程和线程的内存问题

  • 进程的内存依然是系统分配的: 栈区,堆区,BSS 区,数据区和代码区。
  • 线程使用的内存是当前进程分配给线程使用的,每一个线程都在栈区使用一部分数据空间,作为自行执行任务,相关数据的必要内存空间,其他内存使用的都是当前进程中的相关资源。
  • 因为线程都在当前进程中存在,线程与线程之间的通讯效率较高!同时存在共享资源冲突问题,后续需要利用 lock 机制解决。
  • 进程和线程对比,线程占用的资源较少,进程占用的相关资源内容较多。

3. 线程基本操作

注意,编译 pthread 相关代码,格式案例如下

gcc 01-pthread_create.c -pthread
3.1 线程创建 pthread_create

创建一个线程,函数 pthread_create

函数文档:

#include <pthread.h>

int pthread_create(pthread_t *thread, 
                   const pthread_attr_t *attr,
                   void *(*start_routine) (void *), 
				   void *arg);
  • 函数功能:

    • 在当前进程中,创建一个线程,用于执行目标 start_routine 函数对应功能
  • 函数参数:

    • pthread_t *thread : typdef unsigned long int pthread_t 当前创建的线程 id 号,需要一个变量地址,创建成功之后,会将对应的线程 ID 存储到当前变量中。
    • const pthread_attr_t *attr : 线程属性共用体 union 类型,存储当前线程的相关属性 (attribute)
    • void *(*start_routine) (void *) : 函数指针,是当前线程执行的任务函数,也可以认为是开始函数,what will be run?。函数指针对应类型要求,函数参数类型为 (void *),函数返回值类型也是 void *
    • void * arg : 提供给当前函数指针指向函数执行必要参数
  • 返回值

  • 如果函数调用成功,pthread_create 会返回 0

  • 如果调用失败,则会返回一个非零的错误码,用于指示具体的错误类型。常见的错误码有:

    • EAGAIN:系统资源不足,无法创建新线程。
    • EINVAL:传递的 attr 参数无效。
    • EPERM:没有足够的权限来设置指定的线程属性。

线程基本创建

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <pthread.h>
#include <unistd.h>
/*
#include <pthread.h>

int pthread_create(pthread_t *thread,
                const pthread_attr_t *attr,
                void *(*start_routine) (void *),
                   void *arg);

pthread_t pthread_self (void)
    获取当前线程 ID 号、线程标识符
*/

void *thread_fun1(void *arg);
void *thread_fun2(void *arg);

int main(int argc, char const *argv[])
{
    pthread_t thread_id1 = 0;
    pthread_t thread_id2 = 0;

    pthread_create(&thread_id1, NULL, thread_fun1, NULL);
    pthread_create(&thread_id2, NULL, thread_fun2, NULL);

    sleep(15);
    return 0;
}

void *thread_fun1(void *arg)
{
    int second = 10;
    while (second > 0)
    {   
        printf("PID : %d, thread_id : %ld, Thread Fun1 Running! Thread Exit int %d second!\n",
               getpid(), pthread_self(), second);
        second -= 1;
        sleep(1);
    }

    return NULL;
}

void *thread_fun2(void *arg)
{
    int second = 10;
    while (second > 0)
    {
        printf("PID : %d, thread_id : %ld, Thread Fun2 Running! Thread Exit int %d second!\n",
               getpid(), pthread_self(), second);
        second -= 1;
        sleep(1);
    }

    return NULL;
}

验证线程执行执行函数所需参数情况

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <pthread.h>
#include <unistd.h>
/*
#include <pthread.h>

int pthread_create(pthread_t *thread,
                const pthread_attr_t *attr,
                void *(*start_routine) (void *),
                   void *arg);

pthread_t pthread_self (void)
    获取当前线程 ID 号、线程标识符
*/

void *thread_fun1(void *arg);
void *thread_fun2(void *arg);

int main(int argc, char const *argv[])
{
    pthread_t thread_id1 = 0;
    pthread_t thread_id2 = 0;

    int num = 100;

    /*
    利用 pthread_create 函数第四个参数位置,将 num 作为执行所需函数
    对应的参数内容
  
    校验四种形式
        1. num  ERROR
        2. &num SUCCESS
        3. (void *)&num SUCCESS
        4. (void *)num ERROR
  
    执行线程操作之后,num的存储数据为
        1. 100
        2. 200
        3. 500
        200 和 500 随机情况,看哪一个线程后触发执行。

    thread_id1 和 thread_id2 线程,执行了多少次线程任务?
        A. 1  B. 10  C. 2 D. 钝角
        每一个线程中的线程函数,各自有且只执行一次,代码中出现的
        循环效果是函数内部 while 循环导致的。
     */

    pthread_create(&thread_id1, NULL, thread_fun1, &num);
    pthread_create(&thread_id2, NULL, thread_fun2, (void *)&num);

    sleep(15);

    printf("num = %d\n", num);

    return 0;
}

void *thread_fun1(void *arg)
{
    int second = 10;

    /*
    *arg = 200; ERROR 
        因为 arg 数据类型为 void *,进行取值操作之后,对应的真实类型
        为 void 类型,void 类型无法进行任何的赋值操作和取值操作。计
        算机认为是空类型
    *((int *)arg) = 200; SUCCESS
        首先对 arg 进行强制类型转换,将原本 void * 指针转换为 int * 指针
        再进行数据的取值赋值操作
    */
    *((int *)arg) = 200;
  
    while (second > 0)
    {   
        printf("PID : %d, thread_id : %ld, Thread Fun1 Running! Thread Exit int %d second!\n",
               getpid(), pthread_self(), second);
        second -= 1;
        sleep(1);
    }


    return NULL;
}

void *thread_fun2(void *arg)
{
    int second = 10;

    *((int *)arg) = 500;

    while (second > 0)
    {
        printf("PID : %d, thread_id : %ld, Thread Fun2 Running! Thread Exit int %d second!\n",
               getpid(), pthread_self(), second);
        second -= 1;
        sleep(1);
    }

    return NULL;
}
3.2 线程等待 pthread_join

函数文档

#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);
  • 函数功能:
    • tips: retval ==> return value 返回值,因为 pthread 线程执行目标任务函数的返回值为
      void *,当前 retval 存储返回值数据的地址。
    • 等待子线程运行结束,并且回收子线程使用的相关资源。
  • 函数参数:
    • pthread_t thread : 对应执行 pthread_join 函数的目标线程 thread id 数据
    • void **retval: 线程执行完成之后,线程执行函数的返回值存储到 retval 中,后续可以对应当前函数的返回值内容,进行相关的资源处理。 因为 pthrea_create 函数中目标函数返回值类型为 void *,当前存储目标类型是一个 void * 类型,参数类型是一个二级指针 void **,如不需要接受返回值,参数可以是 NULL
  • 返回值
    • 成功返回 0
    • 失败返回非 0

pthread_join 阻塞效果

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

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

void *thread_fun(void *arg);

int main(int argc, char const *argv[])
{
   pthread_t thread_id1 = 0;

   int ret = pthread_create(&thread_id1, NULL, thread_fun, NULL);
   if (ret)
   {
       perror("pthread_create failed");
   }

   printf("Before Join!\n");

   /*
   一旦执行 pthread_join 函数,当前 main线程/进程 被阻塞,优先保证 thread_id1 
   对应线程执行。等待目标子线程执行完毕
   */
   pthread_join(thread_id1, NULL);
   
   int n = 10;
   while (n > 0)
   {
       printf("Process Running!\n");
       sleep(1);
       n -= 1;
   }
   printf("After Join!\n");


   return 0;
}

void *thread_fun(void *arg)
{
   int second = 10;
   while (second > 0)
   {
       printf("PID : %d, thread_id : %ld, Thread Fun1 Running! Thread Exit int %d second!\n",
              getpid(), pthread_self(), second);
       second -= 1;
       sleep(1);
   }

   return NULL;
}

pthread_join 利用第二个参数获取线程执行函数返回值数据

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

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

void *thread_malloc(void *arg);

int main(int argc, char const *argv[])
{
    pthread_t thread_id1 = 0;

    int buf_size = 64;

    int ret = pthread_create(&thread_id1, NULL, thread_malloc, &buf_size);
    if (ret)
    {
        perror("pthread_create failed");
    }

    void * retval = NULL;
    /*
    将子线程执行函数的返回值,存储到 retval 中 
    */
    pthread_join(thread_id1, &retval);

    for (int i = 0; i < buf_size / sizeof(int); i++)
    {
        printf("retval data : %d\n", ((int *)retval)[i]);
    }

    free(retval);

    return 0;
}

void *thread_malloc(void *arg)
{
    // 获取用户提供给当前线程执行函数的参数内容
    int buf_size = *((int *)arg);

    // 如果提供的 buf_size 小于等于 0,直接返回 NULL
    if (buf_size <= 0)
    {
        return NULL;
    }

    void * buffer = malloc(buf_size); 
    memset(buffer, 0, buf_size);

    for (int i = 0; i < buf_size / sizeof(int); i++)
    {
        ((int *)buffer)[i] = i + 1;
    }
  
    return buffer; // void * buffer ==> 0x1000;
}
3.3 线程分离 pthread_detach

pthread_join 之后,主线程会进入阻塞状态,可以使用 pthread_detach 函数将子线程和主线程进行分离,作为独立线程执行

函数文档

#include <pthread.h>

int pthread_detach(pthread_t thread);
  • 函数功能:
    • 根据指定的线程 ID 号,将对应的线程分离为独立的线程,尤其是在 join 函数执行之前,可以避免线程阻塞
  • 函数参数:
    • pthread_t thread :目标分离的线程 pthread_t 线程 ID/线程标识符确定哪一个线程进行分离
  • 返回值类型:
    • 操作成功,返回 0
    • 操作失败,返回非 0 值
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

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

void *thread_function_1(void *arg);

int main(int argc, char const *argv[])
{
    pthread_t thread_id = 0;
    int count = 20;
    int ret = pthread_create(&thread_id, NULL, thread_function_1, NULL);

    if (ret)
    {
        perror("pthread_create failed");
    }

    pthread_detach(thread_id);
    pthread_join(thread_id, NULL);

    int second = 2;

    while (second > 0)
    {
        printf("Thread ID : %ld, main thread runnning!!!\n", pthread_self());
        second -= 1;
        sleep(1);
    }

    return 0;
}

void *thread_function_1(void *arg)
{
    int second = 10;

    while (second > 0)
    {
        printf("Thread ID : %ld, son thread runnning!!!\n", pthread_self());
        second -= 1;
        sleep(1);
    }

    return NULL;
}
3.4 线程退出 pthread_exit

exit(int status)_exit(int status)exit 是 C 库函数,_exit 是系统调用 System Calls 函数,作用都是退出当前进程。在多线程情况下,使用 exit 退出,会导致整个程序运行结束。

**函数文档 **

#include <pthread.h>

void pthread_exit(void *retval);
  • 函数功能:
    • 退出当前执行的线程,同时清理当前线程使用的相关资源,另外函数参数是一个 (void * retval),可以将线程的退出状态告知外部。
  • 函数参数:
    • void * retval : 当前线程的退出状态,可以在外边使用 pthread_join 来获取线程执行完毕之后的数据反馈
3.5 线程取消 pthread_cancel

函数文档

#include <pthread.h>

int pthread_cancel(pthread_t thread);
  • 函数功能:
    • **可以发送给指定线程一个取消【信号】,**收到当前信号的线程, 会根据自身状态,选择是否取消。
  • 函数参数:
    • pthread_t thread : 取消操作目标线程标识符
  • 返回值
    • 成功返回 0,失败返回非 0 错误值。

线程取消情况说明

  • 默认情况下,线程的取消状态是:【可以取消】【收到信号,到达取消点函数取消】
  • 线程取消状态设置函数
    • int pthread_setcancelstate (int __state, int *__oldstate);
    • 设置当前线程的取消状态
      ** 第一个参数 int _state 有两个系统预设的宏作为参数,分别为**
      ** PTHREAD_CANCEL_ENABLE 当前线程可以取消**
      ** PTHREAD_CANCEL_DISABLE 当前线程不可以取消**
    • 如果设置 PTHREAD_CANCEL_DISABLE 状态,其他线程发送给当前线程的取消信号会被挂起,直到当前线程恢复可退出状态,触发原本的取消信号。
  • 线程取消类型设置函数
    • int pthread_setcanceltype(int type, int *oldtype);
    • 设置当前线程的取消类型,有立即取消和到达取消点取消
    • PTHREAD_CANCEL_DEFERRED 【默认状态】当前线程收到取消信号,会执行到下一个取消点函数进行线程取消,较为安全。
    • PTHREAD_CANCEL_ASYNCHRONOUS 收到取消信号立即取消,有一定的风险