C++网络编程(二)并发控制

C++网络编程(二) 并发控制

前言

在网络编程中,要提高整体的运行效率,采用并发执行的方式对大量的数据交互是必不可少的。

线程

创建线程

在Linux系统上,有为Linux专属的线程库NPTL,对线程操作的各种函数则定义在<pthread.h>头文件中,此头文件仅在Linux操作系统下。有更加通用的<thread>头文件,但实质上也是对<pthread.h>的封装,如果编译器版本较低,使用thread库时编译同样需要加上-lphread链接参数。

pthread_create
int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void* (*start_routine)(void*), void* arg);
/*
thread: 新线程的标识符
attr: 设置新线程的属性,传递NULL值代表使用默认线程属性
start_routine: 线程将运行的函数
arg: 所运行函数的参数列表
*/

//pthread_t定义
#include <bits/pthreadtypes.h>
typedef unsigned long int pthread_t;

函数用于创建一个线程。pthread_create成功时返回0,失败时返回错误码。

一个用户可以打开的线程数量不能超过RLIMIT_NPROC软资源限制。此外,系统上所有用户能创建的线程总数也不得超过/proc/sys/kernel/threads-max内核参数所定义的值(一般为118176)

//线程创建示例
#include <iostream>
#include <pthread.h>
#include <cerrno>

using namespace std;

void *fun(void *id)
{
    cout << "id=" << *(int *)id << endl;
}

int main()
{
    pthread_t ptid;
    int id = 12345;
    int res = pthread_create(&ptid, NULL, fun, &id);
    if (res == 0)
    {
        cout << "线程创建成功,ptid=" << ptid << endl;
        pthread_join(ptid, NULL); //等待程序结束
    }
    else
        cout << "线程创建失败" << strerror(errno) << endl;
    return 0;
}

结束线程

多线程编程中,线程结束执行的方式有 3 种,分别是:

  1. 线程将指定函数体中的代码执行完后自行结束;
  2. 线程执行过程中,被同一进程中的其它线程(包括主线程)强制终止;
  3. 线程执行过程中,遇到 pthread_exit() 函数结束执行。
pthread_exit
void pthread_exit(void* retval);

线程函数在调用pthread_exit()后立即结束线程,不再执行之后的语句,同时函数会通过参数retval向线程的回收者传递其退出信息。

注:默认属性的线程执行结束后并不会立即释放占用的资源,直到整个进程执行结束,所有线程的资源以及整个进程占用的资源才会被操作系统回收。

//示例
#include <iostream>
#include <pthread.h>

using namespace std;

void *fun(void *id)
{
    cout << "id=" << *(int *)id << endl;
    int *ret = new int;
    *ret = 555555;
    pthread_exit(ret);
}

int main()
{
    pthread_t ptid;
    int id = 12345;
    int res = pthread_create(&ptid, NULL, fun, &id);
    if (res == 0)
    {
        cout << "线程创建成功,ptid=" << ptid << endl;
        void *ret = NULL;
        pthread_join(ptid, &ret); //等待程序结束并获取返回值
        if (ret != NULL)
        {
            cout << "retval=" << *(int *)ret << endl;
            delete ret;
        }
        else
            cout << "未获取到返回值" << endl;
    }
    else
        cout << "线程创建失败" << endl;
    return 0;
}
pthread_join

一个进程中的所有线程都可以调用pthread_join函数来回收其他线程(前提是目标线程是可回收的)即等待其他线程结束,这类似于回收进程的wait和waitpid系统调用。

int pthread_join(pthread_t thread, void** retval);
/*
thread: 目标线程的标识符
retval: 目标线程返回的退出信息
*/

该函数会一直阻塞,直到被回收的线程结束为止。该函数成功时返回0,失败则返回错误码。

错误码描述
EDEADLK可能引起了死锁,如两个线程互调对方pthread_join,或线程自身调用pthread_join
EINVAL目标线程不可回收,或已有其他线程在回收该目标线程
ESRCH目标线程不存在
pthread_cancel
int pthread_cancel(pthread_t thread);//thread为目标线程标识符

希望异常终止一个线程,即取消一个线程。该函数成功时返回0,失败则返回错误码。

接收到取消请求的目标线程可以决定是否允许被取消以及如何取消,这分别由如下两个函数完成:

int pthread_setcancelstate(int state, int* oldstate);
int pthread_setcanceltype(int type, int* oldtype);

这两个函数的第一个参数分别用于设置线程的取消状态(是否允许取消)和取消类型(如何取消),第二个参数则分别记录线程原来的取消状态和取消类型。

state参数有两个可选值:

  • PTHREAD_CANCEL_ENABLE,允许线程被取消。它是线程被创建时的默认取消状态。
  • PTHREAD_CANCEL_DISABLE,禁止线程被取消。这种情况下,如果一个线程收到取消请求,则它会将请求挂起,直到该线程允许被取消。

type参数也有两个可选值:

  • PTHREAD_CANCEL_ASYNCHRONOUS,线程随时都可以被取消。它将使得接收到取消请求的目标线程立即采取行动。
  • PTHREAD_CANCEL_DEFERRED,允许目标线程推迟行动,直到它调用了下面几个所谓的取消点函数中的一个:pthread_join、pthread_testcancel、pthread_cond_wait、pthread_cond_timedwait、sem_wait和sigwait。根据POSIX标准,其他可能阻塞的系统调用,比如read、wait,也可以成为取消点。不过为了安全起见,我们最好在可能会被取消的代码中调用pthread_testcancel函数以设置取消点。

线程属性

#include <bits/pthreadtypes.h>
#define __SIZEOF_PTHREAD_ATTR_T 36
typedef union
{
    char __size[__SIZEOF_PTHREAD_ATTR_T];
    long int __align;
}pthread_attr_t;//定义线程属性

各种线程属性全部包含在一个字符数组中。线程库定义了一系列函数来操作pthread_attr_t类型的变量,以方便我们获取和设置线程属性。这些函数包括:

#include <pthread.h>
/*初始化线程属性对象*/
int pthread_attr_init(pthread_attr_t* attr);
/*销毁线程属性对象。被销毁的线程属性对象只有再次初始化之后才能继续使用*/
int pthread_attr_destroy(pthread_attr_t* attr);
/*
获取与设置线程的脱离状态
detachstate有PTHREAD_CREATE_JOINABLE和PTHREAD_CREATE_DETACH两个可选值,
前者指定线程可以被回收,后者是调用线程脱离与进程中其他线程的同步。
脱离线程在退出时将自行释放其占用的系统资源。线程创建时该属性的默认值是PTHREAD_CREATE_JOINABLE
*/
int pthread_attr_getdetachstate(const pthread_attr_t* attr, int*detachstate);
int pthread_attr_setdetachstate(pthread_attr_t* attr, int detachstate);
//获取和设置线程堆栈段的起始地址
int pthread_attr_getstackaddr(const pthread_attr_t* attr, void**stackaddr);
int pthread_attr_setstackaddr(pthread_attr_t* attr, void* stackaddr);
//获取和设置线程堆栈段的内存大小,Linux默认分配是8MB
int pthread_attr_getstacksize(const pthread_attr_t* attr size_t* stacksize);
int pthread_attr_setstacksize(pthread_attr_t* attr, size_t stacksize);
//获取和设置线程堆栈段的起始地址与内存大小
int pthread_attr_getstack(const pthread_attr_t* attr, void** stackaddr, 
size_t* stacksize);
int pthread_attr_setstack(pthread_attr_t* attr, void* stackaddr, size_t stacksize);
//guardsize即保护区域大小,位于线程堆栈区域末尾
int pthread_attr_getguardsize(const pthread_attr_t* __attr, size_t* guardsize);
int pthread_attr_setguardsize(pthread_attr_t* attr, size_t guardsize);
//schedparm即线程调度参数,目前其结构体中成员仅有sched_priority表示运行优先级
int pthread_attr_getschedparam(const pthread_attr_t* attr, struct sched_param* param);
int pthread_attr_setschedparam(pthread_attr_t* attr, const struct sched_param* param);
/*
schedpolicy即线程调度策略
默认为SCHED_OTHER,分时调度算法
还可选SCHED_RR(轮转调度),SCHED_FIFO(先进先出)在su身份运行的进程中
*/
int pthread_attr_getschedpolicy(const pthread_attr_t* attr,int* policy);
int pthread_attr_setschedpolicy(pthread_attr_t* attr, int policy);
/*
inheritsched即是否继承线程的调度属性,有两个属性
PTHREAD_INHERIT_SCHED:表示新线程沿用其创建者的线程调度参数
PTHREAD_EXPLICIT_SCHED:表示调用者要明确指定新线程的调度参数
*/
int pthread_attr_getinheritsched(const pthread_attr_t* attr, int* inherit);
int pthread_attr_setinheritsched(pthread_attr_t* attr, int inherit);
/*
scope,线程间竞争CPU的返回,即线程优先级的范围
PTHREAD_SCOPE_SYSTEM:目标线程与系统中所有线程一起竞争CPU
PTHREAD_SCOPE_PROCESS:目标线程仅与其他隶属于同一进程的线程竞争CPU
*/
int pthread_attr_getscope(const pthread_attr_t* attr, int* scope);
int pthread_attr_setscope(pthread_attr_t* attr, int scope);

C++的<thread>

线程库<thread>是对<pthread.h>进行了封装。

线程创建与结束

std::thread t(fun,argu1,argu2,...); //fun之后的参数就是函数fun的参数,即使fun中的参数有默认值,也需要将该参数传入
t.join();  //主线程等待子线程运行结束后再继续运行,会阻塞主线程
t.detach(); //子线程与主线程分离运行,改变子线程的作用域,主线程结束不影响子线程继续运行

注意:线程必须调用join()或detach()之一的函数,否则在析构时会出现异常。

一些常用的函数

get_id():获取当前线程的id,返回值是std::thread::id类型

sleep_for(const std::chrono::milliseconds &__rtime);:使当前线程休眠一段时间,通过std::chrono中的函数来获取时间戳。例如传入的参数std::chrono::milliseconds(1000)表示休眠1000毫秒。

yield():让出该线程的cpu片段

sleep_until(const std::chrono::time_point<Clock,Duration>& sleep_time):使线程休眠,直到设定的时间点

POSIX信号量

POSIX信号量函数的名字都以sem_开头,并不像大多数线程函数那样以pthread_开头。常用的POSIX信号量函数是下面5个:

#include <semaphore.h>
/*初始化一个未命名的信号量,该函数不可用于初始化一个已被初始化的信号量
pshared:指定信号量类型,为0则是进程的局部信号量,非0则是可以在多个进程间共享
value:指定信号量初值
*/
int sem_init(sem_t* sem, int pshared, unsigned int value);
//销毁信号量,释放其占用的内核资源
int sem_destroy(sem_t* sem);
//原子操作的方式使信号量的值减一,如果信号量为0,则阻塞至非零
int sem_wait(sem_t* sem);
//原子操作的方式使信号量的值减一,非阻塞执行
int sem_trywait(sem_t* sem);
//原子操作的方式使信号量的值加一
int sem_post(sem_t* sem);

参数sem指向被操作的信号量。上述函数成功返回0,失败返回-1并且置errno。

互斥锁

互斥锁(也称互斥量)可以用于保护关键代码段,以确保其独占式的访问。即在被锁住代码段,仅有一个线程可以执行,其余线程阻塞。

基础API

主要函数:

#include <pthread.h>
/*初始化互斥锁
mutexattr指定互斥锁属性,设置为NULL表示使用默认属性
*/
int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutexattr_t* mutexattr);
//原子操作销毁一个互斥锁
int pthread_mutex_destroy(pthread_mutex_t* mutex);
//原子操作给一个互斥锁加锁,会阻塞
int pthread_mutex_lock(pthread_mutex_t* mutex);
//原子操作给一个互斥锁加锁,非阻塞
//已经上锁返回EBUSY
int pthread_mutex_trylock(pthread_mutex_t* mutex);
//原子操作给一个互斥锁解锁
int pthread_mutex_unlock(pthread_mutex_t* mutex);

参数mutex指向要操作的目标互斥锁,类型为pthread_mutex_t结构体。

初始化mutex还可以采用

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

来初始化一个互斥锁,实际效果为互斥锁的各个字段都初始化为0.

函数成功返回0,失败返回对应的errno错误码

互斥锁属性

pthread_mutexattr_t结构体定义了一套完整的互斥锁属性。线程库中有一系列的函数来操作它们。

#include <pthread.h>
//初始化互斥锁属性对象
int pthread_mutexattr_init(pthread_mutexattr_t* attr);
//销毁互斥锁属性对象
int pthread_mutexattr_destroy(pthread_mutexattr_t* attr);
/*获取和设置互斥锁的pshared属性,可选:
 PTHREAD_PROCESS_SHARED。互斥锁可以被跨进程共享。
 PTHREAD_PROCESS_PRIVATE。互斥锁只能被和锁的初始化线程隶属于同一个进程的线程共享
*/
int pthread_mutexattr_getpshared(const pthread_mutexattr_t* attr, int* pshared);
int pthread_mutexattr_setpshared(pthread_mutexattr_t* attr, int pshared);
//获取和设置互斥锁的type属性
int pthread_mutexattr_gettype(const pthread_mutexattr_t* attr, int* type);
int pthread_mutexattr_settype(pthread_mutexattr_t* attr, int type);

互斥锁属性type指定互斥锁的类型。Linux支持如下4种类型的互斥锁:

  • PTHREAD_MUTEX_NORMAL,普通锁。这是互斥锁默认的类型。当一个线程对一个普通锁加锁以后,其余请求该锁的线程将形成一个等待队列,并在该锁解锁后按优先级获得它。这种锁类型保证了资源分配的公平性。但这种锁也很容易引发问题:一个线程如果对一个已经加锁的普通锁再次加锁,将引发死锁;对一个已经被其他线程加锁的普通锁解锁,或者对一个已经解锁的普通锁再次解锁,将导致不可预期的后果。
  • PTHREAD_MUTEX_ERRORCHECK,检错锁。一个线程如果对一个已经加锁的检错锁再次加锁,则加锁操作返回EDEADLK。对一个已经被其他线程加锁的检错锁解锁,或者对一个已经解锁的检错锁再次解锁,则解锁操作返回EPERM。
  • PTHREAD_MUTEX_RECURSIVE,嵌套锁。这种锁允许一个线程在释放锁之前多次对它加锁而不发生死锁。不过其他线程如果要获得这个锁,则当前锁的拥有者必须执行相应次数的解锁操作。对一个已经被其他线程加锁的嵌套锁解锁,或者对一个已经解锁的嵌套锁再次解锁,则解锁操作返回EPERM。
  • PTHREAD_MUTEX_DEFAULT,默认锁。一个线程如果对一个已经加锁的默认锁再次加锁,或者对一个已经被其他线程加锁的默认锁解锁,或者对一个已经解锁的默认锁再次解锁,将导致不可预期的后果。这种锁在实现的时候可能被映射为上面三种锁之一。

C++<mutex>

//上锁
#include<mutex> //头文件
std::mutex mtx1; //创建一个mutex变量,可以理解为一个mutex变量就是一把锁
void fun()
{
    mtx1.lock(); //上锁,接下来代码中的变量会互斥
    //code....
    mtx1.unlock(); //解锁
}

有时候可能会出现上锁了,但是在解锁之前就结束了线程的运行。或者是在多把锁之间出现了死锁的现象。这些都是不安全的,因此通常不会直接使用mutex。

有一个更好的上锁方式是创建一个lock_guard类的对象,mutex作为参数传入其中。其构造函数即给mutex上锁,其析构函数是给mutex解锁。由此其上锁的范围也就确定了,稍显不够灵活。

void f1()
{
    for (int i = 0; i < 1000000; i++)
    {
        std::lock_guard<std::mutex> lock1(mtx1);
        n++, n--;
    }
}

出于灵活性考虑,有另一个unique_lock类对lock_guard的功能进行了扩充,提供了lock()和unlock()这两个函数进行控制,也有一些其他的类成员,此处不做扩展说明。unique_lock可以像lock_guard一样使用。

条件变量

条件变量提供了一种线程间的通知机制:当某个共享数据达到某个值的时候,唤醒等待这个共享数据的线程。

#include <pthread.h>
/*初始化条件变量
cond_attr:指定条件变量属性,类似于互斥锁的属性类型.设置为NULL则为默认
*/
int pthread_cond_init(pthread_cond_t* cond, const pthread_condattr_t* cond_attr);
/*
销毁条件变量,释放其占用的内核资源
销毁一个正在被等待的条件变量将失败并返回EBUSY
*/
int pthread_cond_destroy(pthread_cond_t* cond);
//以广播方式唤醒所有等待目标条件变量的线程
int pthread_cond_broadcast(pthread_cond_t* cond);
//唤醒一个等待目标条件变量的线程,被唤醒线程取决于其优先级与调度策略
int pthread_cond_signal(pthread_cond_t* cond);
//用于等待目标条件变量,参数mutex用于保护条件变量
int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex);

参数cond指向要操作的目标条件变量,类型是pthread_cond_t结构体。

初始化条件变量还可以采用

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

来初始化一个条件变量,实际效果为条件变量的各个字段都初始化为0.

在调用pthread_cond_wait前,必须确保互斥锁mutex已经加锁,否则将导致不可预期的结果。pthread_cond_wait函数执行时,首先把调用线程放入条件变量的等待队列中,然后将互斥锁mutex解锁。可见,从pthread_cond_wait开始执行到其调用线程被放入条件变量的等待队列之间的这段时间内,pthread_cond_signal和pthread_cond_broadcast等函数不会修改条件变量。换言之,pthread_cond_wait函数不会错过目标条件变量的任何变化。当pthread_cond_wait函数成功返回时,互斥锁mutex将再次被锁上

函数成功时返回0,失败则返回错误码。

C++<condition_variable>

条件变量能够阻塞一个或多个线程,使得在另一个线程中可以根据条件来进行唤醒。

#include <condition_variable>
#include <deque>
#include <mutex>
#include <thread>
using namespace std;

deque<int> dq;
mutex dqmtx;
condition_variable cv;

const int N = 1e5;

void productor()
{
    for (int i = 0; i < N; i++)
    {
        unique_lock<mutex> lock(dqmtx);
        dq.push_back(rand());
        cv.notify_one(); //唤醒一个阻塞的线程
    }
}

void consumer()
{
    for (int i = 0; i < N; i++)
    {
        unique_lock<mutex> lock(dqmtx);
        if (dq.size() == 0) //如果队列为空就阻塞等待
        //while (dq.size()==0)//虚假唤醒处理
            cv.wait(lock);//互斥锁解锁,唤醒后会自动上锁。
        dq.pop_front();
    }
}

int main()
{
    thread pt(productor);
    thread ct(consumer);
    pt.join();
    ct.join();
    return 0;
}

成员函数

waitwait_forwait_until:使当前线程进入阻塞。调用wait函数后会自动对传入的互斥锁解锁,当线程被唤醒后会自动上锁。

void wait( std::unique_lock<std::mutex>& lock );

notify_one:唤醒一个阻塞的线程
notify_all:唤醒所有阻塞的线程

虚假唤醒

当一次唤醒了多个线程,但只有一个线程是需要被唤醒的(比如只向队列中添加了一个元素,但是唤醒了多个消费者线程),就会造成虚假唤醒。解决的方法是把if改成while,当线程被唤醒时再次判断阻塞条件。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

registor11

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值