二.
int front()
{
return beg->data;
}
如果调用front()操作得时候,beg刚好被删除,就可能造成很严重得问题。如果front()和pop_front()都只在一个读线程中使用,问题不大。如果将这两个接口得功能合并,如下:
int pop_front()
{
int ret = beg->data
ListNode* oldBeg = beg;
beg = beg->next;
delete oldBeg;
return ret;
}
好像使用起来更方便。但考虑到我们最终实现得列表应该可以保存任何类型得数据,如果自定义得类型在函数退出时候执行拷贝构造函数时出现异常,那么列表状态无法恢复了。也就是说这个接口不是异常安全的接口。考虑这一点,还是分为两个独立的接口更好一些。
当然在使用pop_front和front之前必须确保列表不为空:
boolean empty()
{
return beg == end;
}
如果将非空判断放到front内,那么为空得时候应该返回什么值?不知道。最好是用户自己明白这种情况下的风险。
如果存在多个读线程,那么front()和pop_front(),empty()都需要利用同一个互斥量保证线程同步。Push_back不应该和上面上个接口在同一个线程中调用。如果只是这么一个书面规范,实际应用中也很容易因为疏忽没有遵守这个规则。因此我们可以考虑分别提供两个不同的界面给读线程和写线程,这就使用Adaptor模式了,如下:
class WriteList
{
private:
List& list;
public:
WriteList(list& list): this.list(list){}
void push_back(int data) { list.push_back(data); }
};
class ReadList
{
private:
List& list;
public:
ReadList(list& list): this.list(list){}
void pop_front() { list.pop_front(); }
int front() { return list.front(); }
boolean empty() { return list.empty(); }
}
这样读取数据的线程只看到ReadList, 写数据的线程只能看到WriteList,就可以防止使用人员误用这4个接口。
下面考虑查询列表大小的接口:
int size()
{
int size = 0;
ListNode* ptmp = beg;
for(ListNode* ptmp = beg; ptmp != end; ptmp = ptmp.next, size);
return size;
}
这个操作过程需要访问beg,end指向的内存空间中保存的数据,但是pop_front的实现可能导致被访问的beg已经执行无效区域,为了确保代码安全,所有线程调试信息中需要输出列表大小的地方都要和pop_front一样使用同一个互斥量,这既造成性能问题,又有使用上的不便。很多时候写线程是需要调用这个接口的,比如要限制线程的最大元素数量的时候或者调试信息需要。为了解决不用加锁,可以访问size接口,需要保证已经删除的节点的内存区域的next数据可用,也就是说,删除节点只清除节点保存的数据区域,其它数据都是可用的。如果不释放内存区域,必然导致内存泄漏,我们可以考虑将这些已经释放的节点用于保存push_back写入的数据。
一个环行的链表可以解决这个问题。
Struct ListNode
{
int data ;
ListNode* next;
ListNode(int v,ListNode* pnext = 0): data(v),next(pnext){}
}
struct List
{
ListNode* beg;
ListNode* end;
List():beg(new ListNode(0)),end(beg){end->next = end;}
…
}
首先构造一个环行链表,push_back的时候如果先前没有被释放的元素,则将新元素加到end后面,仍然维持环行链表的结构:
int front()
{
return beg->data;
}
如果调用front()操作得时候,beg刚好被删除,就可能造成很严重得问题。如果front()和pop_front()都只在一个读线程中使用,问题不大。如果将这两个接口得功能合并,如下:
int pop_front()
{
int ret = beg->data
ListNode* oldBeg = beg;
beg = beg->next;
delete oldBeg;
return ret;
}
好像使用起来更方便。但考虑到我们最终实现得列表应该可以保存任何类型得数据,如果自定义得类型在函数退出时候执行拷贝构造函数时出现异常,那么列表状态无法恢复了。也就是说这个接口不是异常安全的接口。考虑这一点,还是分为两个独立的接口更好一些。
当然在使用pop_front和front之前必须确保列表不为空:
boolean empty()
{
return beg == end;
}
如果将非空判断放到front内,那么为空得时候应该返回什么值?不知道。最好是用户自己明白这种情况下的风险。
如果存在多个读线程,那么front()和pop_front(),empty()都需要利用同一个互斥量保证线程同步。Push_back不应该和上面上个接口在同一个线程中调用。如果只是这么一个书面规范,实际应用中也很容易因为疏忽没有遵守这个规则。因此我们可以考虑分别提供两个不同的界面给读线程和写线程,这就使用Adaptor模式了,如下:
class WriteList
{
private:
List& list;
public:
WriteList(list& list): this.list(list){}
void push_back(int data) { list.push_back(data); }
};
class ReadList
{
private:
List& list;
public:
ReadList(list& list): this.list(list){}
void pop_front() { list.pop_front(); }
int front() { return list.front(); }
boolean empty() { return list.empty(); }
}
这样读取数据的线程只看到ReadList, 写数据的线程只能看到WriteList,就可以防止使用人员误用这4个接口。
下面考虑查询列表大小的接口:
int size()
{
int size = 0;
ListNode* ptmp = beg;
for(ListNode* ptmp = beg; ptmp != end; ptmp = ptmp.next, size);
return size;
}
这个操作过程需要访问beg,end指向的内存空间中保存的数据,但是pop_front的实现可能导致被访问的beg已经执行无效区域,为了确保代码安全,所有线程调试信息中需要输出列表大小的地方都要和pop_front一样使用同一个互斥量,这既造成性能问题,又有使用上的不便。很多时候写线程是需要调用这个接口的,比如要限制线程的最大元素数量的时候或者调试信息需要。为了解决不用加锁,可以访问size接口,需要保证已经删除的节点的内存区域的next数据可用,也就是说,删除节点只清除节点保存的数据区域,其它数据都是可用的。如果不释放内存区域,必然导致内存泄漏,我们可以考虑将这些已经释放的节点用于保存push_back写入的数据。
一个环行的链表可以解决这个问题。
Struct ListNode
{
int data ;
ListNode* next;
ListNode(int v,ListNode* pnext = 0): data(v),next(pnext){}
}
struct List
{
ListNode* beg;
ListNode* end;
List():beg(new ListNode(0)),end(beg){end->next = end;}
…
}
首先构造一个环行链表,push_back的时候如果先前没有被释放的元素,则将新元素加到end后面,仍然维持环行链表的结构: