Administrator
发布于 2022-03-26 / 6 阅读
0
0

线程的简单使用

线程的简单使用

线程方面的知识一直是很薄弱的, 所以再次翻开 《APUE 》 ,重新再看一遍 第11 章 线程,有不一样的收获,在这总结下 线程的一些使用方法 ; (PS: 发现以前好多看得云里雾里的书籍,现在重头看,有种恍然大悟的感觉!)

线程概念

进程是程序执行时的一个实例,是担当分配系统资源(CPU时间、内存等)的基本单位。在面向线程设计的系统中,进程本身不是基本运行单位,而是线程的容器。程序本身只是指令、数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例。

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。线程包含了表示进程内执行环境必须的信息,其中包括进程中表示线程的线程ID、一组寄存器值、栈、调度优先级和策略、信号屏蔽字、errno常量以及线程私有数据。进程的所有信息对该进程的所有线程都是共享的,包括可执行的程序文本、程序的全局内存和堆内存、栈以及文件描述符。在Unix和类Unix操作系统中线程也被称为轻量级进程(lightweight processes),但轻量级进程更多指的是内核线程(kernel thread),而把用户线程(user thread)称为线程。

线程和进程一样也有标志号作为识别

线程创建 线程创建


int pthread_create (pthread_t *__restrict __newthread, const pthread_attr_t *__attr,void *(*fun) (void *),void *arg);

参数分别是 线程的pid,线程属性,线程的函数,传入线程的参数,创建失败会返回错误码

对于 传参 的arg指针,不需要给线程传入参数的话直接 设置为 null即可

线程终止

终止有三种方式

  • 运行完线程代码return 结束
  • 被其他线程中终止
  • 线程可以调用 pthread\_exit ( 自己终结自己 )

pthread\_exit() 可以携带返回值

如果需要 现有线程的返回值怎么办? 有一个函数可以做到


/* Make calling thread wait for termination of the thread TH.  The
   exit status of the thread is stored in *THREAD_RETURN, if THREAD_RETURN
   is not NULL.
让调用线程等待线程TH的终止。线程的退出状态存储在*THREAD_RETURN中
This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int pthread_join (pthread_t __th, void **__thread_return);

线程的返回转态可以保存在 \_\_thread\_return 中;


#include <arpa/inet.h>
#include <errno.h>
#include <netinet/tcp.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <string>
#include <iostream>
using namespace std;

void cleanup(void *arg){
    printf("cleanup : %s \n",(char *)arg);
}

void* fun(void *arg){
    char *str = (char *)arg;
    printf("%s\n",str);
    sleep(1);
    char *ret = "hrrrr";
    return ((void *)ret);
}
void* fun2(void *arg){
    char *str = (char *)arg;
    printf("%s\n",str);
    sleep(1);
    char *ret = "cccc";
    pthread_exit((void *)ret);
}
int main(){

    pthread_t pid[5];
    char *str1 = "str1 传入的参数内容";
    char *str2 = "str2 传入的参数内容";

    pthread_create(&pid[0],NULL,fun,str1);
    pthread_create(&pid[1],NULL,fun2,str2);

    void *tre,*tre1;
    pthread_join(pid[0],&tre);
    pthread_join(pid[1],&tre1);
    printf("%s \n",(char *)tre);
    printf("%s \n",(char *)tre1);
    sleep(10);
    return 1;
}

!image-20220326154257729

线程结束后可以得到返回值,返回值分别是return返回的和 pthread\_exit结束时返回的

线程同步

提到线程必然有很多问题 ,多线程并发的时,同时读取相同变量,不是原子性 可以能会有数据不一致的情况(i++)

i++ 操作 是

  • 将内存读取寄存器
  • 操作寄存器的值
  • 新的值写内存单元

哪如何保证线程的同步呢?

互斥锁 互斥锁

对资源加锁后,任何线程试图再次对该互斥量加锁的线程都会阻塞,直到到该锁释放为止,这样可以确保每次只有一个线程可以获取资源,保证原子性

使用互斥锁得先初始化


// pthread_mutex_t 是一个结构体
pthread_mutex_t __mutex;
//init
extern int pthread_mutex_init (pthread_mutex_t *__mutex,
			       const pthread_mutexattr_t *__mutexattr)
     __THROW __nonnull ((1));

/* Destroy a mutex.  */
extern int pthread_mutex_destroy (pthread_mutex_t *__mutex)
     __THROW __nonnull ((1));

/* Try locking a mutex.  */
extern int pthread_mutex_trylock (pthread_mutex_t *__mutex)
     __THROWNL __nonnull ((1));

/* Lock a mutex.  */
extern int pthread_mutex_lock (pthread_mutex_t *__mutex)
     __THROWNL __nonnull ((1));

/* Unlock a mutex.  */
extern int pthread_mutex_unlock (pthread_mutex_t *__mutex)
     __THROWNL __nonnull ((1));

使用的话就是在 访问临界区资源之前对 互斥量加锁,尽管这样可以解决线程同步的问题,但是如果存在多个锁,没有设计好加锁顺序,可能造成死锁,看代码

此时有两个锁和两个线程,每个线程加锁顺序相反的话,就会造成 死锁, 互相等待对方释放资源


 #include <bits/stdc++.h>
#include <unistd.h>
using namespace std;
pthread_mutex_t plock2;
pthread_mutex_t plock1;
pthread_mutex_t plock3 = PTHREAD_MUTEX_INITIALIZER;

static int i = 0;
//以不同顺序加锁
void* fun(void *arg){

    while (i < 100)
    {
      pthread_mutex_lock(&plock1);
      cout << "plock1 已经上锁,准备获取plock2锁 " << endl;
      pthread_mutex_lock(&plock2);
      cout << "plock2 已经上锁" << endl;

      pthread_mutex_unlock(&plock2);
      pthread_mutex_unlock(&plock1);
    }
}
void* fun2(void *arg){
    sleep(1);
    while (i < 100)
    {
      pthread_mutex_lock(&plock2);
      cout << "plock2 已经上锁,准备获取plock1锁" << endl;
      pthread_mutex_lock(&plock1);
      cout << "plock1 已经上锁" << endl;
      pthread_mutex_unlock(&plock2);
      pthread_mutex_unlock(&plock1);
    }
}
int main(){
    pthread_t pid[5];
    char *str1 = "str1 传入的参数内容";
    char *str2 = "str2 传入的参数内容";
    pthread_mutex_init(&plock2,NULL);
    pthread_mutex_init(&plock1,NULL);

    pthread_create(&pid[0],NULL,fun,str1);
    pthread_create(&pid[1],NULL,fun2,str2);
    // pthread_create(&pid[2],NULL,fun3,str2);

    void *tre,*tre1;
    pthread_join(pid[0],&tre);
    pthread_join(pid[1],&tre1);
    printf("%s \n",(char *)tre);
    printf("%s \n",(char *)tre1);
    while (i < 100)
    {
          cout << "i的值" << i++ << endl;
          sleep(2);
    }

    return 1;
}

!image-20220327204645817 plock1 和 plock2都 已经上锁,两个线程都在等待对方释放锁,才能进行下去,这种情况下双方都不会释放

读写锁( 共享互斥锁)

写操作的时候才会互斥,而在进行读的时候是可以共享的进行访问临界区的, 在写少读多的情况下用读写锁更好,为什么呢? 写的时候临界区加锁,这没毛病,读的时候并不需要进修改数据,多线程读数据并不影响数据内容,所以读的时候不用锁共享资源,也节省了系统的开销

读写锁的分配规则:

  1. 只有线程没有使用读写锁的写转态,那么任意数目的线程都可以使用读写锁
  2. 当有线程持有读写锁的写转态时,其他线程使用读写锁都会被阻塞
  3. 如果读写锁的读模式加锁时,所有线程以读模式它进行加锁的线程都可以得到访问权

做了实验 使用读写锁,一个线程写数据,两个线程读数据

写的时候,读线程全部被阻塞

读的时候,读线程全都可以获取资源,写线程被阻塞


 #include <bits/stdc++.h>
#include <unistd.h>
using namespace std;
pthread_mutex_t plock2;
pthread_mutex_t plock1;
pthread_rwlock_t rw_lock;
pthread_rwlock_t rwlock;

int i = 13;
//写数据
void* fun(void *arg){
    char *arg1 = (char *)arg;

    while (i < 100)
    {
        pthread_rwlock_wrlock(&rwlock);

        cout << "开启 写的锁后  i = " << i++  << " 并且i++"<< endl;
        cout << "此时所获取这个锁的线程都会被堵塞 3s" << endl;
        cout << endl;
        cout << endl;

        sleep(3);
        pthread_rwlock_unlock(&rwlock);
    }
}
//以下为读数据
void* fun2(void *arg){
    sleep(1);
    char *arg1 = (char *)arg;
    while (i < 100)
    {

        pthread_rwlock_rdlock(&rwlock);
        cout << " 我是 fun2  开启只读的锁后  i =" << i << endl;

        pthread_rwlock_unlock(&rwlock);

        sleep(1);
    }

}
void* fun3(void *arg){
    sleep(1);
    char *arg1 = (char *)arg;
    while (i < 100)
    {
        pthread_rwlock_rdlock(&rwlock);
        cout << " 我是 fun3  开启只读的锁后  i =" << i << endl;

        pthread_rwlock_unlock(&rwlock);

        sleep(1);
    }

}
int main(){

    pthread_t pid[5];
    char *str1 = "str1 传入的参数内容";
    char *str2 = "str2 传入的参数内容";
    pthread_rwlock_init(&rwlock,NULL);

    pthread_create(&pid[0],NULL,fun,str1);
    pthread_create(&pid[1],NULL,fun2,str1);
    pthread_create(&pid[2],NULL,fun3,str1);

    while (1)
    {
        sleep(1);
    }
    pthread_rwlock_destroy(&rwlock);
    return 1;
}

条件变量

条件变量也是同步线程的同步的方式,但还是得借助互斥锁; 当一个线程获取锁之后,他需要等待一定的条件才能继续执行,执行完成再释放锁,这个条件可能是其他线程计算的结果,等待条件的时候可以 一直判断这个条件是否成立(加个while)但这样太消耗CPU的资源了,资源会被一个线程都占用,使用条件变量可以 使线程进入等待转态,等待其他线程完后发送信号给条件变量,从而继续执行

自旋锁

与互斥锁类似,等待互斥锁时线程是阻塞的,而自旋锁是一直在询问这个锁是否可用(会占用CPU),所以自旋锁适用于锁持有时间短的场景,长时间占用自旋锁是很占用CPU资源的,使用自旋锁的话对于 竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞状态,等到持有锁的线程释放锁之后即可获取,这样就避免了用户进程和内核切换的消耗,也是自旋锁适用于所持有时间的场景的原因。

补充

以上加锁的目的保证数据/操作是原子性的,如果说锁里面的操作/数据是原子性的,没有锁也是可以的,就好比 i++,我们都知道这个并不是 原子性的操作,但是如果说用一个函数实现 i++,并且是要原子性的可以吗?有一些汇编指令就是可以原子操作,在函数里嵌入 汇编语言,用这个汇编来使用 i++的操作也是可以的;(其实c++ 11中的 atmoic 可以,对于atomic 以后再介绍)


int inc(int *value, int add) {
  int old;
  __asm__ volatile (
  "lock; xaddl %2, %1;" // "lock; xchg %2, %1, %3;"
    : "=a" (old)
    : "m" (*value), "a" (add)
    : "cc", "memory"
  );
  return old;
}

CAS(先留坑)

。。。。。。。。。。。。。。


评论