线程的概念

回顾进程
在介绍线程之前,我们先来回顾下进程,进程的定义就是指一个具有独立功能的程序在某个数据集上的一次动态执行的过程,是系统进程资源分配和调度的基本单元。 一次任务的运行可以并发激活多个进程,这些进程相互合作完成该任务的一个最终目标。 操作系统对进程的描述:PCB(进程控制块)Linux下的进程描述——task_struct。

那么线程是什么呢?

  • 线程是进程中的一条执行流,Linux下的线程是用进程的PCB模拟的,所以Linux下的线程也叫轻量级进程。 进程是资源分配的基本单位, 那么线程就是CPU调度的基本单位。
  • 一个进程至少有一个线程,因此我们的进程其实就是线程组。进程id = 线程组id,所以才说Linux下的进程是线程组,资源分配的基本单位,并且进程中的线程共享大部分进程的资源。
  • Linux下的线程共用进程的虚拟地址空间,与进程内的其他线程共享进程的资源,共享代码段,数据段。
  • 文件描述符表,信号处理方式,工作目录用户id
  • 线程不仅共享进程的这些资源,并且还独自有一些资源:栈,上下文数据。

有了进程为什么还要线程呢?
线程的优缺点正好说明了我们操作系统为什么还要线程。

优点:

  • 线程的创建和销毁成本更低。
  • 线程的调度切换成本也会更低
  • 线程间通信更加方便
  • 线程执行的力度更加细致

缺点

  • 缺乏访问控制:进程是访问的基本粒度,在一个线程中调用可能会对整个进程造成影响
  • 多个线程对临界资源进行操作时会造成数据混乱
  • 性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享一个处理器。如果密集型线程数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加额外的同步和调度开销,而可用的资源不变。
  • 调试难度大大提高:编写与调试一个多线程程序比单线程程序困难的多。

即便是线程有一些缺点,但它的作用依然非常强大,任然引用与一些项目中。

进程的一些特征: 我们知道,进程有进程的标识符pid,那么线程也有自己的标识符tid

线程的基本操作

1.pthread_create函数 功能:创建线程

int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*func)(void *), void *arg);
    函数参数解释:
      pthread_t *tid:一个进程内的各个线程是由线程ID标识的,如果新线程创建成功,返回tid指针。

      const pthread_attr_t *attr:每个线程有多个属性,包括优先级、初始栈大小、是否是一个守护线程等等。

      void *(*func)(void *):线程启动函数,线程从调用这个函数开始,或显示结束(调用pthread_exit()),或隐式结束(让该函数返回)。

      void *arg:线程执行func函数的传递参数。

2.pthread_join函数 功能:等待一个线程终止

int pthread_join(pthread_t *tid, void **status);
      void **status:二级指针,如果status指针非空,那么所等待线程的返回值将存放在status指向的位置。

3.pthread_self函数 功能:返回线程ID

int pthread_self(void);
      跟进程比较,相当于getpid。

4.pthread_detach函数 功能:线程分离

int pthread_detach(pthread_t tid);  
      线程或者是可汇合的(joinable),或者是脱离的(detach)。当可汇合的线程终止时,线程ID和退出状态将保留,知道另外一个线程调用pthread_join。脱离的线程终止时,释放所有的资源,因此我们不能等待它终止。若要一个线程知道另一个线程的终止时间,我们就要保留第二个线程的可汇合性。

5.pthread_exit函数 功能:线程终止

int pthread_exit(void **status);  
      若线程未脱离,那么它的线程ID和退出状态将保留到另外一个线程调用pthread_join为止。

下面我们演示一个线程的创建

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

void *thr_start(void *arg)
{
    pthread_t *tid = (pthread_t*)arg;
    while(1) {
        printf("i am child~~~%p\n", *tid);
        sleep(1);
        //void pthread_exit(void *retval);
        //用于退出自己,可以返回一个数据
        //pthread_exit(NULL);
    }
    return NULL;
}
int main()
{
    //演示多个线程的并行
    //创建线程
    //int pthread_create(pthread_t *thread, pthread_attr_t *attr,
    //  void *(*start_routine) (void *), void *arg);
    //  thread: 用于获取线程id(用户态的线程id)
    //  attr:  设置线程属性,通常置NULL
    //  start_routine:线程入口函数,线程所运行的代码
    //  arg:   线程入口函数的参数
    //  返回值:成功:0     失败:非0
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, thr_start, (void*)&tid);
    if (ret != 0) {
        printf("pthread create error\n");
        return -1;
    }
    int pid = getpid();
    sleep(3);

    pthread_cancel(tid);  //用于取消标识符为tid线程
    while(1) {
        //pthread_t pthread_self(void);
        //获取线程自身的线程id
        printf("i am main~~~~!%d---%p\n", pid, pthread_self());
        sleep(1);
    }
    return 0;
}

下面是一个线程分离演示

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

void *thr_start(void *arg)
{
    //int pthread_detach(pthread_t thread);
    //  分离指定的线程,被分离的线程退出时自动被回收资源
    //  因为资源立即被回收,所以不会保存返回值,也就无法被等待
    //  thread: 指定要分离的线程id
    //pthread_t pthread_self(void)
    //  获取自身线程 id
    pthread_detach(pthread_self());
    char *ptr = "laozaotai!!\n";

    sleep(3);
    return ptr;
}
int main()
{
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, thr_start, NULL);
    if (ret != 0) {
        printf("thread create error\n");
        return -1;
    }
    usleep(100);
    //int pthread_join(pthread_t thread, void **retval);
    //这是一个阻塞函数,如果线程没有退出就一直等待
    //这里等待的线程必须是处于joinable属性才可以被等待
    //  thread: 指定等待的线程id
    //  retval:获取线程的退出返回值
    void *ptr = NULL;
    int err = pthread_join(tid, &ptr);
    if (err == EINVAL) {
        printf("thread is detached!!\n");
    }
    printf("child thread:%s\n", ptr);
    return 0;
}

多线程

一个线程的作用不大,但是一堆线程能干大事,了解王者荣耀的人可能会知道,王者荣耀采用了多线程开发技术,多线程模式,这个模式的作用可以让玩家在团战中帧率更高,打起来更加的流畅,这就是多线程的一个好处。

线程安全问题

因为线程是CPU调度的基本单位,因此多个线程就有可能同时争抢临界资源,那么这种情况就有可能导致数据的二义性。打个比方:当两个线程同时向一个文件修改数据时,那么这个文件的数据到底让那个线程修改?这就是一个线程安全问题。

为了解决线程安全问题,我们引入了线程的同步与互斥

  • 互斥:保证数据同一时间唯一的访问,那么我们就可以用一个锁来锁住当前线程,不让其它线程进行访问,即互斥锁
    pthread_mutex_init  //初始化   	
    pthread_mutex_destroy
    pthread_mutex_lock   			
    pthread_mutex_trylock
    pthread_mutex_unlock 			
    pthread_mutex_timedlock //指定一段时间内获取锁
  • 死锁:在使用互斥锁的同时,有可能会发生死锁,在前面有关于死锁问题的文章,这里就不做赘述了

  • 同步:保证对临界资源访问的时序性,即我们需要条件变量通知线程或等待线程,满足操作条件,才可以操作,不满足则需要等待,而条件满足就需要其它线程修改条件,并且通知一下等待的进程

    pthread_cond_init	//初始化
    pthread_cond_destroy		
    pthread_cond_broadcast  //唤醒多个线程,广播唤醒
    pthread_cond_wait	//等待		
    pthread_cond_timedwait		
    pthread_cond_signal  // 唤醒第一个等待的线程,通知一个线

下面演示一个互斥锁使用场景,抢票

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

volatile ticket = 100;
pthread_mutex_t mutex;

void *yellow_cow(void *arg)
{
    int id = (int)arg;
    while(1) {
        //int pthread_mutex_lock(pthread_mutex_t *mutex);
        //int pthread_mutex_trylock(pthread_mutex_t *mutex);
        //int pthread_mutex_unlock(pthread_mutex_t *mutex);
        //lock 阻塞加锁,trylock 非阻塞加锁,timedlock 限时阻塞加锁
        pthread_mutex_lock(&mutex);
        if (ticket > 0) {
            usleep(100);
            ticket--;
            printf("cow %d get ticket:%d\n", id, ticket);
        }else {
            //在任何有可能退出的地方都必须在退出前解锁
            pthread_mutex_unlock(&mutex);
            pthread_exit(NULL);
        }
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}
int main()
{
    int max = 4, i;
    pthread_t tid[4];

    //  互斥锁变量的初始化
    //int pthread_mutex_init(pthread_mutex_t *restrict mutex,
    //  const pthread_mutexattr_t *restrict attr);
    //  这种初始化,在使用结束后必须要释放
    //  mutex:  互斥锁变量
    //  attr:  属性,通常置NULL
    //pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    pthread_mutex_init(&mutex, NULL);
    for (i = 0; i < max; i++) {
        int ret = pthread_create(&tid[i],NULL,yellow_cow,(void*)i);
        if (ret != 0) {
            printf("create thread error\n");
            return -1;
        }
    }
    for (i = 0; i < max; i++) {
        pthread_join(tid[i], NULL);
    }
    //int pthread_mutex_destroy(pthread_mutex_t *mutex);
    //  销毁互斥锁
    pthread_mutex_destroy(&mutex);

    return 0;
}