多线程关键思想

Mutex

使用mutex时,注意几个原则,可以减少代码错误:

  • 使用RAII原则对mutex进行封装,封装其创建、销毁、加锁、解锁的操作。
  • 只使用不可重入的mutex
  • 不手动调用lockunlock,一切让在栈上的Guard对象的构造和析构函数完成.Guard对象的生命周期恰好等于临界区.
  • 注意加锁顺序
  • 不使用跨进程的mutex,进程间通信只用TCP sockets
  • 加锁,解锁操作在同一个线程完成,避免跨线程
  • 不要重复加锁与解锁(RAII自动保证)

non-recursive mutex

non-recursive mutex也称为不可重入锁.在同一个进程对其重复加锁会造成死锁,(想象一下,加第一锁成功了,加第二次锁,需要等待第一次锁释放,但是其永远不会释放了)


void *f1(void *p)
{
    pthread_mutex_lock(&lock);
    pthread_mutex_lock(&lock);
    printf("Hello 1\n");
    pthread_mutex_unlock(&lock);

    pthread_exit(nullptr);
}

void *f2(void *p)
{
    pthread_mutex_lock(&lock);
    printf("Hello 2\n");
    pthread_mutex_unlock(&lock);

    pthread_exit(nullptr);
}

int main()
{
    pthread_mutex_init(&lock, NULL);
    pthread_t p1, p2;
    pthread_create(&p1, NULL, f1, NULL);
    pthread_create(&p2, NULL, f2, NULL);
    getchar();
    printf("END\n");
}

就会永远卡在f1中. 不可重入锁会将潜在的问题直接暴露出来(直接死锁),方便调试.

解决二次加锁问题:提供xxxWithLockHold 版本
如:

struct Foo{
    void f1(){
        lock();
        print();
    }

    void print(){
        lock();
        // doSomething
    }
};

调用f1就会死锁,我们可以提供print的其他版本:

struct Foo{
    void f1(){
        lock();
        printWithLockHold();
    }

    void print(){
        lock();
        printWithLockHold();
    }

    void printWithLockHold(){
        // doSomething
    }
};

条件变量

注意使用方式,就几乎不会出错.
wait端

  • 必须与mutex一起使用,该布尔表达式的读写受此互斥锁的保护
  • 在mutex上锁的时候才能调用wait()
  • 把判断布尔条件和wait()放到while循环中

示例代码

MutexLock mutex;
Condition cond(mutex);
queue<int> queue;
int dequeue(){
	MutexLockGuard(mutex);
	while(queue.empty()){ // 这个是判断条件,写为while
		cond.wait(); // 这一步会原子地unlock mutex并进入等待,wait执行完毕会重新加锁
	}
	assert(!queue.empty());
	int top = queue.front();
	queue.pop_front();
	return top;
}

必须用while来等待条件变量,而不是if语句,是为了防止假唤醒(spurious wakeup) 即:判断条件写成while
signal/broadcast端

  • 理论上不一定要在mutex上锁的情况下调用signal
  • 在signal之前一般修改布尔表达式
  • 修改布尔表达式一般需要mutex来保护
  • 注意区分broadcast和signal, broadcast一般表示状态变化,signal一般表示资源可用
    示例代码
void enqueue(int x){
	MutexLockGuard lock(mutex);
	queue.push_back(x);
	cond.notify();
}

配合上面就实现了一个简单的BlockingQueue.

示例,计数器

class Counter{
private:
	int cnt;
	mutable MutexLock mutex;
	Condition cond;
public:
	void wait(){
		MutexLockGuard lock(mutex);
		while(cnt > 0){
			condition.wait();
		}
	}

	void countDown(){
		MutexLockGuard lock(mutex);
		--cnt;
		if(cnt == 0){
			// 表示状态变化
			condition.notifyAll();
		}
	}
}

非必要不使用读写锁和信号量

  • 从正确性方面来说,一种典型的易犯错误是在持有read lock的时候修改了共享数据。这通常发生在程序的维护阶段。程序员不小心在原来read lock保护的函数中调用了会修改状态的函数
  • 从性能方面来说,读写锁不见得比普通的 mutex_ 更高效。因为它要更新 reader 的数目。如果临界区很小,锁竞争不激烈,那么 mutex 往往会更快。

Example,互斥锁+shared_ptr实现读写锁的功能

读取的时候获取对应的shared_ptr副本(获取的时候需要加锁,获得副本之后即释放锁),然后就可以读取数据

全程需要加锁 写前先检查智能指针的计数是否为1

  • 为1的话说明没有其他线程在读,可以直接修改数据
  • 不为1说明有其他线程在读,创建一个对应智能指针的副本,与原始智能指针交换,然后在交换后的数据上修改

存在的问题

读取的数据可能不是最新的:其他线程获得了数据getData(),然后读取中,这个时候某线程修改了数据,但是之前的线程读取的还是先前的数据.

#include "../base/Lock.h"
#include "stdio.h"
#include "map"
#include "vector"
#include "memory"
class CustomerData
{
private:
    using Entry = std::pair<std::string, int>;    // stock-时间
    using EntryList = std::vector<Entry>;         // 不同stock的最小交易间隔,已经排好序
    using Map = std::map<std::string, EntryList>; // 用户名-EntryList
    using MapPtr = std::shared_ptr<Map>;

    void update(const std::string &customer, const EntryList &ent);
    // 在EntryList中寻找股票
    static int findEntry(const EntryList &en, const std::string &stock);

    // 获取数据,对应读锁
    MapPtr getData()
    {
        mymuduo::MutexLockGuard lock(mutex);
        // 这里返回一个shared_ptr副本,在读的话,那么当前shared_ptr的use_count大于1
        return data;
    }
    mutable mymuduo::MutexLock mutex;
    MapPtr data;

public:
    CustomerData() : data(new Map), mutex() {}

    int query(const std::string &customer, const std::string &stock);
};

int CustomerData::query(const std::string &customer, const std::string &stock)
{
	// 获取其副本,计数+1,在获取副本的时候需要锁
    MapPtr temp = getData();
    // 不需要锁了
    Map::const_iterator it = temp->find(customer);
    return (it != temp->end()) ? findEntry(it->second, stock) : -1;
}

void CustomerData::update(const std::string &customer, const EntryList &ent)
{
    mymuduo::MutexLockGuard lock(mutex);
    if (!data.unique())
    {
        MapPtr newData = std::make_shared<Map>(*data);
        data.swap(newData);
    }
    assert(data.unique());
    (*data)[customer] = ent;
}

推荐的多线程编程模型

one (event) loop per thread

一般配合非阻塞IO和IO多路复用使用。
在此模型下,每个IO线程都有一个事件循环(event loop),来处理读写任务和定时任务。代码框架如下:

while(run){
	int number = epoll_wait(epollfd, events, MAX_EVENTNUM, -1);
	if(number < 0 && errno != EAGAIN){
		printf("EPOLL FAILURE\n");
        break;
	}
	for(int i = 0; i < number; i++){
		int sockfd = events[i].data.fd;
		if(sockfd == listenfd){
			// ...
		}
		// .....
	}
}

优势在于:

  • 线程数目固定,不需要频繁创建与销毁
  • IO事件发生的线程是固定的,同一个TCP连接不用考虑事件并发
  • 方便调度线程

线程池

线程池可以避免线程的频繁创建与销毁,减少了线程切换的问题。如果使用线程池来处理IO事件的话,需要配合EPOLLONESHOT使用,使得epoll事件只响应一次。
BlockingQueue<T>可以实现数据的消费者生产者队列,可以将事件放到阻塞队列上,线程池来争用,从而实现服务,缺点是不能保证每个socket都是由同一个线程服务的。

推荐模式

  • event loop用作IO多路复用,配合非阻塞IO和定时器
  • thread pool用作计算任务

进程间通信

一般进程间通信的可选技术是:pipe,消息队列,socket,共享内存,信号量,信号。 在陈硕认为仅使用TCP SOCKET是足够的,这里罗列其优势:

  • 减少项目复杂度
  • 在使用分布式项目时,共享内存需要通过网络同步,这时候不如使用socket
  • TCP socket的调试工具较多,可以检测进程间通信(wireshark)
  • 跨语言方便,与Java、Python等其他服务写的程序交互简单
  • 基于字节流,可选Protocol Buffers作为序列化和反序列化技术
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值