Linux(线程控制)

一 线程的操作

  1. 创建线程:pthread_create
cpp 复制代码
int pthread_create(
                     pthread_t *thread,                 // 线程 id
                     const pthread_attr_t *attr,        // 线程属性设置
                     void *(*start_routine) (void *),   // 回调函数
                     void *arg                          // 传递给回调函数的参数
                  );
// 返回值0成功,否则返回错误码
  1. 等待线程:pthread_join
cpp 复制代码
int pthread_join(
                    pthread_t thread,  // pthread_create 的返回值 
                    void **retval      // pthread_create 回调函数的返回值
                );
// 返回值成功0,否则返回错误码

和进程一样,线程执行完,也需要等待回收获取执行结果,否则类似僵尸进程。

示例:

cpp 复制代码
#include <iostream>
#include <pthread.h>

void* fun(void *arg)
{
	const char *s = static_cast<const char *>(arg);
	std::cout << s << std::endl;

	return (void *)"正常退出";
}
int main()
{
	pthread_t pid;
	pthread_create(&pid, nullptr, fun, (void *)"hello world");

	void *result;
	pthread_join(pid, &result);

	std::cout << static_cast<const char *>(result) << std::endl;
	return 0;
}

创建多线程,进程内部就有多个执行流,谁先执行不一定。

void* 可以接收任意类型参数,内置类型,自定义类型都可以。

二级指针存放回调函数的返回值(一级指针的地址)

  1. 线程终止
cpp 复制代码
#include <pthread.h>

void pthread_exit(
                    void *retval  // 终止后的信息
                 );
// 哪个线程调用终止哪个线程

如果不想正常return返回,可以调用 pthread_exit() ,提前终止,携带退出信息。

  1. 线程分离
cpp 复制代码
#include <pthread.h>

int pthread_detach(
                     pthread_t thread  // 线程的PID
                  );
// 让这个线程分离,此后不需要join

一般情况线程结束需要被等待,但可以自己分离出进程,但资源仍然共享,只是由系统来回收。

  1. 取消线程
cpp 复制代码
int pthread_cancel(
                     pthread_t thread // 线程ID
                  )
// 取消线程

可以主动取消一个线程,一般是由主线程来取消。

  1. 封装原生API(简易)
cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <functional>

// 处理任务
using handler = std::function<void(void)>;

class mythread
{
public:
    // 线程回调函数
    static void *fun(void *arg)
    {
        mythread *mythis = static_cast<mythread *>(arg);

        // 处理任务
        mythis->_handler();
        return nullptr;
    }

    mythread(const std::string &name, handler ha) : _name(name), _handler(ha), _isdetach(false) {}
    ~mythread() {}

    // 创建线程初始化tid
    bool start()
    {
        if (pthread_create(&_tid, nullptr, fun, this) != 0)
            return false;
        return true;
    }

    // 等待线程结束
    bool join()
    {
        if (_isdetach == true)
            return false;
        return pthread_join(_tid, nullptr) == 0;
    }

    // 分离线程
    bool setthread_detach()
    {
        _isdetach = true;
        return pthread_detach(_tid) == 0;
    }

    // 取消线程
    bool setthread_cancel()
    {
        return pthread_cancel(_tid) == 0;
    }

    pthread_t gettid() { return _tid; }

private:
    std::string _name; // 线程名
    pthread_t _tid;    // 线程tid
    handler _handler;  // 执行任务
    bool _isdetach;    // 分离线程
};

二 用户级线程

前章说过,Linux中的线程是通过pthread库对轻量级进程的封装,使用前必须携带 -lpthread 链接这个库。那么用户级线程是如何封装的?用户级线程包含哪些属性?下面来看看

既然是库,和标准库,第三方库一样,也要映射到共享区,pthread库也不例外。

当调用pthread_create(),pthread库会在内部维护一个用户级线程结构,并和其他相同的结构组织起来。

当获取线程的TID的时候,也就是pthread_create第一个参数,就是用户级线程维护的线程的起始地址,不是内核轻量级进程的LWP字段,所以在线程操作的时候,实际是在对pthread库操作,pthread库封装的轻量级进程,对库做操作,库帮你对轻量级进程做操作。

pthread维护的结构,包含很多字段:PID/LWP,回调函数/函数参数,void*退出信息,独立的栈,TLS线程局部存储.....等。

线程局部存储:线程也可以给自己创建独立的对象,维护在pthread结构中,但仅只支持内置类型,不支持自定义类型: __thread 类型。

独立的栈:进程里的栈由主线程使用,即没有创建线程的那个线程使用,其他的线程也是在pthread结构里独立开辟空间并维护自己的栈,也就不会起冲突了。

三 同步与互斥

如果在多执行流对同一个变量进行操作会有什么问题?

假设对一个变量执行 -- 操作,当减到0结束,那么if()判断和变量--都是操作,假设变量当前值为1,此时有多个执行流同时执行if(),if里的条件在内存中,先从内存拿到CPU,在由CPU执行,已经是2步操作了,当某个执行流对变量进行--操作,此时因某种原因被切换,比如:时间片到了,后面有优先级更高的....等,其他判断if()条件的执行流判断完成,此时if()内的语句已经有多条执行流了,但变量当前值为1,切走的线程又回来了进行--,后面的线程已经进来了,也会--,所以会有数据不一致问题,由并发访问导致数据不一致问题,称为线程安全问题。

上述根本原因是if()是由多种操作和切换等方面导致多执行流执行中有中间状态,称为非原子操作。

1. 概念:

原子性:执行流执行一段代码没有中间过程称为原子的,这段代码可能形成一行汇编语句,也可能是多行,只要没有中间状态,也就是原子的。

共享资源/临界资源:多执行流共享一个对象,对象身为共享资源,对共享资源保护的资源,称为临界资源。

临界区:临界资源的代码,称为临界区,其他称为非临界区。

2. 互斥量/互斥锁:

为了保证在多线程情况下,因为并发允许导致对共享资源修改造成的数据不一致问题,提供了很多互斥机制。

互斥:访问临界区的代码的时候,只有一个执行流能访问,其他等待,所以相对于其他线程,进入临界区的线程是原子的,由原来的并发,在临界区中变成了串行访问。

API:

cpp 复制代码
#include <pthread.h>

int pthread_mutex_init(
                         pthread_mutex_t *mutex,               // 锁 
                         const pthread_mutexattr_t *mutexattr  // 锁的属性
                      );

int pthread_mutex_trylock(
                            pthread_mutex_t *mutex // 申请锁失败返回
                         );


int pthread_mutex_destroy(
                            pthread_mutex_t *mutex // 释放锁
                         );

int pthread_mutex_lock(
                         pthread_mutex_t *mutex // 对锁进行加锁
                      );

int pthread_mutex_unlock(
                         pthread_mutex_t *mutex // 对锁进行解锁
                        );

如果锁是局部变量,则需要进行初始化和手动销毁。

全局对象则直接初始化,不用销毁:pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER。

互斥锁特征:如果申请成功就往下执行,否则阻塞,等待后续锁被解锁被唤醒重新申请锁。

所以互斥锁加锁期间就是原子的。

互斥锁实现:

当对锁进行加锁的时候,CPU存在一个汇编指令:swap,exchange,作用是交换内存中的值和寄存器中的值,整个下来只有一行汇编语句,也就是原子的,而之前的++/--操作而是3条汇编指令。

首先,CPU会初始化寄存器里的值为0,对应上图第一行代码,执行完这时如果被切走。

第一:内存中的值没变,其他线程可以继续申请,如果申请到,内存中的值变了,切走的线程再回来,恢复自己的寄存器的里的值:0,交换内存中的值,此时假设内存中原来的值为1,因切走被其他线程交换,变为0,此时0和0交换,在进行if()判断,走else 阻塞等待。

第二:执行第二行代码被切走,此时内存中的值由初始1被交换到寄存器,变为0,线程切换保存寄存器的值,后续线程的寄存器初始化为0,和内存中被交换后的值:0,0和0交换,if()不成立,else阻塞。

第三:解锁重新交换内存中的值和寄存器的值,如果被切走,其他线程已经阻塞,新的线程可以申请,没被切走唤醒阻塞的线程继续申请锁。

示例:

cpp 复制代码
#include "thread.hpp"
#include <vector>

// pthread_mutex_t mymtu = PTHREAD_MUTEX_INITIALIZER;
int val = 10000;
void xx(std::string s)
{
	while (1)
	{
		// pthread_mutex_lock(&mymtu);
		if (val > 0)
		{
			std::cout << s << " :" << val-- << std::endl;
			// pthread_mutex_unlock(&mymtu);
		}
		else
		{
			// pthread_mutex_unlock(&mymtu);
			break;
		}
	}
}
int main()
{
	std::vector<mythread> v;
	for (int i = 0; i < 5; i++)
		v.emplace_back(std::to_string(i), xx);

	for (auto &e : v)
		e.start();

	for (auto &e : v)
		e.join();
	return 0;
}

因显示器本就是共享资源所以打印信息混乱正常,可以看到3号线程打印的val是负数,不加保护必定存在线程安全问题,所以要加锁进行保护。

3 同步/条件变量:

上面说的互斥只是让临界区只有一个线程可以访问。

同步:多个线程访问临界区有一定的顺序。

明显互斥也有顺序,但解锁的线程和阻塞的线程状态不一样,解锁的线程解锁完可以立即去申请锁,而阻塞的线程要先唤醒再去申请锁,这样就导致了一个线程解锁之后又能申请到锁,而后面的锁一直申请不到,导致的问题就是后面的线程干等,线程饥饿问题,也就是同步,但资源竞争不合理。

所以为了让资源竞争合理,又引入了一个锁,条件变量。

API:

cpp 复制代码
// 全局不需要释放
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

// 初始化条件变量
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);

// 唤醒一个线程
int pthread_cond_signal(pthread_cond_t *cond);

// 唤醒全部线程
int pthread_cond_broadcast(pthread_cond_t *cond);

// 线程挂到条件变量中
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

// 释放条件变量
int pthread_cond_destroy(pthread_cond_t *cond);

使用和互斥锁差不多,条件变量作用就是让线程竞争资源具有有合理性,如果资源不就绪就挂到条件变量里(队列里),唤醒依次从队列头部取一个,也就保证了每个线程能合理竞争到资源。

当调用pthread_cond_wait()的时候,会释放互斥锁,当唤醒的时候,会弹出一个线程,并重新申请锁。

示例:

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>

pthread_mutex_t mymtu=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t mycond=PTHREAD_COND_INITIALIZER;

int val=0;
void* fun(void* arg)
{
    while(1)
    {
        pthread_mutex_lock(&mymtu);
        
        pthread_cond_wait(&mycond,&mymtu);
        std::cout<<static_cast<const char*>(arg)<<std::endl;
        
        pthread_mutex_unlock(&mymtu);
    }

    return nullptr;
}
int main()
{
    pthread_t t1,t2,t3;
    pthread_create(&t1,nullptr,fun,(void*)"thread-1");
    pthread_create(&t2,nullptr,fun,(void*)"thread-2");
    pthread_create(&t3,nullptr,fun,(void*)"thread-3");
 
    while(1)
    {
        std::cout<<"wake up"<<std::endl;
        pthread_cond_signal(&mycond);
     // pthread_cond_broadcast(&mycond);
        sleep(1);
    }
 
    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);
    pthread_join(t3,nullptr);
    return 0;
}

当加入条件变量控制线程资源的竞争,明显具有一定的顺序性,也就是同步。

相关推荐
多多*44 分钟前
LUA+Reids实现库存秒杀预扣减 记录流水 以及自己的思考
linux·开发语言·redis·python·bootstrap·lua
何双新2 小时前
第21讲、Odoo 18 配置机制详解
linux·python·开源
21号 12 小时前
9.进程间通信
linux·运维·服务器
Gaoithe7 小时前
ubuntu 端口复用
linux·运维·ubuntu
德先生&赛先生8 小时前
Linux编程:1、文件编程
linux
程序猿小D8 小时前
第16节 Node.js 文件系统
linux·服务器·前端·node.js·编辑器·vim
多多*9 小时前
微服务网关SpringCloudGateway+SaToken鉴权
linux·开发语言·redis·python·sql·log4j·bootstrap
IT界小黑的对象11 小时前
virtualBox部署ubuntu22.04虚拟机 NAT+host only 宿主机ping不通虚拟机
linux·运维·服务器
SilentCodeY11 小时前
Ubuntu 系统通过防火墙管控 Docker 容器
linux·安全·ubuntu·系统防火墙
weixin_5275504011 小时前
Linux 环境下高效视频切帧的实用指南
linux·运维·音视频