C++ 多线程

多线程

https://blog.csdn.net/zhangye3017/article/details/80367223
参考多篇,文中附链接,主要转自https://www.ibm.com/developerworks/cn/linux/1412_zhupx_thread/

线程基本概念

线程,有时称为轻量级(轻负荷)进程,因为切换负荷小,是CPU使用的基本单元(操作系统以进程为单位执行任务),是进程中的一个执行控制单元,执行路径;它由线程ID、程序计数器、寄存器集合和堆栈组成。它与属于同一进程的其他线程共享其代码段、数据段和其他操作系统资源(如打开文件和信号)。维基百科对线程的定义是:线程是一个编排好的指令序列,这个指令序列(线程)可以和其它的指令序列(线程)并行执行,操作系统调度器将线程作为最小的 CPU 调度单元 (内核级线程,用户级线程不能这样说,不受CPU调度)。在进行架构设计时,我们应该多从操作系统线程调度的角度去考虑应用程序的线程安排,而不仅仅是代码。在LINUX中,线程是最小的执行单位,进程是最小的分配资源单位。

线程状态:新生状态、可运行状态、被阻塞状态、死亡状态
新建:new创建线程对象后,处于新建状态,仅被分配了内存。也是等待状态
就绪:对线程对象调用start()之后,线程处于就绪状态,此时线程可能并没有运行,要等待CPU时间片,等待系统调度,等待系统资源。即处于可运行池中,等待CPU使用权。只有处于就绪状态的线程才有机会转到运行状态
运行:处于这个状态的线程占用CPU,执行程序代码。在并发运行环境中,如果计算机只有一个CPU,那么任何时刻只会有一个线程处于这个状态。
阻塞:阻塞状态是指线程因为某些原因放弃CPU,暂时停止运行。当线程处于阻塞状态时,Java虚拟机不会给线程分配CPU,直到线程重新进入就绪状态,它才会有机会获得运行状态。
阻塞状态分为三种:
1、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
2、同步阻塞:运行的线程在
获取对象同步锁时,若该同步锁被别的线程占用
,则JVM会把线程放入锁池中。
3、其他阻塞:运行的线程执行Sleep()方法,或者发出I/O请求时,JVM会把线程设为阻塞状态。当Sleep()状态超时、或者I/O处理完毕时,线程重新转入就绪状态。
死亡:当线程执行完run()方法中的代码,或者遇到了未捕获的异常,就会退出run()方法,此时就进入死亡状态,该线程结束生命周期。
在这里插入图片描述

线程与进程的区别

一个进程有一个或多个线程。线程更细化于进程,使得多线程程序的并发性高。进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。

系统在运行的时候会为每个进程分配不同的内存区域,但是不会为线程分配内存(线程所使用的资源是它所属的进程的资源),线程组只能共享资源。那就是说,除了CPU之外(线程在运行的时候要占用CPU资源),计算机内部的软硬件资源的分配与线程无关,线程只能共享它所属进程的资源进程是系统所有资源分配时候的一个基本单位,拥有一个完整的虚拟空间地址,并不依赖线程而独立存在。

线程在执行过程中与进程的区别在于每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用来实现进程的调度和管理以及资源分配。一个没有线程的进程是可以被看作单线程的,如果一个进程内拥有多个进程,进程的执行过程不是一条线(线程)的,而是多条线(线程)共同完成的。

(原文:https://blog.csdn.net/maoyuanming0806/article/details/78019044
版权声明:本文为博主原创文章,转载请附上博文链接!)
在这里插入图片描述
多线程本质上是单个 CPU 的时间分片,一个时间片运行一个线程的代码,它可以支持并发处理,但是不能说是真正的并行计算。
当有多个 CPU 或者多个内核可供调度时,可以做到真正的并行计算,多个线程的运行示意如下:
在这里插入图片描述
需要注意一点,因为单个 CPU 内核下多个线程并不是真正的并行,有些问题,比如 CPU 缓存不一致问题,不一定能表现出来,一旦这些代码被放到了多核或者多 CPU 的环境运行,就很可能会出现“在开发测试环境一切没有问题,到了实施现场就莫名其妙”的情况,所以,在进行多线程开发时,开发与测试环境应该是多核或者多 CPU 的,以避免出现这类情况。

多线程的实现与同步

面试题:实现方法有哪几种?同步方法有哪几种?(JAVA)
方法一:继承Thread类,重写方法run();
方法二:实现Runnable接口,实现方法run();
实现同步有几种方法:例如:synchronized,wait,notify都可以实现同步。

基于windows.h的实现与同步:
Windows API,MFC(微软基础类库),Visual C++运行期库函数
CreateThread,AfxBeginThread,_beginthread/beginthreadex
CreateThread是Windows API函数,提供操作系统级别操作,不用于MFC及RTL函数中。AfxBeginThread是MFC创建线程函数,首先创建了相应的CWinThread对象,然后调用CWinThread::CreateThread,CWinThread::CreateThread中,完成了对线程对象的初始化工作。AfxBeginThread()是MFC封装的启动线程的函数,里面包含了很多和MFC相关的启动信息,而且封装了一些常用的操作。_beginthreadex()函数是CRun-timeLibrary中的函数,函数的参数和数据类型都是CRun-timeLibrary中的类型。
https://blog.csdn.net/u011028345/article/details/73440873

C++ 11新特性:C++ 11标准库内部包裹了pthread库,因此,编译程序的时候需要加上-lpthread连接选项。

#include <thread>
#include <condition_variable>
#include <mutex>

C++11 的标准类 std::thread 对线程进行了封装,它的声明放在头文件 thread 中,其中声明了线程类 thread, 线程标识符 id,以及名字空间 this_thread,按照 C++11 规范,这个头文件至少应该兼容如下内容:

thread头文件中的主要内容

namespace std{
	struct thread{
        // native_handle_type 是连接 thread 类和操作系统 SDK API 之间的桥梁。
        typedef implementation-dependent native_handle_type;
        native_handle_type native_handle();
        //
        struct id{
            id() noexcept;
            // 可以由==, < 两个运算衍生出其它大小关系运算。
            bool operator==(thread::id x, thread::id y) noexcept;
            bool operator<(thread::id x, thread::id y) noexcept;
            template<class charT, class traits>
            basic_ostream<charT, traits>&
            operator<<(basic_ostream<charT, traits>&out, thread::id id);
            // 哈希函数
            template <class T> struct hash;
            template <> struct hash<thread::id>;
        };
        id get_id() const noexcept;
        // 构造与析构
        thread() noexcept;
        template<class F, class… Args> explicit thread(F&f, Args&&… args);
        ~thread();
        thread(const thread&) = delete;
        thread(thread&&) noexcept;
        thread& operator=( const thread&) = delete;
        thread& operator=(thread&&) noexcept;
        //
        void swap(thread&) noexcept;
        bool joinable() const noexcept;
        void join();
        void detach();
        // 获取物理线程数目
        static unsigned hardware_concurrency() noexcept;
    }
    namespace this_thead{
        thread::id get_id();
        void yield();
        template<class Clock, class Duration>
        void sleep_until(const chrono::time_point<Clock, Duration>& abs_time);
        template<class Rep, class Period>
        void sleep_for(const chromo::duration<Rep, Period>& rel_time);
    }
}

C++11 所定义的线程是和操作系的线程是一一对应的,也就是说我们生成的线程都是直接接受操作系统的调度的,通过操作系统的相关命令(比如 ps -M 命令)是可以看到的,一个进程所能创建的线程数目以及一个操作系统所能创建的总的线程数目等都由运行时操作系统限定
???native_handle_type 是连接 thread 类和操作系统 SDK API 之间的桥梁,在 g++(libstdc++) for Linux 里面,native_handle_type 其实就是 pthread 里面的 pthread_t 类型,当 thread 类的功能不能满足我们的要求的时候(比如改变某个线程的优先级),可以通过 thread 类实例的 native_handle() 返回值作为参数来调用相关的 pthread 函数达到目的。

thread::id 定义了在运行时操作系统内唯一能够标识该线程的标识符,同时其值还能指示所标识的线程的状态:1.其默认值 (thread::id()) 表示不存在可控的正在执行的线程(即空线程,比如,调用 thead() 生成的没有指定入口函数的线程类实例)2.当一个线程类实例的 get_id() 等于默认值的时候,即 get_id() == thread::id(),表示这个线程类实例处于下述状态之一:尚未指定运行的任务;线程运行完毕;线程已经被转移 (move) 到另外一个线程类实例;线程已经被分离 (detached)。空线程 id 字符串表示形式依具体实现而定,有些编译器为 0x0,有些为一句语义解释。

有时候我们需要在线程执行代码里面对当前调用者线程进行操作,针对这种情况,C++11 里面专门定义了一个名字空间 this_thread,其中包括 get_id() 函数可用来获取当前调用者线程的 id,yield() 函数可以用来将调用者线程跳出运行状态,重新交给操作系统进行调度,sleep_untilsleep_for 函数则可以让调用者线程休眠若干时间。get_id() 函数实际上是通过调用 pthread_self() 函数获得调用者线程的标识符,而 yield() 函数则是通过调用操作系统 API sched_yield() 进行调度切换。

C++11 实现

创建和结束

和 pthread_create 不同,使用 thread 类创建线程可以使用一个函数作为入口,也可以是其它的 Callable 对象,而且,可以给入口传入任意个数任意类型的参数:

int funcReturnInt(const char* fmt, ...){
 va_list ap;
 va_start(ap, fmt);
 vprintf( fmt, ap );
 va_end(ap);
 return 0xabcd;
}
void threadRunFunction(void){
 thread* t = new thread(funcReturnInt, "%d%s\n", 100, "\%");
 t->join();
 delete t;
}

也可以传入一个 Lambda 表达式作为入口:

void threadRunLambda(void){
 int a = 100,
 b = 200;
 thread* t = new thread( [](int ia, int ib){
 cout << (ia + ib) << endl;
 },
 a,
 b );
 t->join();
 delete t;
}

一个类的成员函数也可以作为线程入口:

struct God{
 void create(const char* anything){
 cout << "create " << anything << endl;
 }
};
void threadRunMemberFunction(void){
 God god;
 thread* t = new thread( &God::create, god, "the world" );
 t->join();
 delete t;
}

虽然 thread 类的初始化可以提供这么丰富和方便的形式,其实现的底层依然是创建一个 pthread 线程并运行之,有些实现甚至是直接调用 pthread_create 来创建。
创建一个线程之后,我们还需要考虑一个问题:该如何处理这个线程的结束?1.一种方式是等待这个线程结束,在一个合适的地方调用 thread 实例的 join() 方法,调用者线程将会一直等待着目标线程的结束,当目标线程结束之后调用者线程继续运行;2.另一个方式是将这个线程分离,由其自己结束,通过调用 thread 实例的 detach() 方法将目标线程置于分离模式。一个线程的 join() 方法与 detach() 方法只能调用一次,不能在调用了 join() 之后又调用 detach(),也不能在调用 detach() 之后又调用 join()在调用了 join() 或者 detach() 之后,该线程的 id 即被置为默认值(空线程),表示不能继续再对该线程作修改变化。如果没有调用 join() 或者 detach(),那么,在析构的时候,该线程实例将会调用 std::terminate(),这会导致整个进程退出,所以,如果没有特别需要,一般都建议在生成子线程后调用其 join() 方法等待其退出,这样子最起码知道这些子线程在什么时候已经确保结束。
在 C++11 里面没有提供 kill 掉某个线程的能力,只能被动地等待某个线程的自然结束,如果我们要主动停止某个线程的话,可以通过调用 Linux 操作系统提供的 pthread_kill 函数给目标线程发送信号来实现,示例如下:

static void on_signal_term(int sig){
 cout << "on SIGTERM:" << this_thread::get_id() << endl;
 pthread_exit(NULL); 
}
void threadPosixKill(void){
 signal(SIGTERM, on_signal_term);
 thread* t = new thread( [](){
 while(true){
 ++counter;
 }
 });
 // ???
 pthread_t tid = t->native_handle();
 cout << "tid=" << tid << endl;
 // 确保子线程已经在运行。
 this_thread::sleep_for( chrono::seconds(1) );
 pthread_kill(tid, SIGTERM);
 // ???
 t->join();
 delete t;
 cout << "thread destroyed." << endl;
}

上述例子还可以用来给某个线程发送其它信号,具体的 pthread_exit 函数调用的约定依赖于具体的操作系统的实现,所以,这个方法是依赖于具体的操作系统的,而且,因为在 C++11 里面没有这方面的具体约定,用这种方式也是依赖于 C++编译器的具体实现的。

转移或互换

thread 类是一个特殊的类,它不能被拷贝,只能被转移或者互换,这是符合线程的语义的,不要忘记这里所说的线程是直接被操作系统调度的。线程的转移使用 move 函数:

void threadMove(void){
 int a = 1;
 thread t( [](int* pa){
 for(;;){
 *pa = (*pa * 33) % 0x7fffffff;
 if ( ( (*pa) >> 30) & 1) break;
 }
 }, &a);
 thread t2 = move(t);	// 改为 t2 = t 将不能编译。
 t2.join();
 cout << "a=" << a << endl;
}

上述例子中,如果将 t2.join() 改为 t.join() 将会导致整个进程被结束,因为忘记了调用 t2 也就是被转移的线程的 join() 方法,从而导致整个进程被结束,而 t 则因为已经被转移,其 id 已被置空。
线程实例互换使用 swap 函数,示例如下:

void threadSwap(void){
 int a = 1;
 thread t( [](int* pa){
 for(;;){
 *pa = (*pa * 33) % 0x7fffffff;
 if ( ( (*pa) >> 30) & 1) break;
 }
 }, &a);
 thread t2;
 cout << "before swap: t=" << t.get_id() 
 << ", t2=" << t2.get_id() << endl;
 swap(t, t2);
 cout << "after swap : t=" << t.get_id() 
 << ", t2=" << t2.get_id() << endl;
 t2.join();
 cout << "a=" << a << endl;
}

互换和转移很类似,但是互换仅仅进行实例(以 id 作标识)的互换,而转移则在进行实例标识的互换之前,还进行了转移目的实例(如下例的t2)的清理,如果 t2 是可聚合的(joinable() 方法返回 true),则调用 std::terminate(),这会导致整个进程退出,比如下面这个例子:

void threadMoveTerm(void){
 int a = 1;
 thread t( [](int* pa){
 for(;;){
 *pa = (*pa * 33) % 0x7fffffff;
 if ( ( (*pa) >> 30) & 1) break;
 }
 }, &a);
 thread t2( [](){
 int i = 0;
 for(;;)i++;
 } );
 t2 = move(t);	// 将会导致 std::terminate()
 cout << "should not reach here" << endl;
 t2.join();
}

在进行线程实例转移的时候,要注意判断目的实例的 id 是否为空值(即 id())。

如果我们继承了 thread 类,则还需要禁止拷贝构造函数、拷贝赋值函数以及赋值操作符重载函数等,另外,thread 类的析构函数并不是虚析构函数。示例如下:

class MyThread : public thread{
public:
 MyThread() noexcept : thread(){};
 template<typename Callable, typename... Args>
 explicit
 MyThread(Callable&& func, Args&&... args) : 
 thread( std::forward<Callable>(func), 
 std::forward<Args>(args)...){
 }
 ~MyThread() { thread::~thread(); }
 // disable copy constructors
 MyThread( MyThread& ) = delete;
 MyThread( const MyThread& ) = delete;
 MyThread& operator=(const MyThread&) = delete;
};

因为 thread 类的析构函数不是虚析构函数,在上例中,需要避免出现下面这种情况:

MyThread* tc = new MyThread(…);

thread* tp = tc; // 主要是因为这句导致tp没法析构吧?

delete tp;

这种情况会导致 MyThread 的析构函数没有被调用。

调度

我们可以调用 this_thread::yield() 将当前调用者线程切换到重新等待调度,但是不能对非调用者线程进行调度切换,也不能让非调用者线程休眠(这是操作系统调度器干的活)。

void threadYield(void){
 unsigned int procs = thread::hardware_concurrency(), // 获取物理线程数目
 i = 0;
 thread* ta = new thread( [](){
 struct timeval t1, t2;
 gettimeofday(&t1, NULL);
 for(int i = 0, m = 13; i < COUNT; i++, m *= 17){
 this_thread::yield();
 }
 gettimeofday(&t2, NULL);
 print_time(t1, t2, " with yield");
 } );
 thread** tb = new thread*[ procs ]; // 指针数组,数组中每个元素是一个thread*
 for( i = 0; i < procs; i++){
 tb[i] = new thread( [](){
 struct timeval t1, t2;
 gettimeofday(&t1, NULL);
 for(int i = 0, m = 13; i < COUNT; i++, m *= 17){
 do_nothing();
 }
 gettimeofday(&t2, NULL);
 print_time(t1, t2, "without yield");
 });
 }
 ta->join();
 delete ta;
 for( i = 0; i < procs; i++){
 tb[i]->join();
 delete tb[i];
 };
 delete tb;
}

ta 线程因为需要经常切换去重新等待调度,它运行的时间要比 tb 要多,比如在作者的机器上运行得到如下结果:

$time ./a.out
without yield elapse 0.050199s
without yield elapse 0.051042s
without yield elapse 0.05139s
without yield elapse 0.048782s
with yield elapse 1.63366s
real 0m1.643s
user 0m1.175s
sys 0m0.611s
ta 线程即使扣除系统调用运行时间 0.611s 之后,它的运行时间也远大于没有进行切换的线程。

C++11 没有提供调整线程的调度策略或者优先级的能力,如果需要,只能通过调用相关的 pthread 函数来进行,需要的时候,可以通过调用 thread 类实例的 native_handle() 方法或者操作系统 API pthread_self() 来获得 pthread 线程 id,作为 pthread 函数的参数。

线程间数据交互与争用

同一个进程内的多个线程之间多是免不了要有数据互相来往的,队列共享数据是实现多个线程之间的数据交互的常用方式,封装好的队列使用起来相对来说不容易出错一些,而共享数据则是最基本的也是较容易出错的,因为它会产生数据争用的情况,即有超过一个线程试图同时抢占某个资源,比如对某块内存进行读写等,如下例所示:

static void
inc(int *p ){
 for(int i = 0; i < COUNT; i++){
 (*p)++;
 }
}
void threadDataRacing(void){
 int a = 0;
 thread ta( inc, &a);
 thread tb( inc, &a);
 ta.join();
 tb.join();
 cout << "a=" << a << endl;
}

这是简化了的极端情况,我们可以一眼看出来这是两个线程在同时对&a 这个内存地址进行写操作,但是在实际工作中,在代码的海洋中发现它并不一定容易。从表面看,两个线程执行完之后,最后的 a 值应该是 COUNT * 2,但是实际上并非如此,因为简单如 (*p)++这样的操作并不是一个原子动作,要解决这个问题,对于简单的基本类型数据如字符、整型、指针等,C++提供了原子模版类 atomic,而对于复杂的对象,则提供了最常用的锁机制,比如互斥类 mutex,门锁 lock_guard,唯一锁 unique_lock,条件变量 condition_variable 等
使用原子模版类 atomic 改造上述例子得到预期结果:

static void
inc(atomic<int> *p ){
 for(int i = 0; i < COUNT; i++){
 (*p)++;
 }
}
void threadDataRacing(void){
 atomic<int> a(0) ;
 thread ta( inc, &a);
 thread tb( inc, &a);
 ta.join();
 tb.join();
 cout << "a=" << a << endl;
}

也可以使用 lock_guard,lock_guard 是一个范围锁,本质是 RAII(Resource Acquire Is Initialization),在构建的时候自动加锁,在析构的时候自动解锁,这保证了每一次加锁都会得到解锁。即使是调用函数发生了异常,在清理栈帧的时候也会调用它的析构函数得到解锁,从而保证每次加锁都会解锁,但是我们不能手工调用加锁方法或者解锁方法来进行更加精细的资源占用管理,使用 lock_guard 示例如下:

static mutex g_mutex;
static void
inc(int *p ){
 for(int i = 0; i < COUNT; i++){
 lock_guard<mutex> _(g_mutex);
 (*p)++;
 }
}
void threadLockGuard(void){
 int a = 0;
 thread ta( inc, &a);
 thread tb( inc, &a);
 ta.join();
 tb.join();
 cout << "a=" << a << endl;
}

如果要支持手工加锁,可以考虑使用 unique_lock 或者直接使用 mutex。unique_lock 也支持 RAII,它也可以一次性将多个锁加锁;如果使用 mutex 则直接调用 mutex 类的 lock, unlock, trylock 等方法进行更加精细的锁管理

static mutex g_mutex;
static void
inc(int *p ){
 thread_local int i; // TLS 变量
 for(; i < COUNT; i++){
 g_mutex.lock();
 (*p)++;
 g_mutex.unlock();
 }
}
void threadMutex(void){
 int a = 0;
 thread ta( inc, &a);
 thread tb( inc, &a);
 ta.join();
 tb.join();
 cout << "a=" << a << endl;
}

在上例中,我们还使用了线程本地存储 (TLS) 变量,我们只需要在变量前面声明它是 thread_local 即可。TLS 变量在线程栈内分配,线程栈只有在线程创建之后才生效,在线程退出的时候销毁,需要注意不同系统的线程栈的大小是不同的,如果 TLS 变量占用空间比较大,需要注意这个问题。TLS 变量一般不能跨线程,其初始化在调用线程第一次使用这个变量时进行,默认初始化为 0。

对于线程间的事件通知,C++11 提供了条件变量类 condition_variable,可视为 pthread_cond_t 的封装,使用条件变量可以让一个线程等待其它线程的通知 (wait,wait_for,wait_until),也可以给其它线程发送通知 (notify_one,notify_all)条件变量必须和锁配合使用,在等待时因为有解锁和重新加锁,所以,在等待时必须使用可以手工解锁和加锁的锁,比如 unique_lock,而不能使用 lock_guard,示例如下:

#include <thread>
#include <iostream>
#include <condition_variable>
using namespace std;
mutex m;
condition_variable cv;
void threadCondVar(void){
# define THREAD_COUNT 10
 thread** t = new thread*[THREAD_COUNT];
 int i;
 for(i = 0; i < THREAD_COUNT; i++){
 t[i] = new thread( [](int index){
 unique_lock<mutex> lck(m); // 等待时必须使用可手工解锁加锁的锁
 cv.wait_for(lck, chrono::hours(1000)); 
 cout << index << endl;
 }, i );
 this_thread::sleep_for( chrono::milliseconds(50));
 }
 for(i = 0; i < THREAD_COUNT; i++){
 lock_guard<mutex> _(m); // 注意该锁的写法
 cv.notify_one(); // 唤醒
 }
 for(i = 0; i < THREAD_COUNT; i++){
 t[i]->join();
 delete t[i];
 }
 delete t;
}

从上例的运行结果也可以看到,条件变量是不保证次序的,即首先调用 wait 的不一定首先被唤醒。

C++11 中多线程编程的高级概念

C++11 提供了若干多线程编程的高级概念:promise/future, packaged_task, async,来简化多线程编程,尤其是线程之间的数据交互比较简单的情况下,让我们可以将注意力更多地放在业务处理上。

promise/future 可以用来在线程之间进行简单的数据交互,而不需要考虑锁的问题,线程 A 将数据保存在一个 promise 变量中,另外一个线程 B 可以通过这个 promise 变量的 get_future() 获取其值,当线程 A 尚未在 promise 变量中赋值时,线程 B 也可以等待这个 promise 变量的赋值:

promise<string> val;
static void
threadPromiseFuture(){
 thread ta([](){
 future<string> fu = val.get_future();
 cout << "waiting promise->future" << endl;
 cout << fu.get() << endl;
 });
 thread tb([](){
 this_thread::sleep_for( chrono::milliseconds(100) );
 val.set_value("promise is set");
 });
 ta.join();
 tb.join();
}

一个 future 变量只能调用一次 get(),如果需要多次调用 get(),可以使用 shared_future,通过 promise/future 还可以在线程之间传递异常

以下两个例子没太掌握!!!!!!

如果将一个 callable 对象和一个 promise 组合,那就是 packaged_task,它可以进一步简化操作:

static mutex g_mutex;
static void
threadPackagedTask(){
 auto run = [=](int index){  // ???
 {
 lock_guard<mutex> _(g_mutex);
 cout << "tasklet " << index << endl;
 }
 this_thread::sleep_for( chrono::seconds(10) );
 return index * 1000;
 };
 packaged_task<int(int)> pt1(run); // ???
 packaged_task<int(int)> pt2(run);
 thread t1([&](){pt1(2);} ); // ???
 thread t2([&](){pt2(3);} );
 int f1 = pt1.get_future().get();
 int f2 = pt2.get_future().get();
 cout << "task result=" << f1 << endl;
 cout << "task result=" << f2 << endl;
 t1.join();
 t2.join();
}

还可以试图将一个 packaged_task 和一个线程组合,那就是 async() 函数。使用 async() 函数启动执行代码,返回一个 future 对象来保存代码返回值,不需要我们显式地创建和销毁线程等,而是由 C++11 库的实现决定何时创建和销毁线程,以及创建几个线程等,示例如下:

static long
do_sum(vector<long> *arr, size_t start, size_t count){
 static mutex _m;
 long sum = 0;
 for(size_t i = 0; i < count; i++){
 sum += (*arr)[start + i];
 }
 {
 lock_guard<mutex> _(_m);
 cout << "thread " << this_thread::get_id() 
 << ", count=" << count
 << ", sum=" << sum << endl;
 }
 return sum;
}
static void
threadAsync(){
# define COUNT 1000000
 vector<long> data(COUNT);
 for(size_t i = 0; i < COUNT; i++){
 data[i] = random() & 0xff;
 }
 //
 vector< future<long> > result;
 size_t ptc = thread::hardware_concurrency() * 2;
 for(size_t batch = 0; batch < ptc; batch++){
 size_t batch_each = COUNT / ptc;
 if (batch == ptc - 1){
 batch_each = COUNT - (COUNT / ptc * batch);
 }
 result.push_back(async(do_sum, &data, batch * batch_each, batch_each));
 }
 long total = 0;
 for(size_t batch = 0; batch < ptc; batch++){
 total += result[batch].get();
 }
 cout << "total=" << total << endl;
}

如果是在多核或者多 CPU 的环境上面运行上述例子,仔细观察输出结果,可能会发现有些线程 ID 是重复的,这说明重复使用了线程,也就是说,通过使用 async() 还可达到一些线程池的功能。

注意事项

当我们的确需要不同线程访问一个共同的资源时,一般都需要进行加锁保护,否则很可能会出现数据不一致的情况,从而出现各种时现时不现的莫名其妙的问题,加锁保护时有几个问题需要特别注意:一是一个线程内连续多次调用非递归锁 (non-recursive lock) 的加锁动作,这很可能会导致异常。要根据场景调用合适的锁即可,当我们可能会在某个线程内重复调用某个锁的加锁动作时,我们应该使用递归锁 (recursive lock),在 C++11 中,可以根据需要来使用 recursive_mutex,或者 recursive_timed_mutex。;二是加锁的粒度。原则上应该是粒度越小越好,那意味着阻塞的时间越少,效率更高,比如一个数据库,给一个数据行 (data row) 加锁当然比给一个表 (table) 加锁要高效,但是同时复杂度也会越大,越容易出错,比如死锁等。;三是出现死锁 (deadlock),多个线程互相等待对方释放锁导致这些线程全部处于罢工状态。先看下出现死锁的条件:1.资源互斥,某个资源在某一时刻只能被一个线程持有 (hold);2.吃着碗里的还看着锅里的,持有一个以上的互斥资源的线程在等待被其它进程持有的互斥资源;3.不可抢占,只有在某互斥资源的持有线程释放了该资源之后,其它线程才能去持有该资源;。4.环形等待,有两个或者两个以上的线程各自持有某些互斥资源,并且各自在等待其它线程所持有的互斥资源。
我们只要不让上述四个条件中的任意一个不成立即可。在设计的时候,非常有必要先分析一下会否出现满足四个条件的情况,特别是检查有无试图去同时保持两个或者两个以上的锁,当我们发现试图去同时保持两个或者两个以上的锁的时候,就需要特别警惕了。下面我们来看一个简化了的死锁的例子:

static mutex g_mutex1, g_mutex2;
static void
inc1(int *p ){
 for(int i = 0; i < COUNT; i++){
 g_mutex1.lock();
 (*p)++;
 g_mutex2.lock();
 // do something.
 g_mutex2.unlock();
 g_mutex1.unlock();
 }
}
static void
inc2(int *p ){
 for(int i = 0; i < COUNT; i++){
 g_mutex2.lock();
 g_mutex1.lock();
 (*p)++;
 g_mutex1.unlock();
 // do other thing.
 g_mutex2.unlock();
 }
}
void threadMutex(void){
 int a = 0;
 thread ta( inc1, &a);
 thread tb( inc2, &a);
 ta.join();
 tb.join();
 cout << "a=" << a << endl;
}

在这个例子中,g_mutex1 和 g_mutex2 都是互斥的资源,任意时刻都只有一个线程可以持有(加锁成功),而且只有持有线程调用 unlock 释放锁资源的时候其它线程才能去持有,满足条件 1 和 3,线程 ta 持有了 g_mutex1 之后,在释放 g_mutex1 之前试图去持有 g_mutex2,而线程 tb 持有了 g_mutex2 之后,在释放 g_mutex2 之前试图去持有 g_mutex1,满足条件 2 和 4,这种情况之下,当线程 ta 试图去持有 g_mutex2 的时候,如果 tb 正持有 g_mutex2 而试图去持有 g_mutex1 时就发生了死锁。在有些环境下,可能要多次运行这个例子才出现死锁,实际工作中这种偶现特性让查找问题变难。要破除这个死锁,我们只要按如下代码所示破除条件 3 和 4 即可:

static mutex g_mutex1, g_mutex2;
static voi
inc1(int *p ){
 for(int i = 0; i < COUNT; i++){
 g_mutex1.lock();
 (*p)++;
 g_mutex1.unlock();
 g_mutex2.lock();
 // do something.
 g_mutex2.unlock();
 }
}
static void
inc2(int *p ){
 for(int i = 0; i < COUNT; i++){
 g_mutex2.lock();
 // do other thing.
 g_mutex2.unlock();
 g_mutex1.lock();
 (*p)++;
 g_mutex1.unlock();
 }
}
void threadMutex(void){
 int a = 0;
 thread ta( inc1, &a);
 thread tb( inc2, &a);
 ta.join();
 tb.join();
 cout << "a=" << a << endl;
}

在一些复杂的并行编程场景,如何避免死锁是一个很重要的话题,在实践中,当我们看到有两个锁嵌套加锁的时候就要特别提高警惕,它极有可能满足了条件 2 或者 4。

在编译的时候,请注意下述几点:

设置 -std=c++11;
链接的时候设置 -pthread;
使用 g++编译链接时设置 -Wl,–no-as-needed 传给链接器,有些版本的 g++需要这个设置;
设置宏定义 -D_REENTRANT,有些库函数是依赖于这个宏定义来确定是否使用多线程版本的。

C++11 同步

为何要使用同步?在多线程并发控制中,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。

同步/异步,堵塞/非堵塞

同步/异步, 它们是消息的通知机制

同步
所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。
简单来说就是当前程序执行完才能执行后面的程序,程序执行时按照顺序执行,平时写的代码基本都是同步的;

异步
异步的概念和同步相对。
当一个异步过程调用发出后,调用者不会立刻得到结果。实际处理这个调用的部件是在调用发出后,通过状态、通知来通知调用者,或通过回调函数处理这个调用。
简单来说就是程序没有等到上一步程序执行完才执行下一步,而是直接往下执行,前提是下面的程序没有用到异步操作的值,异步的实现方式基本上都是多线程(定时任务也可实现,但是情况少)。

阻塞/非阻塞, 它们是程序在等待消息(无所谓同步或者异步)时的状态

阻塞
阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。
有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。
对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。

非阻塞
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回

以IO为例,OS里面有内核态用户态两种,程序进行IO操作的时候一般是两步,第一步是IO初始化也就是准备好IO操作,第二步就是真正的IO操作。其中第一步决定同步还是异步,第二步决定堵塞还是非堵塞的。

  1. 同步堵塞IO
  2. 同步非堵塞IO
  3. 异步堵塞IO
  4. 异步非堵塞IO

同步堵塞IO:就是最最普通的write/read操作,以读操作为例, 程序调用内核接口,然后等待系统返回,内核做读写初始化操作(寻址,读取原始数据到内核等),接着将数据读出到内核态,然后把数据从内核态拷贝到用户态下并返回结果给程序,然后程序才知道完成整个读操作,在内核初始化到将数据从存储介质取出到内核态内存中的这段时间程序就是一直等待,期间CPU的状态是wait的,夸张一点,如果内核调用一次读的操作是1秒的话,那么这一秒内程序是将CPU占着不做任何事情的,是不是感觉好浪费.

同步非堵塞IO:这里再内核调用里面会有一个设置参数叫O_NONBLOCK,使用这个的时候就会变成了非堵塞的调用,(其实从这个参数差不多可以猜测出堵塞和非堵塞是针对内核调用来说的,因为这个参数直接作用的就是内核调用),这里会有一个状态通知的概念,程序不会立即完成IO操作,还是以读操作为例,程序首先向内核发出一个读操作的调用,这个时候系统会在很短的时候返回一个状态,表示读操作是否初始化完成,大多数时候是返回不能读初始化(因为有资源竞争),接着程序会继续像死循环一个的发起这个请求直到内核返回读操作已经准备好(记住死循环的时候程序实际上被读这个操作的时候实际不能做别的事情,所以是同步的),这个时候程序在才能将数据从内核态拷贝到用户态后并且返回,这个读过程就算完成了.虽然内核态是是非堵塞的,但是程序这个线程里面在完成读操作之前也没有能做别的事情,所以这里还是同步的.相对同步堵塞IO来说,这种方式可以让程序不用占着CPU时间也不给别人用的情况.,另外由于操作是循环调用查看状态的,所以再内核准备好数据到程序下一次查看状态之间存在延时,这样会导致系统的吞吐量下降.

异步堵塞IO:这里就是我们java里面用到的Select或者Clib里面的poll来实现的,这里面实际也用到上面提到的非阻塞的参数,只是采用Select这种阻塞的方式来调用非阻塞的内核调用.虽然读和写的操作并没有被堵塞住,而是由Select中获取了I/O从操作符再进行下一步操作,这里的数据真正的操作还是被堵塞住了,但是这个的好处就是可以同时再一个线程里面对多个IO操作进行处理,这种顺序处理的效率确实不高,再有的程序里面会启动多个线程来做Select的操作提高性能.这个一个的好处就是再同一个线程内可以有多个IO操作同时进行,他不能提高单个IO的吞吐量,但是可以提高程序的IO并发能力,从而提高整体的IO吞吐量.

异步非堵塞IO:这个厉害了,这就是我们提得比较热的一个概念,叫AIO,他采用的是回调的方式实现异步操作,然后也使用非堵塞的参数,这样一来程序不需要死循环来监听IO操作符,内核准备好了就会通知程序做对应的事情,而程序再读写等待的时候就可以把剩下的CPU时间用在做别的业务处理.这个的好处就太多了,首先他吧CPU时间让出来了,可以做别的事情,其次是回调方式的,只要数据准备好就可以回调让程序把数据从内核拷贝走.坏的地方么我只能说回调对于程序员的编码要求确实有点高的.

所以总结一下这四个方式:同步堵塞IO在IO操作开始的时候就堵塞程序,这个时候还不能重复的进行别的IO操作,同步非堵塞IO解决了内核可以同时处理来自程序的多个IO请求.而异步堵塞主要是解决在一个线程里面处理多个IO的操作,他并没有对IO进行堵塞,但是对事件进行了堵塞(Select实际上实在选择事件),最后的异步非堵塞IO,实现了内核非堵塞和回调通知的方法,让IO的操作更高效。

那是不是异步非堵塞是最好的呢,这个不会一定,我们在做大数据的时候会知道程序有CPU密集型和IO密集型两种区别,至于那种类型使用哪种方式的IO模式这个就需要看攻城狮自己的判断了

进程与线程的空间分配问题

根据CPU核心数确定线程池并发线程数

第一派:《Java Concurrency in Practice》即《java并发编程实践》
在这里插入图片描述
如上图,在《Java Concurrency in Practice》一书中,给出了估算线程池大小的公式:

Nthreads=NcpuUcpu(1+w/c),其中

Ncpu=CPU核心数

Ucpu=cpu使用率,0~1

W/C=等待时间与计算时间的比率

第二派:《Programming Concurrency on the JVM Mastering》即《Java 虚拟机并发编程》
在这里插入图片描述
线程数=Ncpu/(1-阻塞系数)

对于派系一,假设cpu100%运转,即撇开CPU使用率这个因素,线程数=Ncpu*(1+w/c)。

现在假设将派系二的公式等于派系一公式,即Ncpu/(1-阻塞系数)=Ncpu*(1+w/c),===》阻塞系数=w/(w+c),即阻塞系数=阻塞时间/(阻塞时间+计算时间)。

那么实际使用中并发线程数如何设置呢?分析如下(我们以派系一公式为例):

Nthreads=Ncpu(1+w/c)*

IO密集型:一般情况下,如果存在IO,那么肯定w/c>1(阻塞耗时一般都是计算耗时的很多倍),但是需要考虑系统内存有限(每开启一个线程都需要内存空间),这里需要上服务器测试具体多少个线程数适合(CPU占比、线程数、总耗时、内存消耗)。如果不想去测试,保守点取1即,Nthreads=Ncpu*(1+1)=2Ncpu。这样设置一般都OK。

计算密集型:假设没有等待w=0,则W/C=0. Nthreads=Ncpu。

IO密集型=2Ncpu(可以测试后自己控制大小,2Ncpu一般没问题)(常出现于线程中:数据库数据交互、文件上传下载、网络数据传输等等)

计算密集型=Ncpu(常出现于线程中:复杂算法)

WINDOWS操作系统中可以允许的最大线程数:

默认情况下,一个线程的栈要预留1M的内存空间
而一个进程中可用的内存空间只有2G,所以理论上一个进程中最多可以开2048个线程 。但是内存当然不可能完全拿来作线程的栈,所以实际数目要比这个值要小。你也可以通过连接时修改默认栈大小,将其改的比较小,这样就可以多开一些线程。 如将默认栈的大小改成512K,这样理论上最多就可以开4096个线程。
即使物理内存再大,一个进程中可以起的线程总要受到2GB这个内存空间的限制。 比方说你的机器装了64GB物理内存,但每个进程的内存空间还是4GB,其中用户态可用的还是2GB。
如果是同一台机器内的话,能起多少线程也是受内存限制的。每个线程对象都要站用非页面内存,而非页面内存也是有限的,当非页面内存被耗尽时,也就无法创建线程了。
如果物理内存非常大,同一台机器内可以跑的线程数目的限制值会越来越大。
在Windows下写个程序,一个进程Fork出2000个左右线程就会异常退出了,为什么?这个问题的产生是因为windows32位系统,一个进程所能使用的最大虚拟内存为2G,而一个线程的默认线程栈StackSize为1024K(1M),这样当线程数量逼近2000时,2000*1024K=2G(大约),内存资源就相当于耗尽。
如何突破2000个限制?
可以通过修改CreateThread参数来缩小线程栈StackSize

Linux下进程栈和线程栈:

Linux虚拟地址空间布局以及进程栈和线程栈总结
原文链接:http://blog.csdn.net/freeelinux/article/details/53782986【侵删】

1.进程栈大小时执行时确定的,与编译链接无关
2.进程栈大小是随机确认的,至少比线程栈要大,但不会超过2倍
3.**线程栈(一个线程可用的栈大小)**是固定大小的,可以使用ulimit -a 查看,使用ulimit -s 修改
4.一般默认情况下,线程栈是在进程的堆中分配栈空间,每个线程拥有独立的栈空间,为了避免线程之间的栈空间踩踏,线程栈之间还会有以小块guardsize用来隔离保护各自的栈空间,一旦另一个线程踏入到这个隔离区,就会引发段错误。
查看线程栈大小:
在这里插入图片描述
可以看到默认情况下线程栈大小为8192(8MB),可以使用ulimit -s xxx修改线程默认栈大小。
其他修改线程栈与验证的过程见https://www.jianshu.com/p/654a4de9e56e

CPU的核心数、线程数的关系和区别

我们在选购电脑的时候,CPU是一个需要考虑到核心因素,因为它决定了电脑的性能等级。CPU从早期的单核,发展到现在的双核,多核。CPU除了核心数之外,还有线程数之说,下面笔者就来解释一下CPU的核心数与线程数的关系和区别。
  简单地说,CPU的核心数是指物理上,也就是硬件上存在着几个核心。比如,双核就是包括2个相对独立的CPU核心单元组,四核就包含4个相对独立的CPU核心单元组,等等,依次类推。
  线程数是一种逻辑的概念,简单地说,就是模拟出的CPU核心数。比如,可以通过一个CPU核心数模拟出2线程的CPU,也就是说,这个单核心的CPU被模拟成了一个类似双核心CPU的功能。我们从任务管理器的性能标签页中看到的是两个CPU。
  比如Intel 赛扬G460是单核心,双线程的CPU,Intel 酷睿i3 3220是双核心 四线程,Intel 酷睿i7 4770K是四核心 八线程 ,Intel 酷睿i5 4570是四核心 四线程等等。
  对于一个CPU,线程数总是大于或等于核心数的。一个核心最少对应一个线程,但通过超线程技术,一个核心可以对应两个线程,也就是说它可以同时运行两个线程。
  CPU的线程数概念仅仅只针对Intel的CPU才有用,因为它是通过Intel超线程技术来实现的,最早应用在Pentium4上。如果没有超线程技术,一个CPU核心对应一个线程。所以,对于AMD的CPU来说,只有核心数的概念,没有线程数的概念。
  CPU之所以要增加线程数,是源于多任务处理的需要。线程数越多,越有利于同时运行多个程序,因为线程数等同于在某个瞬间CPU能同时并行处理的任务数。
  在Windows中,在cmd命令中输入“wmic”,然后在出现的新窗口中输入“cpu get *”即可查看物理CPU数、CPU核心数、线程数。其中,
  Name:表示物理CPU数
  NumberOfCores:表示CPU核心数
  NumberOfLogicalProcessors:表示CPU线程数
在Java中通过Runtime.getRuntime().availableProcessors();获得OS线程数,

查看电脑支持的核心数和线程数在电脑上也能看,通过查看电脑属性选项即可。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值