多线程实践概述

目录

一、线程背景

一、线程与进程

二、线程属性

2.1 线程ID

2.2 获取线程ID

二、Linux同步对象

2.1 Linux互斥量

2.2 条件变量

三、C++11同步对象

3.1  独占性互斥量 std::mutex

3.2 递归互斥量 std::recursive_mutex

3.4 条件变量


一、线程背景

        在单核CPU时代,程序的执行效率单纯的依赖更快的硬件;随着时间的变化,在2005年3月,Herb Sutter在Dobb's Journal发表了《The Free Lunch is over: A Fundamental Turn Toward Concurrency is Software》一文,文章分析处理器厂商改善CPU性能的传统方法,如提升时钟速度和指令吞吐量的方式,遇到了瓶颈,处理器发展方向趋向于超线程和多架构模式。

         为了让代码执行的更快、更高效,开发人员可通过编写并发代码,充分大会多核处理器的优势,使程序性能得到提升。

一、线程与进程

        线程与进程有着千丝万缕的联系,两者密不可分。在Linux下程序或可执行文件只是一组指令的集合,没有执行的含义。进程拥有自己独立的进程地址空间和上下文堆栈,但对一个程序本身的执行操作来说,进程不执行任何代码,只是提供一个大环境容器,进程中实际执行体是线程,因此每个进程中至少的有一个线程,即主线程。

        多进程、多线程都能做并发,两者的差异何在?

        进程之间,彼此的地址空间是独立的,但线程会共享内存地址空间。同一个进程的多个线程共享一份全局内存区域,包括初始化数据段、未初始化数据段和动态分配的堆内存。

  

 这种共享方式给线程带来了很多优势:

  • 创建线程花费的时间少于创建进程花费的时间
  • 线程间的上下文切换小于进程间的上下文切换
  • 线程间数据共享比进程之间的共享要简单

线程的缺点:

  • 多个线程中只要有一个线程出现BUG,就会出现SegmenFault导致进程退出,相比之下进程的地址空间互相独立,多个进程通过进程间通信(IPC),一个进程的异常退出不会影响其他进程
  • 多线程控制流、时许很难控制,每次执行都不一样

        多进程和多线程都能够很好的发挥出多核处理器的性能,两者孰优孰劣需要依赖具体的应用场景去抉择。

二、线程属性

2.1 线程ID

        在内核2.6以前的调度实体都是进程,内核并没有真正执行线程,为了遵循POSIX标准,Redhat发布了NPTL(native posix thread library)库,它的实现方式是,在内核中线程仍然被当作进程,也是用task_struct进行描述,这种线程又叫LWP(用户态线程)。

Struct 	task_struct {		
    …
	pid_t	pid;	//线程id
	pid_t	tgid;	//Thread Group ID(对应用户层的进程ID)

	…
	struct	task_struct	*group_leader;
	struct	list_head	thread_group;

	…
}

        没有线程之前,一个进程对应内核的一个进程描述符,对应一个进程ID。但是引入线程概念后,情况发生了变化,一个进程下管辖的N个线程。每个线程作为一个独立的调度实体在内核中有自己的进程描述符。进程和内核的进程描述符由1:1变成1:n。POSIX标准要求所有线程调用getpid函数返回相同的进程ID。为了解决这个问题,引入了线程组的概念。

        线程组内的第一个线程,在用户态被称为主线程,在内核中被称为Group Leader.内核在创建第一个线程时,会将线程组ID的值设置成第一个线程ID,group_leader指针则指向自身,即主线程的进程描述符。

//线程组ID等于主线程ID
p->tgid = p->pid;
p->group_leader = p;
INIT_LIST_HEAD(&p->thread_group);

        通过group_leader指针,每个线程都能够找到主线程,主线程中存在一个链表头结构,后面创建的每一个线程都会被链入到该双向链表中。通过链表结构能偶遍历组内其他线程,也能够找到主线程。

Linux线程ID:

        用ps -eLf查询得,进程syslog(680)组内有4个线程,每个线程的ID号分别为680、703、704、705共4个线程。

yp@ubuntu:~$ ps -eLf
UID         PID   PPID    LWP  C NLWP STIME TTY          TIME CMD
........
syslog      680      1    680  0    4 01:02 ?        00:00:00 /usr/sbin/rsyslogd -n
syslog      680      1    703  0    4 01:02 ?        00:00:00 /usr/sbin/rsyslogd -n
syslog      680      1    704  0    4 01:02 ?        00:00:00 /usr/sbin/rsyslogd -n
syslog      680      1    705  0    4 01:02 ?        00:00:00 /usr/sbin/rsyslogd -n

         在已知进程ID前提下,可在/proc/PID/task目录下的子目录来查看进程内的线程个数及其线程ID。示例如下:

yp@ubuntu:~$ ll /proc/680/task/
total 0
dr-xr-xr-x 6 syslog syslog 0 Jul  5 03:36 ./
dr-xr-xr-x 9 syslog syslog 0 Jul  5 01:02 ../
dr-xr-xr-x 7 syslog syslog 0 Jul  5 03:36 680/
dr-xr-xr-x 7 syslog syslog 0 Jul  5 03:36 703/
dr-xr-xr-x 7 syslog syslog 0 Jul  5 03:36 704/
dr-xr-xr-x 7 syslog syslog 0 Jul  5 03:36 705/

2.2 获取线程ID

  • 调用pthread_create函数时,函数调用成功,通过第一个参数返回
#include <pthread.h>

pthread_t   tid;
pthread_create(&tid, NULL, thread_proc, NULL);
  • 在对应线程中调用pthread_self()
#include <pthread.h>
 pthread_t   tid = pthread_self();
  • 系统调用
#include <sys/syscall.h>
int tid = syscall(SYS_gettid);

        pthread_t输出的是一块内存地址空间,由于进程的虚拟地址与物理地址的映射关系,pthread_t类型的线程ID可能不是全系统唯一的。通过syscall(SYS_gettid)获取的线程ID是系统范围内全局唯一的。

二、Linux同步对象

2.1 Linux互斥量

        Linux互斥量与Windows的临界区对象用法相似,也是通过限制多个线程同时访问临界资源以达到保护的目的。Linux互斥量在NPTL中实现,使用数据结构pthread_mutex_t表示一个互斥体对象。

互斥量初始化

(1) 使用PTHREAD_MUTEX_INITIALIZER直接给互斥体变量赋值

#include <pthread.h>
pthread_mutex_t = PTHREAD_MUTEX_INITIALIZER;

(2) 若互斥量是动态分配或者需要设置其属性,则使用pthread_mutex_init函数

int pthread_mutex_init(pthread_mutex_t* restrict_mutex, 
                       const pthread_mutexattr_init* restrict_attr)

        当我们不在需要一个互斥体对象时,可以使用pthread_mutex_destroy函数来销毁它,函数签名如下:

int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_trylock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread_mutex_t* mutex);

设置互斥量的属性

        设置互斥体属性时需要创建一个pthread_mutexattr_t类型的对象,并用pthread_mutexattr_init进行初始化。

#include <pthread.h>

int main()
{
    ...

    pthread_mutexattr_t mtx_attr;
    pthread_mutexattr_init(&mtx_attr);
    pthread_mutexattr_settype(&mtx_attr, PTHREAD_MUTEX_NORMAL);  //普通锁
    pthread_mutex_init(&g_mutex, &mtx_attr);

    ...
}

互斥体的加锁解锁

        互斥量一般配合加锁、解锁操作完成对临界资源的安全访问,Linux相关函数签名如下:

int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_trylock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread_mutex_t* mutex);

2.2 条件变量

1. 为什么使用环境变量?

        不使用条件变量的情形,伪代码如下:

pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

int WaitForTrue()
{
    pthread_mutex_lock(&mtx);

    while (condition is false)          // 条件不满足
    {
        pthread_mutex_unlock(&mtx);     //解锁,等待其他线程改变共享数据
        sleep(n);                       //睡眠等待,再次加锁验证条件是否满足
        pthread_mutex_lock(&mtx);       
    }
}

        这段代码对共享资源进行访问时存在严重的效率问题。假设等待的条件突然满足了,但是while循环仍然会消耗一下时间,响应不及时。

         因此需要一种机制:线程在条件不满足的时候,主动让出互斥量,让其他线程执行,其他线程发现条件满足后向其发送信号,就可以立即唤醒它。

2. 条件变量的使用

        环境变量的初始化和销毁,使用如下API函数:

int pthread_cond_init(pthread_cond_t* cond, const pthread_condattr_t* attr);
int pthread_cond_destory(pthread_cond_t* cond);

        可使用如下API函数等待条件变量被唤醒:

int pthread_cond_wait(pthread_cond_t* restrict cond, pthread_mutex_t* restrict mutex);
int pthread_cond_timedwait(pthread_cond_t* restrict cond, pthread_mutex_t* restrict mutex
                           const struct timespec* restrict abstime);

        如果条件变量等待的条件没有被满足,则条用pthread_cond_wait的线程会一直阻塞,pthread_cond_timedwait是带超时时间的阻塞,超过abstime设置的时间后,立即返回。

        因调用pthread_cond_wait而阻塞的线程可被如下API唤醒:

int pthread_cond_signal(pthread_cond_t* cond);
int pthread_cond_broadcast(pthread_cond_t* cond);

        如果多个线程阻塞在pthread_cond_wait,由于pthread_cond_signal一次只能唤醒一个线程,具体那个线程被唤醒是随机的。pthread_cond_broadcast可以唤醒所有等待的线程。

3. 条件变量虚假唤醒

if (tasks.empty()) {
    pthread_cond_wait(&cv, &mutex);
}
while (tasks.empty()) {
    pthread_cond_wait(&cv, &mutex);
}

       没有其他线程向条件变量发送信号,但等待此条件变量的线程可能被唤醒,即操作系统唤醒pthread_cond_wait时,tasks.empty()仍然为true,这种情形称为虚假唤醒。因此需要在while循环中,当条件变量被唤醒时,还需要判断条件是否满足。

三、C++11同步对象

3.1  独占性互斥量 std::mutex

       互斥量的使用一般是通过lock()方法来阻塞线程,直到获得互斥量的所有权为止。在线程获得互斥量并完成任务后,就必须使用unlock()来解除对互斥量的占用,lock ()和unlock()必须成对出现。代码示例如下:

#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

std::mutex g_lock;
void ThreadProc()
{
    g_lock.lock();

    std::cout << "entered thread " << std::this_thread::get_id() << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "leaving thread " << std::this_thread::get_id() << std::endl;

    g_lock.unlock();
} 

int main(void)
{
    std::thread t1(ThreadProc);
    std::thread t2(ThreadProc);

    t1.join();
    t2.join();

    system("pause");
    return 0;
}

        使用lock_guard()可以简化lock/unlock写法,避免忘记unlock()。lock_guard()出作用域会自动解锁。

void ThreadProc()
{
    std::lock_guard<std::mutex> locker(g_lock);

    std::cout << "entered thread " << std::this_thread::get_id() << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "leaving thread " << std::this_thread::get_id() << std::endl;

} 

3.2 递归互斥量 std::recursive_mutex

#include <iostream>
#include <thread>
#include <mutex>
#include <unistd.h>

int g_resourceID = 1;
std::recursive_mutex mtx;

void ThreadFunc_2() {
    mtx.lock();                     //第二次上锁

	g_resourceID = g_resourceID * g_resourceID ;
	printf("g_resourceID = %d \n", g_resourceID );
	mtx.unlock();
}

void ThreadFunc_1() {
	mtx.lock(); 

	g_resourceID = g_resourceID + 1;
	ThreadFunc_2();
	printf("g_resourceID = %d \n", g_resourceID );

	mtx.unlock();
}

int main() {
	std::thread t1(ThreadFunc_1);
	t1.join();

	return 0;
}

        递归互斥锁可对同一互斥量多次加锁,可以用来解决同一线程对互斥量多次加锁导致死锁阻塞的问题。

3.4 条件变量

         C++11使用condition_variable类表示条件变量,与上文介绍的Linux系统原生的条件变量一样,此外还提供了等待条件满足的wait方法(wait、wait_for、wait_until)。发送信号使用notify方法(notify_one、notify_all)。与Linux系统的条件变量相比,C++11的std::condition_variable不在需要显示地初始化和销毁。

#include <thread>
#include <condition_variable>
#include <mutex>
#include <list>
#include <iostream>

template<typename T>
class SimpleSyncQueue
{
    public:
    SimpleSyncQueue() { }
    void Put(const T& x)
    {
        std::lock_guard<std::mutex> locker(m_mutex);
        m_queue.push_back(x);
        m_notEmpty.notify_one();
    } 
    void Take(T& x)
    {
        std::unique_lock<std::mutex> locker(m_mutex);
        m_notEmpty.wait(locker, [this] {
            return !m_queue.empty(); }
        );

        x = m_queue.front();
        m_queue.pop_front();
    } 

private:
    std::list<T> m_queue;
    std::mutex m_mutex;
    std::condition_variable m_notEmpty;
};

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值